create-aura3d 1.1.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +6 -4
  4. package/dist/index.js.map +1 -1
  5. package/package.json +10 -9
  6. package/templates/{cartoon-channel → animation-channel}/README.md +11 -11
  7. package/templates/{cartoon-studio → animation-channel}/index.html +1 -1
  8. package/templates/{cartoon-channel → animation-channel}/package.json +2 -2
  9. package/templates/{cartoon-channel → animation-channel}/src/episode.ts +22 -22
  10. package/templates/{cartoon-channel → animation-channel}/src/experimental/README.md +5 -5
  11. package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.css +3 -3
  12. package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.ts +7 -7
  13. package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.css +4 -4
  14. package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.ts +6 -6
  15. package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.css +3 -3
  16. package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.ts +7 -7
  17. package/templates/{cartoon-channel → animation-channel}/src/main.ts +32 -32
  18. package/templates/{cartoon-channel → animation-channel}/src/render-plan.ts +13 -13
  19. package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.css +34 -34
  20. package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.ts +20 -20
  21. package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d-animation.spec.ts +3 -3
  22. package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d.spec.ts +6 -6
  23. package/templates/{cartoon-channel → animation-channel}/tests/image-puppet-animation.spec.ts +3 -3
  24. package/templates/{cartoon-channel → animation-channel}/tests/image-puppet.spec.ts +5 -5
  25. package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d-animation.spec.ts +3 -3
  26. package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d.spec.ts +4 -4
  27. package/templates/{prompt-cartoon-channel → animation-channel}/tests/route-health.spec.ts +1 -1
  28. package/templates/{cartoon-channel → animation-channel}/tests/sample-episode-visual.spec.ts +5 -5
  29. package/templates/{cartoon-channel → animation-channel}/tests/storyboard-playback.spec.ts +7 -7
  30. package/templates/animation-studio/AGENTS.md +71 -0
  31. package/templates/{cartoon-studio → animation-studio}/ASSET-LICENSES.md +7 -7
  32. package/templates/{cartoon-studio → animation-studio}/README.md +6 -6
  33. package/templates/animation-studio/aura.assets.json +669 -0
  34. package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
  35. package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
  36. package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
  37. package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
  38. package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
  39. package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
  40. package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
  41. package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +10284 -0
  42. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
  43. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
  44. package/templates/animation-studio/dist/generated/second-scene.document.json +1210 -0
  45. package/templates/animation-studio/dist/generated/studio-edited.document.json +1196 -0
  46. package/templates/animation-studio/dist/scene/inshader-accept.document.json +1 -0
  47. package/templates/animation-studio/dist/scene/inshader-test.document.json +1 -0
  48. package/templates/animation-studio/dist/scene/integ.document.json +1 -0
  49. package/templates/animation-studio/dist/scene/working.document.json +723 -0
  50. package/templates/animation-studio/dist/scene/working.history.json +1 -0
  51. package/templates/{cartoon-studio → animation-studio}/live-route.html +1 -1
  52. package/templates/{cartoon-studio → animation-studio}/package-lock.json +2 -2
  53. package/templates/animation-studio/package.json +32 -0
  54. package/templates/animation-studio/public/aura-assets/cast-a.catalog.glb +0 -0
  55. package/templates/animation-studio/public/aura-assets/cast-b.catalog.glb +0 -0
  56. package/templates/animation-studio/public/aura-assets/luma.catalog.glb +0 -0
  57. package/templates/animation-studio/public/aura-assets/luma2.catalog.glb +0 -0
  58. package/templates/animation-studio/public/aura-assets/miko.catalog.glb +0 -0
  59. package/templates/{cartoon-studio → animation-studio}/public/aura-assets/moonGarden.gltf +1 -1
  60. package/templates/animation-studio/public/aura-assets/mushroom.catalog.glb +0 -0
  61. package/templates/animation-studio/public/aura-assets/rusty.catalog.glb +0 -0
  62. package/templates/animation-studio/public/cast-library/cast-library.json +71 -0
  63. package/templates/animation-studio/public/clip-library/gesture.json +173903 -0
  64. package/templates/animation-studio/public/clip-library/idle.json +23035 -0
  65. package/templates/animation-studio/public/clip-library/manifest.json +154 -0
  66. package/templates/animation-studio/public/clip-library/nod.json +173903 -0
  67. package/templates/animation-studio/public/clip-library/point.json +69879 -0
  68. package/templates/animation-studio/public/clip-library/react.json +66084 -0
  69. package/templates/animation-studio/public/clip-library/run.json +9648 -0
  70. package/templates/animation-studio/public/clip-library/talk.json +69879 -0
  71. package/templates/animation-studio/public/clip-library/walk.json +8283 -0
  72. package/templates/animation-studio/public/hifi-cast/BrainStem.glb +0 -0
  73. package/templates/animation-studio/public/hifi-cast/CesiumMan.glb +0 -0
  74. package/templates/animation-studio/public/hifi-cast/RiggedFigure.glb +0 -0
  75. package/templates/animation-studio/public/hifi-cast/kaykit/Barbarian.glb +0 -0
  76. package/templates/animation-studio/public/hifi-cast/kaykit/Knight.glb +0 -0
  77. package/templates/animation-studio/public/hifi-cast/kaykit/Mage.glb +0 -0
  78. package/templates/animation-studio/public/hifi-cast/kaykit/Rogue.glb +0 -0
  79. package/templates/animation-studio/public/hifi-cast/kaykit/Rogue_Hooded.glb +0 -0
  80. package/templates/animation-studio/public/hifi-cast/khronos-report.json +295 -0
  81. package/templates/animation-studio/public/hifi-cast/quaternius-report.json +449 -0
  82. package/templates/animation-studio/public/hifi-cast/rpm/cesiumman.glb +0 -0
  83. package/templates/animation-studio/public/hifi-cast/rpm/fox.glb +0 -0
  84. package/templates/animation-studio/public/hifi-cast/rpm/riggedfigure.glb +0 -0
  85. package/templates/animation-studio/public/hifi-cast/rpm-report.json +301 -0
  86. package/templates/animation-studio/scripts/anim.ts +103 -0
  87. package/templates/animation-studio/scripts/animation-scene.ts +457 -0
  88. package/templates/animation-studio/scripts/asset-motion-probe.ts +450 -0
  89. package/templates/animation-studio/scripts/asset-render-probe.ts +152 -0
  90. package/templates/animation-studio/scripts/author-office-scene.ts +65 -0
  91. package/templates/animation-studio/scripts/build-cast-library.ts +97 -0
  92. package/templates/animation-studio/scripts/build-characters.ts +1543 -0
  93. package/templates/animation-studio/scripts/build-clip-library.ts +972 -0
  94. package/templates/animation-studio/scripts/clip-decision-cli.ts +51 -0
  95. package/templates/animation-studio/scripts/determinism-check.ts +48 -0
  96. package/templates/animation-studio/scripts/generate-scene.ts +95 -0
  97. package/templates/animation-studio/scripts/hifi-cast-validate.ts +572 -0
  98. package/templates/animation-studio/scripts/motion-evidence-cli.ts +150 -0
  99. package/templates/animation-studio/scripts/motion-evidence.ts +362 -0
  100. package/templates/animation-studio/scripts/preview-server.ts +52 -0
  101. package/templates/animation-studio/scripts/regen-manifest.ts +156 -0
  102. package/templates/animation-studio/scripts/render-core.ts +360 -0
  103. package/templates/animation-studio/scripts/render-live.ts +813 -0
  104. package/templates/animation-studio/scripts/render-parallel.ts +70 -0
  105. package/templates/animation-studio/scripts/render-server.ts +119 -0
  106. package/templates/animation-studio/scripts/resolve-asset.ts +669 -0
  107. package/templates/animation-studio/scripts/skeleton-overlay-cli.ts +82 -0
  108. package/templates/animation-studio/scripts/skeleton-overlay.ts +293 -0
  109. package/templates/animation-studio/scripts/studio-tools-demo.ts +45 -0
  110. package/templates/animation-studio/scripts/texture-bake.ts +297 -0
  111. package/templates/{cartoon-studio → animation-studio}/scripts/validate-characters.ts +0 -0
  112. package/templates/animation-studio/scripts/verify-extracted-overlay-cli.ts +264 -0
  113. package/templates/animation-studio/src/animation-episode-validator.ts +97 -0
  114. package/templates/animation-studio/src/animation-performance.ts +556 -0
  115. package/templates/animation-studio/src/aura-assets.ts +71 -0
  116. package/templates/animation-studio/src/character.ts +40 -0
  117. package/templates/animation-studio/src/director/compile-episode-document.ts +45 -0
  118. package/templates/animation-studio/src/director/director-heuristics.ts +697 -0
  119. package/templates/animation-studio/src/director/prompt-to-scene.ts +369 -0
  120. package/templates/animation-studio/src/empty-document.ts +77 -0
  121. package/templates/animation-studio/src/episode-document.ts +442 -0
  122. package/templates/animation-studio/src/examples/moon-garden.example.ts +183 -0
  123. package/templates/animation-studio/src/fidelity.ts +129 -0
  124. package/templates/animation-studio/src/graph.ts +14 -0
  125. package/templates/animation-studio/src/procedural-hdri.ts +243 -0
  126. package/templates/animation-studio/src/profile.ts +97 -0
  127. package/templates/animation-studio/src/render-live-route.ts +42 -0
  128. package/templates/animation-studio/src/render-modes.ts +137 -0
  129. package/templates/animation-studio/src/scene-player.ts +867 -0
  130. package/templates/animation-studio/src/set-templates.ts +361 -0
  131. package/templates/animation-studio/src/studio/episode-document-store.ts +72 -0
  132. package/templates/animation-studio/src/studio/scene-tool-registry.ts +86 -0
  133. package/templates/animation-studio/src/studio/scene-tools.ts +169 -0
  134. package/templates/animation-studio/tests/reports/animation-studio/hifi-cast-validation.json +938 -0
  135. package/templates/animation-studio/tsconfig.json +26 -0
  136. package/templates/{cartoon-studio → animation-studio}/vite.config.ts +6 -1
  137. package/templates/character-controller/README.md +33 -0
  138. package/templates/character-controller/index.html +19 -0
  139. package/templates/character-controller/package.json +23 -0
  140. package/templates/character-controller/playwright.config.ts +14 -0
  141. package/templates/character-controller/src/controller.ts +44 -0
  142. package/templates/character-controller/src/main.ts +62 -0
  143. package/templates/character-controller/tests/route-health.spec.ts +26 -0
  144. package/templates/character-controller/tests/screenshot.spec.ts +9 -0
  145. package/templates/character-controller/tsconfig.json +15 -0
  146. package/templates/cinematic-scene/package.json +1 -1
  147. package/templates/episode-builder/README.md +4 -4
  148. package/templates/episode-builder/package.json +1 -1
  149. package/templates/episode-builder/src/aura-assets.ts +1 -1
  150. package/templates/episode-builder/src/builder.ts +4 -4
  151. package/templates/episode-builder/src/episode.ts +21 -21
  152. package/templates/episode-builder/src/main.ts +20 -20
  153. package/templates/episode-builder/src/render-plan.ts +13 -13
  154. package/templates/episode-builder/tests/storyboard-playback.spec.ts +7 -7
  155. package/templates/fighting-game/package.json +1 -1
  156. package/templates/mini-game/package.json +1 -1
  157. package/templates/product-viewer/package.json +1 -1
  158. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/README.md +5 -5
  159. package/templates/{cartoon-channel → prompt-animation-channel}/index.html +1 -1
  160. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/package.json +2 -2
  161. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/aura-assets.ts +1 -1
  162. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/episode.ts +22 -22
  163. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/main.ts +28 -28
  164. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/render-plan.ts +13 -13
  165. package/templates/{cartoon-channel → prompt-animation-channel}/tests/route-health.spec.ts +1 -1
  166. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tests/storyboard-playback.spec.ts +8 -8
  167. package/templates/cartoon-studio/aura.assets.json +0 -400
  168. package/templates/cartoon-studio/package.json +0 -33
  169. package/templates/cartoon-studio/public/aura-assets/luma.authored.glb +0 -0
  170. package/templates/cartoon-studio/public/aura-assets/miko.authored.glb +0 -0
  171. package/templates/cartoon-studio/scripts/build-characters.ts +0 -738
  172. package/templates/cartoon-studio/scripts/build-dialogue-audio.ts +0 -262
  173. package/templates/cartoon-studio/scripts/episode.ts +0 -323
  174. package/templates/cartoon-studio/scripts/render-live.ts +0 -873
  175. package/templates/cartoon-studio/src/aura-assets.ts +0 -64
  176. package/templates/cartoon-studio/src/characters.ts +0 -65
  177. package/templates/cartoon-studio/src/episode-renderer.ts +0 -605
  178. package/templates/cartoon-studio/src/episode.ts +0 -306
  179. package/templates/cartoon-studio/src/main.ts +0 -400
  180. package/templates/cartoon-studio/src/render-live-route.ts +0 -913
  181. package/templates/cartoon-studio/src/render-plan.ts +0 -567
  182. package/templates/cartoon-studio/src/review.ts +0 -99
  183. package/templates/cartoon-studio/src/studio.ts +0 -101
  184. package/templates/cartoon-studio/test-results/.last-run.json +0 -4
  185. package/templates/cartoon-studio/tests/episode-render.spec.ts +0 -44
  186. package/templates/cartoon-studio/tests/motion-quality.spec.ts +0 -45
  187. package/templates/cartoon-studio/tests/route-health.spec.ts +0 -7
  188. package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +0 -99
  189. package/templates/cartoon-studio/tests/visual-review.spec.ts +0 -21
  190. package/templates/cartoon-studio/tsconfig.json +0 -14
  191. package/templates/prompt-cartoon-channel/index.html +0 -13
  192. package/templates/prompt-cartoon-channel/src/sets.ts +0 -14
  193. /package/templates/{cartoon-channel → animation-channel}/aura.assets.json +0 -0
  194. /package/templates/{cartoon-channel → animation-channel}/playwright.config.ts +0 -0
  195. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.047f5e5f.glb +0 -0
  196. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.thumb.svg +0 -0
  197. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.047f5e5f.glb +0 -0
  198. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.thumb.svg +0 -0
  199. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/moon-garden-feature-frame.png +0 -0
  200. /package/templates/{cartoon-channel → animation-channel}/src/aura-assets.ts +0 -0
  201. /package/templates/{cartoon-channel → animation-channel}/src/characters.ts +0 -0
  202. /package/templates/{cartoon-channel → animation-channel}/src/contract.ts +0 -0
  203. /package/templates/{cartoon-channel → animation-channel}/src/sets.ts +0 -0
  204. /package/templates/{cartoon-channel → animation-channel}/test-results/.last-run.json +0 -0
  205. /package/templates/{cartoon-channel → animation-channel}/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
  206. /package/templates/{cartoon-channel → animation-channel}/tsconfig.json +0 -0
  207. /package/templates/{cartoon-studio → animation-studio}/playwright.config.ts +0 -0
  208. /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/luma.thumb.svg +0 -0
  209. /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/miko.thumb.svg +0 -0
  210. /package/templates/{cartoon-studio → animation-studio}/src/contract.ts +0 -0
  211. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/playwright.config.ts +0 -0
  212. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/characters.ts +0 -0
  213. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/contract.ts +0 -0
  214. /package/templates/{cartoon-studio → prompt-animation-channel}/src/sets.ts +0 -0
  215. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tsconfig.json +0 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * skeleton-overlay-cli.ts — standalone runner for the B2 skeleton-overlay strip.
