Add tutorial docs and interactive playgrounds
Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -1,183 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
animateInstance,
|
||||
canonicalAstExample,
|
||||
classifyInstance,
|
||||
compileAndSwitch,
|
||||
computePipelineExample,
|
||||
connectFromWorker,
|
||||
createInstance,
|
||||
createVelocityColumn,
|
||||
customRenderPipelineExample,
|
||||
defaultPipelineExample,
|
||||
fluentGraphExample,
|
||||
fxNodeExportExample,
|
||||
importGltf,
|
||||
installCameraRenderDataControls,
|
||||
jsoGraphExample,
|
||||
loadCompleteScene,
|
||||
pickNearest,
|
||||
conventionalSceneHandles,
|
||||
restyleScene,
|
||||
setVelocity,
|
||||
simpleSceneShader,
|
||||
updateInstance,
|
||||
} from "../examples/cookbook/index.js";
|
||||
|
||||
test("cookbook graph recipes produce canonical addon-owned ASTs", () => {
|
||||
const canonical = canonicalAstExample();
|
||||
assert.equal(canonical.ast.id, "shared_dag");
|
||||
assert.equal(
|
||||
(canonical.source.match(/\(ref "source" "value"\)/g) ?? []).length,
|
||||
2,
|
||||
);
|
||||
assert.deepEqual(
|
||||
[jsoGraphExample().id, fluentGraphExample().id],
|
||||
["jso_graph", "fluent_graph"],
|
||||
);
|
||||
|
||||
const fxnode = fxNodeExportExample();
|
||||
assert.equal(fxnode.kind, "yawn-render-graph");
|
||||
assert.equal(fxnode.pipelines.render.length, 4);
|
||||
|
||||
const defaults = defaultPipelineExample();
|
||||
assert.deepEqual(
|
||||
defaults.pipelines.render.map(({ name }) => name),
|
||||
[
|
||||
"ground_plane",
|
||||
"gltf_standard",
|
||||
"gltf_standard_double_sided",
|
||||
"frame_out",
|
||||
],
|
||||
);
|
||||
assert.equal(defaults.pipelines.compute.length, 1);
|
||||
|
||||
const custom = customRenderPipelineExample();
|
||||
assert.equal(custom.pipelines.render[0].shader, simpleSceneShader);
|
||||
assert.match(simpleSceneShader, /@vertex fn vertex_main/);
|
||||
|
||||
const compute = computePipelineExample().pipelines.compute[0];
|
||||
assert.deepEqual(
|
||||
[compute.entry, compute.dispatch],
|
||||
["initialize", [4, 1, 1]],
|
||||
);
|
||||
});
|
||||
|
||||
test("cookbook graph lifecycle recipe serializes and switches", async () => {
|
||||
const calls = [];
|
||||
const core = {
|
||||
compileGraph(source) {
|
||||
calls.push(["compile", source]);
|
||||
return Promise.resolve({ compiledId: [3, 4] });
|
||||
},
|
||||
switchCompiledGraph(id) {
|
||||
calls.push(["switch", id]);
|
||||
return Promise.resolve();
|
||||
},
|
||||
dropCompiledGraph(id) {
|
||||
calls.push(["drop", id]);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
const graph = { id: "lifecycle", revision: 1, nodes: [] };
|
||||
const compiled = await compileAndSwitch(core, graph);
|
||||
assert.match(calls[0][1], /^\(yawn-graph 1/);
|
||||
assert.deepEqual(calls.slice(1), [["switch", [3, 4]]]);
|
||||
});
|
||||
|
||||
test("cookbook mutation recipes use handles and SOA writes directly", async () => {
|
||||
const calls = [];
|
||||
const column = {
|
||||
write(slot, values) {
|
||||
calls.push(["velocity", slot, values]);
|
||||
},
|
||||
};
|
||||
const core = {
|
||||
allocateArray(layout) {
|
||||
calls.push(["allocate", layout]);
|
||||
return Promise.resolve(column);
|
||||
},
|
||||
setInstanceTransform(handle, transform) {
|
||||
calls.push(["transform", handle, transform]);
|
||||
},
|
||||
setInstanceType(handle, words) {
|
||||
calls.push(["type", handle, words]);
|
||||
},
|
||||
};
|
||||
const instance = {
|
||||
handle: [7, 2],
|
||||
setTransform(transform) {
|
||||
calls.push(["wrapped-transform", transform]);
|
||||
},
|
||||
setType(words) {
|
||||
calls.push(["wrapped-type", words]);
|
||||
},
|
||||
};
|
||||
const mesh = {
|
||||
createInstance(transform) {
|
||||
calls.push(["create", transform]);
|
||||
return Promise.resolve(instance);
|
||||
},
|
||||
};
|
||||
const transform = Array.from({ length: 16 }, (_, index) => index);
|
||||
const words = Array(16).fill(1);
|
||||
|
||||
assert.equal(await createInstance(mesh, transform), instance);
|
||||
updateInstance(instance, transform, words);
|
||||
const velocity = await createVelocityColumn(core);
|
||||
setVelocity(velocity, instance, [1, 2, 3]);
|
||||
animateInstance(core, instance, transform);
|
||||
classifyInstance(core, instance, words);
|
||||
|
||||
assert.deepEqual(calls.find(([name]) => name === "allocate")[1], {
|
||||
name: "instance.velocity",
|
||||
domain: "instance",
|
||||
scalar: "f32",
|
||||
lanes: 4,
|
||||
});
|
||||
assert.deepEqual(
|
||||
calls.find(([name]) => name === "velocity"),
|
||||
["velocity", 7, [1, 2, 3, 0]],
|
||||
);
|
||||
assert.ok(calls.some(([name]) => name === "transform"));
|
||||
assert.ok(calls.some(([name]) => name === "type"));
|
||||
});
|
||||
|
||||
test("cookbook picking and worker-to-worker recipes use public facades", async () => {
|
||||
const picked = await pickNearest(
|
||||
{
|
||||
pickRay: async () => ({
|
||||
epoch: 5,
|
||||
hits: [{ instance: [9, 3], distance: 2 }],
|
||||
}),
|
||||
},
|
||||
[0, 0, 0],
|
||||
[0, 0, -1],
|
||||
);
|
||||
assert.deepEqual(picked.hits[0].instance, [9, 3]);
|
||||
|
||||
class Port extends EventTarget {
|
||||
postMessage() {}
|
||||
start() {
|
||||
queueMicrotask(() =>
|
||||
this.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: { type: "soa-init", arrays: [] },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
terminate() {}
|
||||
}
|
||||
const core = await connectFromWorker(new Port());
|
||||
assert.equal(core.constructor.name, "YawnCore");
|
||||
core.dispose();
|
||||
|
||||
assert.equal(typeof importGltf, "function");
|
||||
assert.equal(typeof installCameraRenderDataControls, "function");
|
||||
assert.equal(typeof conventionalSceneHandles, "function");
|
||||
assert.equal(typeof restyleScene, "function");
|
||||
assert.equal(typeof loadCompleteScene, "function");
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile, readdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
PLAYGROUND_RECIPES,
|
||||
playgroundRecipe,
|
||||
} from "../examples/playground/recipes.js";
|
||||
|
||||
const root = path.resolve(import.meta.dirname, "..");
|
||||
const docsRoot = path.join(root, "docs");
|
||||
|
||||
async function markdownFiles(directory) {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const nested = await Promise.all(
|
||||
entries.map((entry) => {
|
||||
const file = path.join(directory, entry.name);
|
||||
return entry.isDirectory()
|
||||
? markdownFiles(file)
|
||||
: entry.name.endsWith(".md")
|
||||
? [file]
|
||||
: [];
|
||||
}),
|
||||
);
|
||||
return nested.flat();
|
||||
}
|
||||
|
||||
test("playground recipes are unique, compilable, and linked to docs", async () => {
|
||||
const entries = Object.entries(PLAYGROUND_RECIPES);
|
||||
assert.equal(entries.length, 7);
|
||||
assert.equal(playgroundRecipe("unknown"), PLAYGROUND_RECIPES["first-scene"]);
|
||||
|
||||
const sources = new Set();
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
for (const [id, recipe] of entries) {
|
||||
assert.match(id, /^[a-z][a-z0-9-]+$/);
|
||||
assert.ok(!sources.has(recipe.source), `${id} must have unique editable code`);
|
||||
sources.add(recipe.source);
|
||||
assert.doesNotThrow(() => new AsyncFunction("yawn", recipe.source));
|
||||
|
||||
const relativePage = recipe.docs
|
||||
.replace(/^\/docs\//, "")
|
||||
.split("#", 1)[0]
|
||||
.replace(/\/$/, "/index");
|
||||
const markdown = await readFile(
|
||||
path.join(docsRoot, `${relativePage}.md`),
|
||||
"utf8",
|
||||
);
|
||||
assert.ok(markdown.length > 0, `${id} docs page must exist`);
|
||||
}
|
||||
});
|
||||
|
||||
test("docs contain every former recipe and only link to playground examples", async () => {
|
||||
const files = await markdownFiles(docsRoot);
|
||||
const contents = await Promise.all(files.map((file) => readFile(file, "utf8")));
|
||||
const documentation = contents.join("\n");
|
||||
|
||||
for (let number = 1; number <= 17; number++) {
|
||||
const prefix = String(number).padStart(2, "0");
|
||||
assert.match(documentation, new RegExp(`^## ${prefix} —`, "m"));
|
||||
}
|
||||
for (const id of Object.keys(PLAYGROUND_RECIPES)) {
|
||||
assert.match(documentation, new RegExp(`id=["']${id}["']`));
|
||||
}
|
||||
|
||||
const examplesIndex = await readFile(path.join(root, "examples/index.html"), "utf8");
|
||||
assert.doesNotMatch(examplesIndex, /cookbook|\.\/[^\s"']+\.js/i);
|
||||
assert.match(examplesIndex, /\/playground\//);
|
||||
});
|
||||
Reference in New Issue
Block a user