cozyclay 1.10.0 → 2.0.0

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 (272) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/bin/agent/agent-routes.mjs +74 -70
  3. package/bin/agent/agent-runner.mjs +10 -3
  4. package/bin/agent/motion-runtime.mjs +32 -260
  5. package/bin/agent/providers.mjs +46 -3
  6. package/bin/agent/session-store.mjs +41 -5
  7. package/bin/agent/studio-prompt.mjs +1 -1
  8. package/bin/agent/studio-tools.mjs +48 -11
  9. package/bin/cozyclay.mjs +26 -8
  10. package/bin/live/cli.mjs +22 -3
  11. package/bin/mcp-runtime.mjs +35 -20
  12. package/bin/telemetry-state.mjs +5 -2
  13. package/bin/update-check.mjs +15 -0
  14. package/dist/app/index.html +6 -6
  15. package/dist/assets/analytics-MVZOEW8O.js +1 -0
  16. package/dist/assets/app-C1sdVbg4.css +1 -0
  17. package/dist/assets/app-CA55tcI3.js +4876 -0
  18. package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
  19. package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
  20. package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
  21. package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
  22. package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
  23. package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
  24. package/dist/assets/workflow-DRdwoDBT.css +1 -0
  25. package/dist/assets/workflow-dNPGY9xz.js +179 -0
  26. package/dist/cozyclay-package.json +1 -1
  27. package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
  28. package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
  29. package/dist/fonts/README.md +13 -9
  30. package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
  31. package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
  32. package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
  33. package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
  34. package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
  35. package/dist/index.html +7 -7
  36. package/dist/privacy/index.html +13 -8
  37. package/dist/sitemap.xml +6 -6
  38. package/dist/workflow/index.html +5 -5
  39. package/mcp/live-hub.mjs +35 -93
  40. package/mcp/mesh-file.mjs +48 -0
  41. package/mcp/server.mjs +6 -77
  42. package/mcp/tool-handlers.mjs +219 -376
  43. package/package.json +2 -1
  44. package/src/App.jsx +1249 -9076
  45. package/src/analytics.js +393 -9
  46. package/src/app-context.js +211 -0
  47. package/src/app-stage.jsx +25 -24
  48. package/src/ardy/auto-fix-panel.css +108 -0
  49. package/src/ardy/fix-collisions.js +10 -66
  50. package/src/ardy/ik-drag.js +101 -0
  51. package/src/ardy/ik-key-json.js +43 -0
  52. package/src/ardy/ik.js +97 -67
  53. package/src/ardy/physics-panel.css +2 -0
  54. package/src/ardy/physics-panel.jsx +17 -5
  55. package/src/ardy/platform-fit-panel.css +64 -0
  56. package/src/ardy/platform-fit-panel.jsx +40 -0
  57. package/src/ardy/platform-fit.js +326 -0
  58. package/src/ardy/playback.js +166 -20
  59. package/src/ardy/range-pin.js +299 -0
  60. package/src/ardy/timeline-coordinates.js +26 -0
  61. package/src/ardy/timeline.css +1199 -0
  62. package/src/ardy/timeline.jsx +149 -41
  63. package/src/ardy/waypoints.js +2 -2
  64. package/src/asset-pane.css +462 -0
  65. package/src/asset-pane.jsx +461 -232
  66. package/src/command-bus.js +349 -0
  67. package/src/commands/ai.js +31 -0
  68. package/src/commands/cast.js +140 -0
  69. package/src/commands/elements/character.js +13 -0
  70. package/src/commands/elements/motion.js +14 -0
  71. package/src/commands/elements/object.js +22 -0
  72. package/src/commands/elements/scene.js +16 -0
  73. package/src/commands/elements/shot.js +9 -0
  74. package/src/commands/elements/stage.js +11 -0
  75. package/src/commands/elements.js +155 -0
  76. package/src/commands/export.js +20 -0
  77. package/src/commands/index.js +40 -0
  78. package/src/commands/motion.js +231 -0
  79. package/src/commands/objects.js +174 -0
  80. package/src/commands/project.js +53 -0
  81. package/src/commands/scene.js +81 -0
  82. package/src/commands/shared.js +19 -0
  83. package/src/commands/shot.js +103 -0
  84. package/src/commands/stage.js +17 -0
  85. package/src/commands/view.js +42 -0
  86. package/src/document-store.js +144 -0
  87. package/src/domains/cast.js +1007 -0
  88. package/src/domains/motion.js +3219 -0
  89. package/src/domains/objects.js +932 -0
  90. package/src/domains/scenes.js +817 -0
  91. package/src/domains/shots.js +404 -0
  92. package/src/domains/stage.js +93 -0
  93. package/src/dualview.jsx +113 -57
  94. package/src/facing-marks.js +3 -0
  95. package/src/first-success-guide.jsx +14 -14
  96. package/src/grid-view.js +18 -5
  97. package/src/hierarchy-panel.css +386 -0
  98. package/src/hierarchy-panel.jsx +88 -19
  99. package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
  100. package/src/i2v-motion-studio.jsx +180 -0
  101. package/src/ik-camera.js +3 -0
  102. package/src/main.jsx +6 -0
  103. package/src/motion/generation.js +105 -0
  104. package/src/motion-readiness-ui.jsx +4 -4
  105. package/src/motion-readiness.js +11 -0
  106. package/src/motion-trail.js +366 -9
  107. package/src/object-gizmo.jsx +7 -1
  108. package/src/panels/CameraPanel.jsx +60 -0
  109. package/src/panels/CharacterTransformPanel.jsx +48 -0
  110. package/src/panels/EnvironmentPanel.jsx +40 -0
  111. package/src/panels/Foldout.jsx +32 -0
  112. package/src/panels/LightPanel.jsx +24 -0
  113. package/src/panels/ObjectTransformPanel.jsx +554 -0
  114. package/src/panels/PosePanel.jsx +97 -0
  115. package/src/panels/ProjectPanel.jsx +33 -0
  116. package/src/panels/PromptBlocksPanel.jsx +442 -0
  117. package/src/panels/PropsPanel.jsx +70 -0
  118. package/src/panels/ReferenceImageField.jsx +91 -0
  119. package/src/panels/RigControlPanel.jsx +78 -0
  120. package/src/panels/RigPanel.jsx +37 -0
  121. package/src/panels/SubjectBox.jsx +47 -0
  122. package/src/panels/SubjectsPanel.jsx +39 -0
  123. package/src/panels/VideoCapturePanel.jsx +180 -0
  124. package/src/panels/details.css +1171 -0
  125. package/src/panels/motion.css +197 -0
  126. package/src/panels/pose.css +39 -0
  127. package/src/planview.jsx +60 -31
  128. package/src/posestudio.jsx +59 -4
  129. package/src/project-browser.css +1073 -0
  130. package/src/project-browser.jsx +284 -123
  131. package/src/range-pin-object-transform.js +19 -0
  132. package/src/range-pin-panel.css +518 -0
  133. package/src/range-pin-panel.jsx +344 -0
  134. package/src/result-modal.jsx +5 -5
  135. package/src/room.jsx +79 -51
  136. package/src/scenes.js +5 -0
  137. package/src/semantic-edit.js +2 -0
  138. package/src/settings-menu.jsx +39 -105
  139. package/src/shell/BottomDock.jsx +450 -0
  140. package/src/shell/DetailsSlot.jsx +513 -0
  141. package/src/shell/LibrarySlot.jsx +86 -0
  142. package/src/shell/MenuBar.jsx +601 -0
  143. package/src/shell/OutlinerSlot.jsx +50 -0
  144. package/src/shell/PreferencesDialog.jsx +538 -0
  145. package/src/shell/PreferencesSlot.jsx +40 -0
  146. package/src/shell/StatusBar.jsx +125 -0
  147. package/src/shell/StudioShell.jsx +92 -0
  148. package/src/shell/TopBar.jsx +129 -0
  149. package/src/shell/ViewportToolbar.jsx +656 -0
  150. package/src/shell/agent-glass.css +296 -0
  151. package/src/shell/dock.css +93 -0
  152. package/src/shell/glass-regions.css +985 -0
  153. package/src/shell/glass.css +687 -0
  154. package/src/shell/log-store.js +52 -0
  155. package/src/shell/mode.css +9 -0
  156. package/src/shell/preferences.css +651 -0
  157. package/src/shell/shell.css +282 -0
  158. package/src/shell/studio-shell-context.js +19 -0
  159. package/src/shell/topbar.css +435 -0
  160. package/src/shell/viewport.css +451 -0
  161. package/src/store/authored-intent.js +22 -0
  162. package/src/store/runtime-adapters.js +92 -0
  163. package/src/store/scene-stage.js +21 -0
  164. package/src/store/use-document-store.js +14 -0
  165. package/src/studio-actions.js +210 -0
  166. package/src/studio-agent-commands.js +38 -268
  167. package/src/studio-agent-context.js +38 -9
  168. package/src/studio-agent-motion.js +122 -378
  169. package/src/studio-agent-protocol.js +66 -30
  170. package/src/studio-app-binding.js +379 -0
  171. package/src/studio-contact-sheet.js +73 -0
  172. package/src/studio-elements.js +58 -51
  173. package/src/styles/themes.css +200 -0
  174. package/src/styles/tokens.css +69 -0
  175. package/src/styles.css +443 -1753
  176. package/src/theme.js +44 -0
  177. package/src/timeline-extent.js +16 -0
  178. package/src/trail-key-conflicts.js +39 -0
  179. package/src/trail-pick.js +59 -0
  180. package/src/ui.jsx +8 -8
  181. package/src/use-case-question.jsx +66 -0
  182. package/src/workflow/AgentPanel.jsx +82 -69
  183. package/src/workflow/agent-client.js +12 -2
  184. package/src/workflow/agent-panel.css +34 -32
  185. package/src/workflow/cozy-scene-node.css +2 -0
  186. package/src/workflow/workflow.css +2 -0
  187. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  188. package/tools/ardy/bridge.mjs +164 -146
  189. package/tools/ardy/visual-qa.mjs +5 -5
  190. package/tools/bench/EXP3.md +61 -0
  191. package/tools/bench/cclay_bench_extract_incam.py +125 -0
  192. package/tools/bench/cclay_bench_extract_obs.py +204 -0
  193. package/tools/bench/cclay_bench_runner.py +42 -0
  194. package/tools/bench/cube-contact.mjs +162 -0
  195. package/tools/bench/exp3.mjs +152 -0
  196. package/tools/bench/extract-bench-lib.mjs +216 -0
  197. package/tools/bench/extract-bench.mjs +302 -0
  198. package/tools/bench/fal-generate.mjs +88 -0
  199. package/tools/bench/fit/README.md +189 -0
  200. package/tools/bench/fit/camera.mjs +59 -0
  201. package/tools/bench/fit/contact.mjs +419 -0
  202. package/tools/bench/fit/footlock.mjs +234 -0
  203. package/tools/bench/fit/motion.mjs +85 -0
  204. package/tools/bench/fit/pin.mjs +27 -0
  205. package/tools/bench/fit/remote.mjs +105 -0
  206. package/tools/bench/fit-bench.mjs +126 -0
  207. package/tools/bench/fit-sanity.mjs +45 -0
  208. package/tools/bench/metrics.mjs +313 -0
  209. package/tools/bench/obs/depth.mjs +258 -0
  210. package/tools/bench/obs/extrinsics.mjs +175 -0
  211. package/tools/bench/obs/fit_mannequin_betas.py +340 -0
  212. package/tools/bench/obs/ground.mjs +294 -0
  213. package/tools/bench/obs/heading.mjs +151 -0
  214. package/tools/bench/obs/ladder.mjs +555 -0
  215. package/tools/bench/obs/mannequin-betas.json +124 -0
  216. package/tools/bench/obs/remote.mjs +83 -0
  217. package/tools/bench/obs/rest_joints.py +52 -0
  218. package/tools/bench/obs/ybot-targets.mjs +49 -0
  219. package/tools/bench/obs-bench.mjs +520 -0
  220. package/tools/bench/score.mjs +343 -0
  221. package/tools/bench/summarize.mjs +86 -0
  222. package/tools/dev/pages/privacy.html +13 -8
  223. package/tools/gt-render/browser.mjs +159 -0
  224. package/tools/gt-render/camera-math.mjs +276 -0
  225. package/tools/gt-render/page.mjs +267 -0
  226. package/tools/gt-render/render.mjs +514 -0
  227. package/tools/gt-render/scene-box.mjs +31 -0
  228. package/tools/gt-render/take-transform.mjs +87 -0
  229. package/tools/qa/css-rule-usage.mjs +283 -0
  230. package/tools/qa/studio-control-count.mjs +70 -32
  231. package/tools/run-tests.mjs +58 -3
  232. package/tools/track/DESIGN.md +513 -0
  233. package/tools/track/backfill-provenance.mjs +116 -0
  234. package/tools/track/budget.mjs +8 -0
  235. package/tools/track/check-rig.mjs +209 -0
  236. package/tools/track/diagnostics.schema.json +60 -0
  237. package/tools/track/export-rig.mjs +256 -0
  238. package/tools/track/fallback.mjs +7 -0
  239. package/tools/track/fk-parity-fixture.mjs +161 -0
  240. package/tools/track/gate.mjs +349 -0
  241. package/tools/track/masks.mjs +318 -0
  242. package/tools/track/metrics.mjs +201 -0
  243. package/tools/track/publish-obs.mjs +108 -0
  244. package/tools/track/py/check_env.py +61 -0
  245. package/tools/track/py/eval_lr.py +229 -0
  246. package/tools/track/py/lr_viterbi.py +294 -0
  247. package/tools/track/py/masks.py +405 -0
  248. package/tools/track/py/objective.py +382 -0
  249. package/tools/track/py/rig.py +508 -0
  250. package/tools/track/py/scene.py +326 -0
  251. package/tools/track/py/test_joint_indices.py +123 -0
  252. package/tools/track/py/test_lr_viterbi.py +195 -0
  253. package/tools/track/py/test_masks.py +229 -0
  254. package/tools/track/py/test_rig.py +288 -0
  255. package/tools/track/py/test_scene.py +354 -0
  256. package/tools/track/py/test_track.py +328 -0
  257. package/tools/track/py/track.py +411 -0
  258. package/tools/track/remote.mjs +140 -0
  259. package/tools/track/rig-dump.mjs +227 -0
  260. package/tools/track/run-box-tests.mjs +14 -0
  261. package/tools/track/run-box.mjs +168 -0
  262. package/tools/track/setup-box.sh +59 -0
  263. package/tools/track/study-2d.mjs +728 -0
  264. package/dist/assets/analytics-B1hnH66c.js +0 -1
  265. package/dist/assets/app-Cu1cexZR.css +0 -1
  266. package/dist/assets/app-Dxdh8kds.js +0 -4861
  267. package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
  268. package/dist/assets/shot-prompt-HVJRP4Gr.js +0 -17
  269. package/dist/assets/workflow-CJd-PDtV.js +0 -179
  270. package/dist/assets/workflow-DsudxKHF.css +0 -1
  271. package/src/fal-motion-studio.jsx +0 -180
  272. package/src/scene-history.js +0 -129
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Code that runs INSIDE the Studio page (#428). `installPageHelpers` is
3
+ * serialised with Function.prototype.toString and evaluated in the page, so it
4
+ * must not reference anything from this module's scope: every dependency
5
+ * (the support reduction from camera-math.mjs, the joint table) is passed in.
6
+ *
7
+ * It only reads the Studio through the `window.__cozyclay` QA hook and the
8
+ * Three objects that hook exposes. Besides scrub/part colours it makes two
9
+ * transient writes, both undone before the call returns: hiding the rig for
10
+ * the background plate, and swapping the rig's materials to unlit magenta for
11
+ * the silhouette mask capture.
12
+ */
13
+
14
+ /** The unlit colour the mask capture paints the character with. */
15
+ export const MASK_RGB = [255, 0, 255];
16
+
17
+ /** Rig bones written to joints.json, in cskel27 order where one exists. The
18
+ * Mixamo spine has one segment fewer than cskel27, so Mixamo Spine/Spine1/
19
+ * Spine2 are the cskel27 Spine1/Spine2/Spine3 joints (src/ardy/playback.js).
20
+ * The Mixamo end sites (HeadTop_End, Toe_End) are left out: they sit outside
21
+ * the skin, so they are not points on the drawn body. */
22
+ export const RIG_JOINTS = [
23
+ ["Hips", "Hips"], ["Spine", "Spine1"], ["Spine1", "Spine2"], ["Spine2", "Spine3"],
24
+ ["Neck", "Neck"], ["Head", "Head"],
25
+ ["RightShoulder", "RightShoulder"], ["RightArm", "RightArm"], ["RightForeArm", "RightForeArm"], ["RightHand", "RightHand"],
26
+ ["LeftShoulder", "LeftShoulder"], ["LeftArm", "LeftArm"], ["LeftForeArm", "LeftForeArm"], ["LeftHand", "LeftHand"],
27
+ ["RightUpLeg", "RightUpLeg"], ["RightLeg", "RightLeg"], ["RightFoot", "RightFoot"], ["RightToeBase", "RightToeBase"],
28
+ ["LeftUpLeg", "LeftUpLeg"], ["LeftLeg", "LeftLeg"], ["LeftFoot", "LeftFoot"], ["LeftToeBase", "LeftToeBase"],
29
+ ].map(([bone, cskel27]) => ({ bone: `mixamorig${bone}`, cskel27 }));
30
+
31
+ export function installPageHelpers(supportValues, rigJoints, maskRgb, boxContactValues) {
32
+ const raf = () => new Promise((resolve) => requestAnimationFrame(() => resolve()));
33
+ const hook = () => window.__cozyclay;
34
+ let orientation = null;
35
+
36
+ /** Every bone of the rig by name. Mixamo FBX nests an identity copy of
37
+ * each bone and every skinned mesh has its own skeleton, so a name has
38
+ * several Bone objects; the first one a skinned mesh deforms with comes
39
+ * first. End sites (HeadTop_End, Toe_End) deform nothing and only exist in
40
+ * the hierarchy. readJoints asserts that all copies coincide. */
41
+ function skinBones(rig) {
42
+ const skinned = [];
43
+ rig.traverse((node) => {
44
+ if (node.isSkinnedMesh) skinned.push(node);
45
+ });
46
+ if (!skinned.length) throw new Error("active rig has no skinned mesh");
47
+ const byName = new Map();
48
+ const add = (bone) => {
49
+ const list = byName.get(bone.name) ?? [];
50
+ if (!list.includes(bone)) list.push(bone);
51
+ byName.set(bone.name, list);
52
+ };
53
+ for (const mesh of skinned) mesh.skeleton.bones.forEach(add);
54
+ rig.traverse((node) => {
55
+ if (node.isBone) add(node);
56
+ });
57
+ return byName;
58
+ }
59
+
60
+ function visibleMeshes(rig) {
61
+ const meshes = [];
62
+ rig.traverseVisible((node) => {
63
+ if (node.isMesh && node.layers.isEnabled(0)) meshes.push(node);
64
+ });
65
+ return meshes;
66
+ }
67
+
68
+ /** Scrub to `frame` and wait until the Studio has applied it: the hook
69
+ * reports the frame, then two animation frames so the pose effect and a
70
+ * paint have run on it. Already on the frame (e.g. right after a rig
71
+ * rebuild) it steps away first, so the pose is always freshly applied. */
72
+ async function moveTo(frame) {
73
+ hook().scrub(frame);
74
+ for (let i = 0; i < 600 && hook().tlFrame !== frame; i += 1) await raf();
75
+ }
76
+
77
+ async function settle(frame) {
78
+ hook().pause();
79
+ if (hook().tlFrame === frame && hook().frameCount > 1) await moveTo(frame === 0 ? 1 : 0);
80
+ hook().scrub(frame);
81
+ for (let i = 0; i < 600 && hook().tlFrame !== frame; i += 1) await raf();
82
+ if (hook().tlFrame !== frame) throw new Error(`timeline did not reach frame ${frame} (at ${hook().tlFrame})`);
83
+ await raf();
84
+ await raf();
85
+ const rig = hook().rigA;
86
+ rig.updateMatrixWorld(true);
87
+ return rig;
88
+ }
89
+
90
+ function readJoints(rig) {
91
+ const bones = skinBones(rig);
92
+ const out = [];
93
+ const v = new rig.position.constructor();
94
+ const other = new rig.position.constructor();
95
+ for (const { bone } of rigJoints) {
96
+ const copies = bones.get(bone);
97
+ if (!copies) throw new Error(`rig has no bone ${bone}`);
98
+ copies[0].getWorldPosition(v);
99
+ for (const copy of copies.slice(1)) {
100
+ const gap = copy.getWorldPosition(other).distanceTo(v);
101
+ if (gap > 1e-6) throw new Error(`bone copies of ${bone} disagree by ${gap} m`);
102
+ }
103
+ out.push(v.x, v.y, v.z);
104
+ }
105
+ return out;
106
+ }
107
+
108
+ /** Every vertex the capture can draw of this rig (skinned on the CPU with
109
+ * the same bone matrices the GPU uses), in world metres. */
110
+ function rigVertices(rig) {
111
+ const meshes = visibleMeshes(rig);
112
+ let total = 0;
113
+ for (const mesh of meshes) total += mesh.geometry.attributes.position.count;
114
+ const points = new Float64Array(total * 3);
115
+ const v = new rig.position.constructor();
116
+ let offset = 0;
117
+ for (const mesh of meshes) {
118
+ const count = mesh.geometry.attributes.position.count;
119
+ for (let i = 0; i < count; i += 1) {
120
+ if (mesh.isSkinnedMesh) mesh.getVertexPosition(i, v);
121
+ else v.fromBufferAttribute(mesh.geometry.attributes.position, i);
122
+ v.applyMatrix4(mesh.matrixWorld);
123
+ points[offset++] = v.x;
124
+ points[offset++] = v.y;
125
+ points[offset++] = v.z;
126
+ }
127
+ }
128
+ return points;
129
+ }
130
+
131
+ /** A clone of the shot camera set up exactly as captureFramingPng +
132
+ * createExportCapture set theirs, for an independent projection check. */
133
+ function captureCamera(framing, output) {
134
+ const cam = hook().shotCam.clone();
135
+ cam.position.set(framing.pos.x, framing.pos.y, framing.pos.z);
136
+ cam.rotation.order = "YXZ";
137
+ cam.rotation.set(framing.pitch, framing.yaw, 0);
138
+ cam.fov = framing.fovDeg;
139
+ cam.aspect = output.width / output.height;
140
+ cam.updateProjectionMatrix();
141
+ cam.updateMatrixWorld(true);
142
+ return cam;
143
+ }
144
+
145
+ window.__gtRender = {
146
+ state: () => {
147
+ const c = hook();
148
+ const rig = c?.rigA;
149
+ let vertexColors = null;
150
+ rig?.traverse((node) => {
151
+ if (node.isSkinnedMesh && vertexColors === null) vertexColors = !!node.material?.vertexColors;
152
+ });
153
+ const cam = c?.shotCam;
154
+ return {
155
+ ready: !!(c && c.motion && rig && typeof c.captureFraming === "function"),
156
+ motionUrl: c?.motion?.url ?? null,
157
+ frameCount: c?.frameCount ?? null,
158
+ fps: c?.motion?.fps ?? null,
159
+ characterModel: c?.characterModel ?? null,
160
+ characterScale: c?.characterScale ?? null,
161
+ rigUuid: rig?.uuid ?? null,
162
+ // Where playback put frame 0 of the take (scene metres) and the
163
+ // Character group's yaw: the pivot of a scene-calibration yaw.
164
+ motionAnchor: c?.motion ? { x: c.motion.anchorX ?? null, z: c.motion.anchorZ ?? null, rotationDeg: c.motion.rotationDeg ?? null } : null,
165
+ vertexColors,
166
+ shotCam: cam ? { zoom: cam.zoom, filmOffset: cam.filmOffset, view: cam.view, near: cam.near, far: cam.far } : null,
167
+ };
168
+ },
169
+ placeBox: async (b, offset) => {
170
+ const { id } = hook().sceneObject.place({ kind: "cube", x: b.x - offset.x, y: -offset.y, z: b.z - offset.z, rot: b.rot, name: "Bench contact cube" });
171
+ let scene = hook().rigA;
172
+ while (scene.parent) scene = scene.parent;
173
+ const find = () => { let found; scene.traverse(n => { if (n.userData.sceneObjectId === id) found = n; }); return found; };
174
+ const until = async predicate => {
175
+ const end = performance.now() + 10000;
176
+ while (!predicate()) { if (performance.now() > end) throw new Error("Studio cube placement did not settle"); await raf(); }
177
+ };
178
+ await until(find);
179
+ hook().sceneObject.update({ id, scaleX: b.sx, scaleY: b.sy, scaleZ: b.sz });
180
+ await until(() => { const n = find(); return n && Math.abs(n.scale.x - b.sx) < 1e-9 && Math.abs(n.scale.y - b.sy) < 1e-9 && Math.abs(n.scale.z - b.sz) < 1e-9; });
181
+ const n = find(); n.updateWorldMatrix(true, true);
182
+ return { id, position: n.position.toArray(), scale: n.scale.toArray(), yaw: n.rotation.y };
183
+ },
184
+ setOrientation: (R, kx, ky) => {
185
+ orientation = { R, kx, ky };
186
+ },
187
+ /** Pre-pass sample: joints plus the five support values of every
188
+ * drawable vertex and joint of the frame, and with `box` the signed
189
+ * distance of the closest skinned vertex to that box. `vertices: false`
190
+ * skips the CPU skinning (joints only). */
191
+ sample: async (frame, { vertices: withVertices = true, box = null, exportVertices = false } = {}) => {
192
+ if (!orientation) throw new Error("setOrientation first");
193
+ const rig = await settle(frame);
194
+ const joints = readJoints(rig);
195
+ if (!withVertices && !box && !exportVertices) return { joints, support: null, vertexCount: 0, contact: null };
196
+ const vertices = rigVertices(rig);
197
+ const contact = box ? boxContactValues(vertices, box) : null;
198
+ const all = new Float64Array(vertices.length + joints.length);
199
+ all.set(vertices, 0);
200
+ all.set(joints, vertices.length);
201
+ const support = supportValues(all, orientation.R, orientation.kx, orientation.ky);
202
+ return { joints, support, vertexCount: vertices.length / 3, contact, ...(exportVertices ? { vertices: Array.from(vertices) } : {}) };
203
+ },
204
+ /** Render one timeline frame through the real export path and read the
205
+ * joints of the exact pose it drew. `withColour: false` skips the
206
+ * colour capture (mask-only renders). */
207
+ capture: async (frame, framing, output, withMask, withColour = true) => {
208
+ const rig = await settle(frame);
209
+ const joints = readJoints(rig);
210
+ const png = withColour ? hook().captureFraming(framing, output) : null;
211
+ if (withColour && (typeof png !== "string" || !png.startsWith("data:image/png;base64,"))) throw new Error(`captureFraming returned no PNG for frame ${frame}`);
212
+ let mask = null;
213
+ if (withMask) {
214
+ // Same pose, same camera, character drawn unlit in one colour no
215
+ // lit scene pixel can take: the exact silhouette of this frame.
216
+ // Three is not a page global, so the rig's own MeshStandardMaterial
217
+ // class is reused: black + metalness 1 zeroes diffuse and specular
218
+ // (F0 = colour), leaving only the emissive, untone-mapped, unfogged.
219
+ const swapped = visibleMeshes(rig).map((mesh) => [mesh, mesh.material]);
220
+ const standard = swapped.map(([, material]) => material).find((material) => material?.isMeshStandardMaterial);
221
+ if (!standard) throw new Error("rig has no MeshStandardMaterial to build the mask material from");
222
+ const flat = new standard.constructor({ color: 0x000000, metalness: 1, roughness: 1, envMapIntensity: 0, toneMapped: false, fog: false });
223
+ flat.emissive.setRGB(maskRgb[0] / 255, maskRgb[1] / 255, maskRgb[2] / 255, "srgb");
224
+ try {
225
+ for (const [mesh] of swapped) mesh.material = flat;
226
+ mask = hook().captureFraming(framing, output);
227
+ } finally {
228
+ for (const [mesh, material] of swapped) mesh.material = material;
229
+ flat.dispose();
230
+ }
231
+ if (typeof mask !== "string" || !mask.startsWith("data:image/png;base64,")) throw new Error(`mask capture failed for frame ${frame}`);
232
+ mask = mask.slice("data:image/png;base64,".length);
233
+ }
234
+ const cam = captureCamera(framing, output);
235
+ const v = new rig.position.constructor();
236
+ const threeUv = [];
237
+ for (let i = 0; i < joints.length; i += 3) {
238
+ v.set(joints[i], joints[i + 1], joints[i + 2]).project(cam);
239
+ threeUv.push(((v.x + 1) / 2) * output.width, ((1 - v.y) / 2) * output.height);
240
+ }
241
+ return { png: png ? png.slice("data:image/png;base64,".length) : null, mask, joints, threeUv };
242
+ },
243
+ /** The same framing with the character hidden: the background plate. */
244
+ plate: (framing, output) => {
245
+ const rig = hook().rigA;
246
+ const visible = rig.visible;
247
+ rig.visible = false;
248
+ try {
249
+ const png = hook().captureFraming(framing, output);
250
+ return png.slice("data:image/png;base64,".length);
251
+ } finally {
252
+ rig.visible = visible;
253
+ }
254
+ },
255
+ /** Switch part colours and wait for the rebuilt rig to carry them. */
256
+ setPartColours: async (enabled) => {
257
+ hook().setPartColours(enabled, "shaded");
258
+ for (let i = 0; i < 600; i += 1) {
259
+ await raf();
260
+ const state = window.__gtRender.state();
261
+ if (state.ready && state.vertexColors === enabled) return state;
262
+ }
263
+ throw new Error(`part colours did not become ${enabled ? "shaded" : "off"}`);
264
+ },
265
+ };
266
+ return true;
267
+ }