3
+ *
4
+ * Produces a 3-frame (first/mid/final) bone-projection PNG strip for a given intent and writes it to
5
+ * `--out`. This is the same `buildSkeletonStrip` render-live.ts calls; isolating it in a tiny CLI
6
+ * lets a unit test produce + validate the REAL artifact without spinning up Playwright/the GPU.
7
+ *
8
+ * Usage: tsx scripts/skeleton-overlay-cli.ts --intent walk --out <path.png>
9
+ */
10
+ import { writeFileSync, mkdirSync, existsSync } from "node:fs";
11
+ import { dirname, resolve } from "node:path";
12
+ import { createRequire } from "node:module";
13
+ import { fileURLToPath } from "node:url";
14
+ import { buildSkeletonStrip } from "./skeleton-overlay.js";
15
+
16
+ // sharp directly (the render-core helper pulls in @aura3d/engine, which only resolves under the vite
17
+ // alias used by the full render — this CLI stays standalone/Playwright-free).
18
+ interface SharpModule {
19
+ (input: Buffer | Uint8Array, options?: { raw: { width: number; height: number; channels: number } }): {
20
+ png(): { toBuffer(): Promise<Buffer> };
21
+ };
22
+ }
23
+ function loadSharp(): SharpModule {
24
+ try {
25
+ return createRequire(import.meta.url)("sharp") as SharpModule;
26
+ } catch {
27
+ // pnpm hoists sharp to the workspace root .pnpm store; search up from both cwd and this file.
28
+ const roots = [process.cwd(), dirname(fileURLToPath(import.meta.url))];
29
+ for (const start of roots) {
30
+ let dir = start;
31
+ for (let up = 0; up < 8; up += 1) {
32
+ const storePkg = resolve(dir, "node_modules/.pnpm/sharp@0.33.5/node_modules/sharp/package.json");
33
+ if (existsSync(storePkg)) return createRequire(storePkg)("sharp") as SharpModule;
34
+ const parent = dirname(dir);
35
+ if (parent === dir) break;
36
+ dir = parent;
37
+ }
38
+ }
39
+ throw new Error("Could not load `sharp`. Install it: pnpm add -D sharp");
40
+ }
41
+ }
42
+ async function rawRgbaToPng(pixels: Uint8Array, width: number, height: number): Promise<Uint8Array> {
43
+ const sharp = loadSharp();
44
+ const buffer = await sharp(Buffer.from(pixels.buffer, pixels.byteOffset, pixels.byteLength), {
45
+ raw: { width, height, channels: 4 }
46
+ })
47
+ .png()
48
+ .toBuffer();
49
+ return new Uint8Array(buffer);
50
+ }
51
+
52
+ function arg(name: string, fallback: string): string {
53
+ const i = process.argv.indexOf(`--${name}`);
54
+ return i >= 0 && process.argv[i + 1] ? process.argv[i + 1]! : fallback;
55
+ }
56
+
57
+ async function main(): Promise<void> {
58
+ const intent = arg("intent", "walk");
59
+ const out = arg("out", `dist/skeleton-overlays/${intent}.png`);
60
+ const strip = buildSkeletonStrip({ intent });
61
+ const png = await rawRgbaToPng(strip.rgba, strip.width, strip.height);
62
+ mkdirSync(dirname(out), { recursive: true });
63
+ writeFileSync(out, png);
64
+ // Emit a machine-readable line the test can parse.
65
+ console.log(
66
+ JSON.stringify({
67
+ ok: true,
68
+ out,
69
+ intent: strip.intent,
70
+ width: strip.width,
71
+ height: strip.height,
72
+ panelJointCounts: strip.panelJointCounts,
73
+ firstFinalMaxDiff: strip.firstFinalMaxDiff,
74
+ pngBytes: png.byteLength
75
+ })
76
+ );
77
+ }
78
+
79
+ main().catch((err) => {
80
+ console.error(JSON.stringify({ ok: false, error: (err as Error).message }));
81
+ process.exitCode = 1;
82
+ });
@@ -0,0 +1,293 @@
1
+ /**
2
+ * skeleton-overlay.ts — B2 skeleton-overlay strip.
3
+ *
4
+ * The render proof must show MOTION as a SKELETON (per-bone transforms over time), not as a raw
5
+ * pixel-diff. `render-live.ts` already records per-bone local rotation ranges + the per-frame
6
+ * clip-decision log in `render-live-summary.json` (semantic, mouth/caption/camera-free). This module
7
+ * adds the VISUAL counterpart the PRD asks for: a 3-frame (first / mid / final) bone-projection PNG
8
+ * strip per character, saved to the episode dir.
9
+ *
10
+ * It is intentionally browser-FREE and GPU-FREE: it runs forward kinematics on a canonical humanoid
11
+ * rest skeleton, drives it with the SAME shared standard-library clip the player retargets (so the
12
+ * strip reflects the real library motion), projects the world bone positions orthographically, and
13
+ * rasterizes the bone segments. Deterministic — identical bytes every run for the same inputs — so a
14
+ * unit test can assert the strip is produced and that the three frames differ (the body moved).
15
+ */
16
+
17
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
18
+ // @ts-ignore — resolved at runtime to the freshly-built monorepo dist (has co-located .d.ts).
19
+ import {
20
+ createStandardHumanoidClipRegistry,
21
+ type AnimationClipRegistry
22
+ } from "@aura3d/animation";
23
+
24
+ type Vec3 = readonly [number, number, number];
25
+ type Quat = readonly [number, number, number, number];
26
+
27
+ /** Canonical rest skeleton: each bone's PARENT and its parent-relative rest offset (metres). */
28
+ interface RestBone {
29
+ readonly parent: string | null;
30
+ readonly offset: Vec3;
31
+ }
32
+
33
+ /**
34
+ * A neutral, roughly-proportioned humanoid in metres (hips at ~0.95m). Mirrors the canonical
35
+ * HUMANOID_BONES the shared library animates; only the bones the standard clips touch need offsets.
36
+ */
37
+ const REST_SKELETON: Readonly<Record<string, RestBone>> = {
38
+ hips: { parent: null, offset: [0, 0.95, 0] },
39
+ spine: { parent: "hips", offset: [0, 0.12, 0] },
40
+ chest: { parent: "spine", offset: [0, 0.14, 0] },
41
+ upperChest: { parent: "chest", offset: [0, 0.1, 0] },
42
+ neck: { parent: "upperChest", offset: [0, 0.08, 0] },
43
+ head: { parent: "neck", offset: [0, 0.12, 0] },
44
+ leftShoulder: { parent: "upperChest", offset: [0.06, 0.05, 0] },
45
+ leftUpperArm: { parent: "leftShoulder", offset: [0.14, 0, 0] },
46
+ leftLowerArm: { parent: "leftUpperArm", offset: [0.26, 0, 0] },
47
+ leftHand: { parent: "leftLowerArm", offset: [0.24, 0, 0] },
48
+ rightShoulder: { parent: "upperChest", offset: [-0.06, 0.05, 0] },
49
+ rightUpperArm: { parent: "rightShoulder", offset: [-0.14, 0, 0] },
50
+ rightLowerArm: { parent: "rightUpperArm", offset: [-0.26, 0, 0] },
51
+ rightHand: { parent: "rightLowerArm", offset: [-0.24, 0, 0] },
52
+ leftUpperLeg: { parent: "hips", offset: [0.09, -0.04, 0] },
53
+ leftLowerLeg: { parent: "leftUpperLeg", offset: [0, -0.42, 0] },
54
+ leftFoot: { parent: "leftLowerLeg", offset: [0, -0.42, 0.04] },
55
+ rightUpperLeg: { parent: "hips", offset: [-0.09, -0.04, 0] },
56
+ rightLowerLeg: { parent: "rightUpperLeg", offset: [0, -0.42, 0] },
57
+ rightFoot: { parent: "rightLowerLeg", offset: [0, -0.42, 0.04] }
58
+ };
59
+
60
+ /** Draw order: bone segments (parent → child) that form the visible stick figure. */
61
+ const BONE_SEGMENTS: readonly (readonly [string, string])[] = Object.entries(REST_SKELETON)
62
+ .filter(([, b]) => b.parent !== null)
63
+ .map(([child, b]) => [b.parent as string, child] as const);
64
+
65
+ function qMulV(q: Quat, v: Vec3): Vec3 {
66
+ const [x, y, z, w] = q;
67
+ // t = 2 * cross(q.xyz, v)
68
+ const tx = 2 * (y * v[2] - z * v[1]);
69
+ const ty = 2 * (z * v[0] - x * v[2]);
70
+ const tz = 2 * (x * v[1] - y * v[0]);
71
+ // v + w*t + cross(q.xyz, t)
72
+ return [
73
+ v[0] + w * tx + (y * tz - z * ty),
74
+ v[1] + w * ty + (z * tx - x * tz),
75
+ v[2] + w * tz + (x * ty - y * tx)
76
+ ];
77
+ }
78
+
79
+ function qMul(a: Quat, b: Quat): Quat {
80
+ return [
81
+ a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
82
+ a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
83
+ a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
84
+ a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
85
+ ];
86
+ }
87
+
88
+ const IDENTITY: Quat = [0, 0, 0, 1];
89
+
90
+ /** Sample a standard clip at `t` into per-bone local rotations (+ hips translation) (canonical names). */
91
+ function samplePose(
92
+ clip: { duration: number; loop: boolean; tracks: readonly { target: string; valueType: string; sample(t: number): readonly number[] }[] },
93
+ t: number
94
+ ): { rotations: Record<string, Quat>; hipsOffset: Vec3 } {
95
+ const duration = clip.duration > 0 ? clip.duration : 1;
96
+ const local = clip.loop ? t % duration : Math.min(t, duration);
97
+ const rotations: Record<string, Quat> = {};
98
+ let hipsOffset: Vec3 = [0, 0, 0];
99
+ for (const track of clip.tracks) {
100
+ const dot = track.target.lastIndexOf(".");
101
+ if (dot < 0) continue;
102
+ const bone = track.target.slice(0, dot);
103
+ const path = track.target.slice(dot + 1);
104
+ const v = track.sample(local);
105
+ if (path === "rotation" && v.length >= 4) rotations[bone] = [v[0]!, v[1]!, v[2]!, v[3]!];
106
+ else if (path === "translation" && bone === "hips" && v.length >= 3) hipsOffset = [v[0]!, v[1]!, v[2]!];
107
+ }
108
+ return { rotations, hipsOffset };
109
+ }
110
+
111
+ /** Forward kinematics: world positions for every bone given local rotations + a hips offset. */
112
+ function solveWorld(rotations: Record<string, Quat>, hipsOffset: Vec3): Record<string, Vec3> {
113
+ const worldPos: Record<string, Vec3> = {};
114
+ const worldRot: Record<string, Quat> = {};
115
+ // REST_SKELETON is declared parent-before-child, so a single pass resolves the chain.
116
+ for (const [bone, rest] of Object.entries(REST_SKELETON)) {
117
+ const localRot = rotations[bone] ?? IDENTITY;
118
+ if (rest.parent === null) {
119
+ worldRot[bone] = localRot;
120
+ worldPos[bone] = [rest.offset[0] + hipsOffset[0], rest.offset[1] + hipsOffset[1], rest.offset[2] + hipsOffset[2]];
121
+ } else {
122
+ const pRot = worldRot[rest.parent] ?? IDENTITY;
123
+ const pPos = worldPos[rest.parent] ?? [0, 0, 0];
124
+ const rotated = qMulV(pRot, rest.offset);
125
+ worldPos[bone] = [pPos[0] + rotated[0], pPos[1] + rotated[1], pPos[2] + rotated[2]];
126
+ worldRot[bone] = qMul(pRot, localRot);
127
+ }
128
+ }
129
+ return worldPos;
130
+ }
131
+
132
+ export interface SkeletonStripOptions {
133
+ /** Intent (standard clip id) the strip should visualize (e.g. the character's dominant beat). */
134
+ readonly intent?: string;
135
+ /** Times (seconds) for the three panels. Defaults to first / mid / final of the clip. */
136
+ readonly times?: readonly [number, number, number];
137
+ /** Per-panel pixel size. */
138
+ readonly panelWidth?: number;
139
+ readonly panelHeight?: number;
140
+ /** Optional shared registry (reuse across characters); built if omitted. */
141
+ readonly registry?: AnimationClipRegistry;
142
+ /**
143
+ * Optional allow-list of canonical bone names to draw. When provided, only joints whose bone is in
144
+ * the list are projected and only bone segments whose BOTH endpoints are in the list are drawn.
145
+ * Used by the B5 retargeting overlay to mask the figure down to the bones a target rig actually
146
+ * maps (a sparse mascot draws a sparser stick figure than a full humanoid). Omit to draw the full
147
+ * canonical skeleton.
148
+ */
149
+ readonly bonesAllow?: readonly string[];
150
+ }
151
+
152
+ export interface SkeletonStripResult {
153
+ /** RGBA bytes for the full 3-panel strip (panelWidth*3 × panelHeight). */
154
+ readonly rgba: Uint8Array;
155
+ readonly width: number;
156
+ readonly height: number;
157
+ /** Per-panel projected joint count (a panel with 0 joints means the FK produced nothing — a bug). */
158
+ readonly panelJointCounts: readonly [number, number, number];
159
+ /** Max pixel-difference between the first and final panels (>0 ⇒ the skeleton visibly moved). */
160
+ readonly firstFinalMaxDiff: number;
161
+ readonly intent: string;
162
+ }
163
+
164
+ /**
165
+ * Build a 3-panel (first / mid / final) skeleton-overlay strip for one character's dominant intent.
166
+ * Returns raw RGBA so the caller can encode a PNG with whatever encoder it has (sharp / pngjs / a
167
+ * browser canvas). Pure + deterministic.
168
+ */
169
+ export function buildSkeletonStrip(options: SkeletonStripOptions = {}): SkeletonStripResult {
170
+ const intent = options.intent ?? "walk";
171
+ const panelW = options.panelWidth ?? 160;
172
+ const panelH = options.panelHeight ?? 240;
173
+ const registry = options.registry ?? createStandardHumanoidClipRegistry();
174
+ const clip = (registry.get?.(intent) ?? registry.require?.(intent)) as
175
+ | { duration: number; loop: boolean; tracks: readonly { target: string; valueType: string; sample(t: number): readonly number[] }[] }
176
+ | undefined;
177
+ if (!clip) throw new Error(`skeleton-overlay: no standard clip for intent "${intent}".`);
178
+
179
+ const duration = clip.duration > 0 ? clip.duration : 1;
180
+ const times = options.times ?? [0, duration / 2, duration * 0.999];
181
+ const allow = options.bonesAllow ? new Set(options.bonesAllow) : null;
182
+ const boneVisible = (bone: string): boolean => (allow ? allow.has(bone) : true);
183
+
184
+ const width = panelW * 3;
185
+ const height = panelH;
186
+ const rgba = new Uint8Array(width * height * 4);
187
+ // Background: dark slate (matches the live route).
188
+ for (let i = 0; i < width * height; i += 1) {
189
+ rgba[i * 4] = 11;
190
+ rgba[i * 4 + 1] = 15;
191
+ rgba[i * 4 + 2] = 26;
192
+ rgba[i * 4 + 3] = 255;
193
+ }
194
+
195
+ // Orthographic projection: fit the standing figure (~y 0..1.8m, x ±0.7m) into the panel.
196
+ const project = (p: Vec3, panelIndex: number): [number, number] => {
197
+ const sx = panelW * 0.42; // metres → px (x)
198
+ const sy = panelH * 0.46; // metres → px (y)
199
+ const cx = panelIndex * panelW + panelW / 2;
200
+ const cy = panelH * 0.92; // ground line near the bottom
201
+ return [cx + p[0] * sx, cy - (p[1] - 0.0) * sy];
202
+ };
203
+
204
+ const panelJointCounts: [number, number, number] = [0, 0, 0];
205
+ for (let panel = 0; panel < 3; panel += 1) {
206
+ const { rotations, hipsOffset } = samplePose(clip, times[panel]!);
207
+ const world = solveWorld(rotations, hipsOffset);
208
+ panelJointCounts[panel] = Object.keys(world).filter((b) => boneVisible(b)).length;
209
+ // Bone segments (only when both endpoints are visible under the allow-list).
210
+ for (const [a, b] of BONE_SEGMENTS) {
211
+ if (!boneVisible(a) || !boneVisible(b)) continue;
212
+ const wa = world[a];
213
+ const wb = world[b];
214
+ if (!wa || !wb) continue;
215
+ drawLine(rgba, width, height, project(wa, panel), project(wb, panel), [158, 255, 162]);
216
+ }
217
+ // Joint dots (visible bones only).
218
+ for (const [bone, pos] of Object.entries(world)) {
219
+ if (!boneVisible(bone)) continue;
220
+ const [px, py] = project(pos, panel);
221
+ drawDot(rgba, width, height, px, py, [255, 224, 130]);
222
+ }
223
+ // Panel divider.
224
+ if (panel > 0) {
225
+ for (let y = 0; y < height; y += 1) setPixel(rgba, width, height, panel * panelW, y, [40, 56, 80]);
226
+ }
227
+ }
228
+
229
+ // First-vs-final visible difference (proves the body moved across the strip).
230
+ let firstFinalMaxDiff = 0;
231
+ for (let y = 0; y < height; y += 1) {
232
+ for (let x = 0; x < panelW; x += 1) {
233
+ const iFirst = (y * width + x) * 4;
234
+ const iFinal = (y * width + (x + panelW * 2)) * 4;
235
+ const d =
236
+ Math.abs(rgba[iFirst]! - rgba[iFinal]!) +
237
+ Math.abs(rgba[iFirst + 1]! - rgba[iFinal + 1]!) +
238
+ Math.abs(rgba[iFirst + 2]! - rgba[iFinal + 2]!);
239
+ firstFinalMaxDiff = Math.max(firstFinalMaxDiff, d);
240
+ }
241
+ }
242
+
243
+ return { rgba, width, height, panelJointCounts, firstFinalMaxDiff, intent };
244
+ }
245
+
246
+ function setPixel(buf: Uint8Array, w: number, h: number, x: number, y: number, rgb: readonly [number, number, number]): void {
247
+ const xi = Math.round(x);
248
+ const yi = Math.round(y);
249
+ if (xi < 0 || yi < 0 || xi >= w || yi >= h) return;
250
+ const i = (yi * w + xi) * 4;
251
+ buf[i] = rgb[0];
252
+ buf[i + 1] = rgb[1];
253
+ buf[i + 2] = rgb[2];
254
+ buf[i + 3] = 255;
255
+ }
256
+
257
+ function drawDot(buf: Uint8Array, w: number, h: number, x: number, y: number, rgb: readonly [number, number, number]): void {
258
+ for (let dy = -1; dy <= 1; dy += 1) for (let dx = -1; dx <= 1; dx += 1) setPixel(buf, w, h, x + dx, y + dy, rgb);
259
+ }
260
+
261
+ function drawLine(
262
+ buf: Uint8Array,
263
+ w: number,
264
+ h: number,
265
+ a: readonly [number, number],
266
+ b: readonly [number, number],
267
+ rgb: readonly [number, number, number]
268
+ ): void {
269
+ let x0 = Math.round(a[0]);
270
+ let y0 = Math.round(a[1]);
271
+ const x1 = Math.round(b[0]);
272
+ const y1 = Math.round(b[1]);
273
+ const dx = Math.abs(x1 - x0);
274
+ const dy = Math.abs(y1 - y0);
275
+ const sx = x0 < x1 ? 1 : -1;
276
+ const sy = y0 < y1 ? 1 : -1;
277
+ let err = dx - dy;
278
+ // Cap iterations defensively.
279
+ for (let guard = 0; guard < 4096; guard += 1) {
280
+ setPixel(buf, w, h, x0, y0, rgb);
281
+ setPixel(buf, w, h, x0 + 1, y0, rgb); // 2px stroke for visibility
282
+ if (x0 === x1 && y0 === y1) break;
283
+ const e2 = 2 * err;
284
+ if (e2 > -dy) {
285
+ err -= dy;
286
+ x0 += sx;
287
+ }
288
+ if (e2 < dx) {
289
+ err += dx;
290
+ y0 += sy;
291
+ }
292
+ }
293
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * studio-tools-demo.ts — proves the Scene-Tool API guardrail.
3
+ *
4
+ * The store/registry must REJECT an edit that leaves the walkable set (a broken scene)
5
+ * and ACCEPT a valid one, with working undo — i.e. there is no path to a broken document.
6
+ *
7
+ * Run: pnpm exec tsx --tsconfig tsconfig.base.json packages/create-aura3d/templates/animation-studio/scripts/studio-tools-demo.ts
8
+ */
9
+
10
+ import { moonGardenDocument } from "../src/examples/moon-garden.example.js";
11
+ import { EpisodeDocumentStore } from "../src/studio/episode-document-store.js";
12
+ import { SceneToolRegistry } from "../src/studio/scene-tool-registry.js";
13
+
14
+ function main(): void {
15
+ const store = new EpisodeDocumentStore(moonGardenDocument);
16
+ const reg = new SceneToolRegistry(store);
17
+ console.log(`initial revision=${store.currentRevision()} revisions=${store.revisionCount()}`);
18
+
19
+ // 1. OFF-SET edit must be REJECTED.
20
+ const offSet = reg.block("miko", "shot-moon-garden-open", [{ time: 0, position: [50, 0, 50], yaw: 0 }], "Loops");
21
+ console.log(`off-set block committed=${offSet.committed} (expect false) :: ${offSet.validation.errors[0] ?? "—"}`);
22
+
23
+ // 2. VALID edit must be ACCEPTED.
24
+ const valid = reg.block("miko", "shot-moon-garden-open", [{ time: 0, position: [1.5, 0, 0.5], yaw: 0.4 }], "Loops");
25
+ console.log(`valid block committed=${valid.committed} (expect true) revision=${store.currentRevision()}`);
26
+
27
+ // 3. Undo returns to the previous revision.
28
+ const undone = reg.undo();
29
+ const mikoOpen = store
30
+ .current()
31
+ .blocking.find((b) => b.characterId === "miko")
32
+ ?.shots.find((s) => s.shotId === "shot-moon-garden-open")?.waypoints[0]?.position;
33
+ console.log(`undo=${undone} revision=${store.currentRevision()} miko@open=[${mikoOpen?.join(",")}] (expect original [-0.95,0,0])`);
34
+
35
+ // 4. A camera edit + a prop placement (valid).
36
+ const cam = reg.camera("shot-moon-garden-open", { preset: "two-shot" });
37
+ const prop = reg.dress({ propId: "mushroom", position: [2.0, 0, 1.0], scale: 0.1, feetOffset: 2.8 });
38
+ console.log(`camera committed=${cam.committed} prop committed=${prop.committed} props=${store.current().setDressing.length}`);
39
+
40
+ const ok = offSet.committed === false && valid.committed === true && undone === true && cam.committed && prop.committed;
41
+ console.log(`\nGUARDRAIL ${ok ? "PASS" : "FAIL"} — off-set rejected, valid accepted, undo works, no path to a broken document.`);
42
+ if (!ok) process.exitCode = 1;
43
+ }
44
+
45
+ main();