cozyclay 2.0.2 → 2.1.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 (262) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +17 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/bin/agent/agent-routes.mjs +211 -36
  5. package/bin/agent/agent-runner.mjs +98 -4
  6. package/bin/agent/codex-client.mjs +2 -1
  7. package/bin/agent/fal-contract.mjs +0 -17
  8. package/bin/agent/h3-preservation.mjs +6 -8
  9. package/bin/agent/provider-keys.mjs +1 -1
  10. package/bin/agent/providers.mjs +124 -3
  11. package/bin/agent/script-tool.mjs +74 -0
  12. package/bin/agent/session-store.mjs +5 -5
  13. package/bin/agent/studio-prompt.mjs +2 -1
  14. package/bin/agent/studio-rebase.mjs +314 -0
  15. package/bin/agent/studio-tools.mjs +21 -14
  16. package/bin/agent/supervisor.mjs +311 -0
  17. package/bin/agent/video-adapters.mjs +0 -7
  18. package/bin/codex-auth.mjs +4 -4
  19. package/bin/live/cli.mjs +23 -8
  20. package/bin/live/client.mjs +1 -1
  21. package/bin/live/studio.mjs +5 -2
  22. package/bin/mcp-runtime.mjs +2 -2
  23. package/bin/telemetry-state.mjs +3 -3
  24. package/dist/app/index.html +6 -6
  25. package/dist/assets/analytics-C9pMpW7s.js +1 -0
  26. package/dist/assets/app-B_2XB9rw.css +1 -0
  27. package/dist/assets/app-DAuvKPP8.js +4920 -0
  28. package/dist/assets/{demo-ByeYp-dY.js → demo-CvKuBgy5.js} +1 -1
  29. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-msLI71g1.js} +1 -1
  30. package/dist/assets/{landing-BaXkpFip.js → landing-ChUvB_zL.js} +1 -1
  31. package/dist/assets/scene-asset-sync-DKkpjKBV.js +19 -0
  32. package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
  33. package/dist/assets/{ticket-BcmsnHn5.js → ticket-BPk2Ni95.js} +1 -1
  34. package/dist/assets/workflow-jJRBR94Z.js +179 -0
  35. package/dist/cozyclay-package.json +1 -1
  36. package/dist/index.html +328 -222
  37. package/dist/llms.txt +14 -0
  38. package/dist/sitemap.xml +1 -1
  39. package/dist/workflow/index.html +4 -4
  40. package/mcp/LIVE-PROTOCOL.md +23 -2
  41. package/mcp/README.md +37 -2
  42. package/mcp/ardy-prompts.mjs +1 -1
  43. package/mcp/image-file.mjs +132 -0
  44. package/mcp/live-hub.mjs +2 -2
  45. package/mcp/package.json +1 -0
  46. package/mcp/runtime/package-lock.json +7 -0
  47. package/mcp/runtime/package.json +1 -0
  48. package/mcp/server.mjs +0 -6
  49. package/mcp/tool-handlers.mjs +289 -87
  50. package/package.json +11 -2
  51. package/src/App.jsx +813 -278
  52. package/src/agent-image-client.js +94 -0
  53. package/src/analytics.js +47 -3
  54. package/src/app-stage.jsx +129 -2745
  55. package/src/ardy/auto-physics.js +6 -16
  56. package/src/ardy/collision-blockers.js +3 -7
  57. package/src/ardy/fix-collisions.js +5 -35
  58. package/src/ardy/ik-drag.js +86 -5
  59. package/src/ardy/ik-key-json.js +3 -1
  60. package/src/ardy/ik.js +15 -13
  61. package/src/ardy/motion-calibration.js +1 -1
  62. package/src/ardy/npz.js +0 -8
  63. package/src/ardy/physics-panel.jsx +3 -2
  64. package/src/ardy/physics-review.js +3 -2
  65. package/src/ardy/physics-support.js +1 -1
  66. package/src/ardy/physics-surface.js +1 -1
  67. package/src/ardy/platform-fit-panel.jsx +2 -1
  68. package/src/ardy/playback.js +1 -5
  69. package/src/ardy/range-pin.js +2 -2
  70. package/src/ardy/root-drop.js +0 -1
  71. package/src/ardy/timeline.css +184 -6
  72. package/src/ardy/timeline.jsx +121 -36
  73. package/src/ardy/to-cskel27.js +0 -2
  74. package/src/ardy/waypoints.js +0 -3
  75. package/src/ardy-bridge-contract.js +281 -0
  76. package/src/asset-pane.jsx +19 -9
  77. package/src/auto-color.js +1 -1
  78. package/src/burn-in.js +1 -1
  79. package/src/camera-block.js +1 -1
  80. package/src/camera-follow.js +75 -32
  81. package/src/camera-move.js +0 -6
  82. package/src/camera-tutorial.jsx +6 -7
  83. package/src/character-kind.js +79 -0
  84. package/src/command-bus.js +76 -17
  85. package/src/commands/cast.js +31 -7
  86. package/src/commands/elements/object.js +1 -1
  87. package/src/commands/elements/shot.js +1 -1
  88. package/src/commands/elements.js +1 -1
  89. package/src/commands/export.js +3 -3
  90. package/src/commands/motion.js +2 -2
  91. package/src/commands/objects.js +105 -10
  92. package/src/commands/project.js +15 -2
  93. package/src/commands/sculpt-images.js +53 -0
  94. package/src/commands/shot.js +88 -10
  95. package/src/commands/view.js +10 -4
  96. package/src/coplanar-depth.js +216 -0
  97. package/src/cuts.js +7 -6
  98. package/src/domains/cast.js +60 -43
  99. package/src/domains/motion/motion-intent-codec.js +50 -0
  100. package/src/domains/motion/motion-range-pin-rules.js +23 -0
  101. package/src/domains/motion/motion-video-capture.js +664 -0
  102. package/src/domains/motion.js +253 -742
  103. package/src/domains/objects.js +106 -5
  104. package/src/domains/scenes.js +79 -28
  105. package/src/domains/shots.js +33 -7
  106. package/src/error-boundary.jsx +1 -1
  107. package/src/execution-telemetry.js +5 -5
  108. package/src/facing-marks.js +14 -1
  109. package/src/first-success-guide.jsx +35 -4
  110. package/src/gizmo-claim.js +3 -1
  111. package/src/grid-floor.jsx +1 -4
  112. package/src/hierarchy-model.js +48 -35
  113. package/src/hierarchy-panel.css +1 -0
  114. package/src/hierarchy-panel.jsx +54 -102
  115. package/src/history.js +0 -2
  116. package/src/i2v-motion-client.js +1 -4
  117. package/src/ime.js +6 -0
  118. package/src/keyframe-pack.js +15 -6
  119. package/src/line-edit.js +4 -4
  120. package/src/live-control.js +6 -6
  121. package/src/matte-editor.js +2 -8
  122. package/src/matte.js +0 -19
  123. package/src/mesh-graph-clone.js +16 -2
  124. package/src/mesh-sniff.js +0 -1
  125. package/src/monotone-cubic.js +40 -0
  126. package/src/motion-store.js +4 -4
  127. package/src/motion-trail.js +5 -11
  128. package/src/multimodel-ingest.js +1 -3
  129. package/src/object-catalog.jsx +7 -5
  130. package/src/object-gizmo.jsx +45 -31
  131. package/src/object-path.js +310 -67
  132. package/src/object-travel.js +224 -0
  133. package/src/otio.js +8 -3
  134. package/src/panels/CharacterTransformPanel.jsx +24 -6
  135. package/src/panels/ObjectTransformPanel.jsx +4 -3
  136. package/src/panels/PoseImagePanel.jsx +205 -0
  137. package/src/panels/PromptBlocksPanel.jsx +3 -1
  138. package/src/panels/ProxyFigurePanel.jsx +56 -0
  139. package/src/panels/ReferenceImageField.jsx +1 -1
  140. package/src/panels/RigControlPanel.jsx +24 -3
  141. package/src/panels/RigPanel.jsx +2 -1
  142. package/src/panels/SculptPanel.jsx +405 -0
  143. package/src/panels/SubjectBox.jsx +1 -1
  144. package/src/panels/SubjectsPanel.jsx +0 -2
  145. package/src/panels/VideoCapturePanel.jsx +2 -1
  146. package/src/panels/pose-image.css +42 -0
  147. package/src/panels/pose.css +3 -0
  148. package/src/panels/sculpt.css +162 -0
  149. package/src/plan-diagram.js +184 -0
  150. package/src/planview.jsx +127 -80
  151. package/src/playground.js +3 -5
  152. package/src/pose-fade.js +77 -0
  153. package/src/pose-gizmo.css +23 -0
  154. package/src/pose-gizmo.js +96 -0
  155. package/src/posestudio.jsx +134 -83
  156. package/src/project-browser.css +53 -0
  157. package/src/project-browser.jsx +49 -4
  158. package/src/project.js +55 -24
  159. package/src/props.jsx +177 -36
  160. package/src/proxy-figure.jsx +69 -0
  161. package/src/range-pin-panel.jsx +0 -1
  162. package/src/receipt-findings.js +20 -0
  163. package/src/render-passes.js +0 -1
  164. package/src/resource-status.jsx +1 -1
  165. package/src/root-path.js +48 -0
  166. package/src/route-owner.js +52 -0
  167. package/src/scene-asset-cache.js +1 -2
  168. package/src/scene-assets.js +12 -4
  169. package/src/scene-fbx.js +1 -1
  170. package/src/scene-mesh-cache.js +0 -1
  171. package/src/scene-objects.js +319 -19
  172. package/src/scenes.js +58 -9
  173. package/src/sculpt-blueprint.js +65 -0
  174. package/src/sculpt-compare.js +384 -0
  175. package/src/sculpt-geometry.js +141 -0
  176. package/src/sculpt-mode.js +168 -0
  177. package/src/sculpt-recipe.js +385 -0
  178. package/src/sculpt-run.js +351 -0
  179. package/src/sculpt-stage.jsx +221 -0
  180. package/src/settings-menu.jsx +1 -1
  181. package/src/shell/BoardSlot.jsx +341 -0
  182. package/src/shell/BottomDock.jsx +60 -16
  183. package/src/shell/CompositionGuideButton.jsx +20 -0
  184. package/src/shell/DetailsSlot.jsx +26 -6
  185. package/src/shell/MenuBar.jsx +78 -6
  186. package/src/shell/OutlinerSlot.jsx +3 -1
  187. package/src/shell/ShotPreviewHeader.jsx +17 -0
  188. package/src/shell/TopBar.jsx +18 -3
  189. package/src/shell/ViewportToolbar.jsx +36 -31
  190. package/src/shell/dock.css +239 -0
  191. package/src/shell/glass-regions.css +3 -2
  192. package/src/shell/log-store.js +4 -4
  193. package/src/shell/mode.css +20 -0
  194. package/src/shell/studio-shell-context.js +4 -1
  195. package/src/shell/viewport.css +12 -0
  196. package/src/shot-authoring.js +69 -6
  197. package/src/stable-items.js +1 -1
  198. package/src/stage-attach-frames.js +169 -0
  199. package/src/stage-canvas-rigs.jsx +402 -0
  200. package/src/stage-capture.jsx +216 -0
  201. package/src/stage-characters.jsx +302 -0
  202. package/src/stage-editor-gizmos.jsx +1229 -0
  203. package/src/stage-labels-ko.js +204 -0
  204. package/src/stage-startup-defaults.js +182 -0
  205. package/src/starter-scenes.js +1 -1
  206. package/src/storyboard.js +24 -5
  207. package/src/studio-actions.js +42 -9
  208. package/src/studio-agent-commands.js +213 -23
  209. package/src/studio-agent-context.js +11 -9
  210. package/src/studio-agent-motion.js +2 -1
  211. package/src/studio-agent-protocol.js +60 -24
  212. package/src/studio-app-binding.js +56 -23
  213. package/src/studio-contact-sheet.js +3 -3
  214. package/src/studio-elements.js +11 -2
  215. package/src/studio-geometry-facts.js +114 -0
  216. package/src/styles.css +92 -9
  217. package/src/ui-scrub.js +4 -4
  218. package/src/ui.jsx +6 -4
  219. package/src/use-asset-shelf.js +60 -0
  220. package/src/use-camera-tutorial.js +50 -0
  221. package/src/use-embed-bridge.js +12 -0
  222. package/src/use-first-success-prompt.js +7 -0
  223. package/src/use-i2v-capture-modal.js +15 -0
  224. package/src/use-sculpt-mode.js +95 -0
  225. package/src/workflow/AgentPanel.jsx +106 -18
  226. package/src/workflow/WorkflowBuilder.jsx +1 -1
  227. package/src/workflow/agent-client.js +113 -45
  228. package/src/workflow/agent-panel.css +35 -0
  229. package/src/workflow/attachment-image.js +1 -1
  230. package/src/workflow/canvas-commands.js +3 -3
  231. package/src/workflow/cozy-scene-node.js +3 -3
  232. package/src/workflow/local-workflow.js +2 -2
  233. package/src/workflow/motion-input.js +3 -3
  234. package/src/workflow/node-schema.js +1 -1
  235. package/src/workflow/scene-asset-sync.js +2 -9
  236. package/src/workflow/shot-prompt-node.js +1 -1
  237. package/src/workflow/video-contract.js +1 -1
  238. package/src/workflow/workflow-resources.js +0 -8
  239. package/src/workflow-send.js +87 -0
  240. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  241. package/tools/check-eslint.mjs +57 -0
  242. package/tools/check-knip.mjs +60 -0
  243. package/tools/check-loc.mjs +54 -0
  244. package/tools/demo-worker/api-client.mjs +7 -13
  245. package/tools/demo-worker/generate.mjs +2 -4
  246. package/tools/eslint-baseline.json +23 -0
  247. package/tools/gt-render/browser.mjs +23 -0
  248. package/tools/headless-studio.mjs +85 -0
  249. package/tools/kimodo/effector-constraints.mjs +4 -4
  250. package/tools/kimodo/generate.mjs +1 -1
  251. package/tools/knip-baseline.json +331 -0
  252. package/tools/loc-baseline.json +126 -0
  253. package/tools/process-supervisor.mjs +1 -1
  254. package/tools/projflow/replay.mjs +4 -4
  255. package/tools/projflow/runner.mjs +3 -3
  256. package/tools/qa/studio-control-count.mjs +2 -0
  257. package/tools/run-tests.mjs +95 -7
  258. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  259. package/dist/assets/app-BxBLC6GT.js +0 -4876
  260. package/dist/assets/app-CJdxvnrl.css +0 -1
  261. package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
  262. package/dist/assets/workflow-B5XZwVfe.js +0 -179
@@ -21,7 +21,7 @@ function publish(next) {
21
21
  * newest entry updates that entry in place instead of adding a row, so an
22
22
  * encoding counter reads as one line that ticks.
23
23
  */
24
- export function push({ kind = "info", text, key = null, at = Date.now() }) {
24
+ function push({ kind = "info", text, key = null, at = Date.now() }) {
25
25
  const message = typeof text === "string" ? text.trim() : "";
26
26
  if (!message) return null;
27
27
  const last = entries[entries.length - 1];
@@ -36,16 +36,16 @@ export function push({ kind = "info", text, key = null, at = Date.now() }) {
36
36
  return entry;
37
37
  }
38
38
 
39
- export function getEntries() {
39
+ function getEntries() {
40
40
  return entries;
41
41
  }
42
42
 
43
- export function subscribe(listener) {
43
+ function subscribe(listener) {
44
44
  listeners.add(listener);
45
45
  return () => listeners.delete(listener);
46
46
  }
47
47
 
48
- export function clear() {
48
+ function clear() {
49
49
  if (entries.length) publish([]);
50
50
  }
51
51
 
@@ -7,3 +7,23 @@
7
7
  .app[data-workflow-mode="pose"] .workflow-toolbar-hint { display: inline-flex; }
8
8
  .app[data-workflow-mode="pose"] .vp-inset,
9
9
  .app[data-workflow-mode="pose"] .vp-shot-preview { display: none !important; }
10
+
11
+ /* A storyboard project (#650) is stills: the take, motion and pose-correction
12
+ tools, the root-path (waypoint) switch and a prop's Top-View travel path
13
+ never show, whatever the mode. */
14
+ .app[data-previs-mode="storyboard"] .take-bar,
15
+ .app[data-previs-mode="storyboard"] .tl-motion-tools,
16
+ .app[data-previs-mode="storyboard"] .tl-pose-tools,
17
+ .app[data-previs-mode="storyboard"] .tl-btn.wp,
18
+ .app[data-previs-mode="storyboard"] .tl-wp-hint,
19
+ .app[data-previs-mode="storyboard"] .tl-track.objmo { display: none !important; }
20
+
21
+ /* The project mode under the project name: secondary text, never a control. */
22
+ .topbar-previs-mode {
23
+ color: var(--text-2, #9b9ba1);
24
+ font-size: 11px;
25
+ font-weight: 500;
26
+ line-height: 1.3;
27
+ letter-spacing: .02em;
28
+ white-space: nowrap;
29
+ }
@@ -11,7 +11,10 @@ import { createContext, useContext } from "react";
11
11
  // - generation, blocks/refine, takes/recipe and preserve strength;
12
12
  // - agentOpen/toggleAgent, preferencesOpen/setPreferencesOpen;
13
13
  // - statusText/setStatusText, previewRanges/setPreviewRanges, liveHubStatus,
14
- // flySpeed (the navigation multiplier, not metres per second).
14
+ // flySpeed (the navigation multiplier, not metres per second);
15
+ // - previsMode ("storyboard" | "animation"), sceneRevision (bumps on every
16
+ // authored edit) and capturePanelThumbnail(shot) (the shot's first frame as
17
+ // a PNG data URL, or null) for the Board dock tab.
15
18
  export const StudioShellContext = createContext(null);
16
19
 
17
20
  export function useStudioShell() {
@@ -32,6 +32,18 @@
32
32
  color: var(--text-1);
33
33
  }
34
34
  .app[data-embed-mode] .viewport-titlebar { display: none; }
35
+ .app[data-embed-mode] .vp-embed-guide {
36
+ position: absolute;
37
+ top: var(--sp-1);
38
+ right: var(--sp-1);
39
+ z-index: 6;
40
+ padding: var(--sp-1);
41
+ border-radius: var(--r-2);
42
+ background: var(--surface-panel);
43
+ color: var(--text-2);
44
+ }
45
+ .app[data-embed-mode] .vp-embed-guide:hover,
46
+ .app[data-embed-mode] .vp-embed-guide.on { color: var(--select); }
35
47
 
36
48
  .viewport-titlebar button { font-family: inherit; }
37
49
  .viewport-titlebar button:focus-visible {
@@ -9,26 +9,62 @@ import { createShot } from "./cuts.js";
9
9
  import { normalizeStableItems } from "./stable-items.js";
10
10
  import { VIDEO_MODEL_PRESETS } from "./model-presets.js";
11
11
  import { elementByPath } from "./studio-elements.js";
12
+ import { isImageAssetId } from "./scene-assets.js";
13
+ import { createCharacterEntry } from "./scenes.js";
12
14
 
13
15
  const VIDEO_MODEL_IDS = new Set(VIDEO_MODEL_PRESETS.map((preset) => preset.id));
14
16
  const CAMERA_KEY_LIMITS = elementByPath("shot.cameraKeys");
15
17
 
16
- export const SHOT_AUTHORING_VERSION = 4;
17
- export const SHOT_AUTHORING_KEY = "cozyclay.shot-authoring.v4";
18
+ export const SHOT_AUTHORING_VERSION = 5;
19
+ export const SHOT_AUTHORING_KEY = "cozyclay.shot-authoring.v5";
18
20
  // The single-key alias points at the newest legacy body for older callers.
19
21
  // New readers should walk the list so a user can still arrive directly from v1.
20
- export const SHOT_AUTHORING_LEGACY_KEY = "cozyclay.shot-authoring.v3";
22
+ export const SHOT_AUTHORING_LEGACY_KEY = "cozyclay.shot-authoring.v4";
21
23
  export const SHOT_AUTHORING_LEGACY_KEYS = Object.freeze([
22
24
  SHOT_AUTHORING_LEGACY_KEY,
25
+ "cozyclay.shot-authoring.v3",
23
26
  "cozyclay.shot-authoring.v2",
24
27
  "cozyclay.shot-authoring.v1",
25
28
  ]);
26
- export const SHOT_AUTHORING_QUARANTINE_KEY = "cozyclay.shot-authoring.v4.quarantine";
29
+ export const SHOT_AUTHORING_QUARANTINE_KEY = "cozyclay.shot-authoring.v5.quarantine";
30
+
31
+ /** A still holds one picture for `endFrame - startFrame + 1` frames (24 fps). */
32
+ export const STILL_HOLD_DEFAULT = 48;
33
+ export const STILL_HOLD_MAX = 240;
34
+ export const shotHold = (shot) => shot.endFrame - shot.startFrame + 1;
35
+
36
+ /** Lay the stills end to end from frame 0 in their array order, each keeping
37
+ * its hold; camera keys travel with their still. Clip shots are untouched.
38
+ * The result is sorted by startFrame, the order the document stores. */
39
+ export function reflowStillShots(shots) {
40
+ let cursor = 0;
41
+ return shots.map((shot) => {
42
+ if (shot.kind !== "still") return shot;
43
+ const hold = shotHold(shot), delta = cursor - shot.startFrame;
44
+ cursor += hold;
45
+ if (!delta) return shot;
46
+ return { ...shot, startFrame: shot.startFrame + delta, endFrame: shot.endFrame + delta,
47
+ cameraKeys: shot.cameraKeys.map((key) => ({ ...key, frame: key.frame + delta })) };
48
+ }).sort((a, b) => a.startFrame - b.startFrame);
49
+ }
50
+
51
+ /** The timeline length a reflowed document needs, unclamped so a caller can
52
+ * refuse a storyboard past STILL_FRAME_COUNT_MAX instead of truncating it. */
53
+ export function stillFrameCount(shots) {
54
+ return Math.max(DEFAULT_FRAME_COUNT, ...shots.map((shot) => shot.endFrame + 1));
55
+ }
56
+
57
+ const SHOT_KINDS = new Set(["clip", "still"]);
58
+ const CAPTION_MAX = 500;
59
+ // Mirrors the cast posture vocabulary (POSTURES in src/scenes.js once the
60
+ // proxy-figure work lands); kept local so this module does not wait on it.
61
+ const POSTURES = new Set(["stand", "sit", "lie"]);
27
62
 
28
63
  /** clip length sanity bounds, frames @ 24 fps: 1 s .. 20 min */
29
64
  const FRAME_COUNT_MIN = 24;
30
65
  const FRAME_COUNT_MAX = 28800;
31
66
  const DEFAULT_FRAME_COUNT = 144;
67
+ export const STILL_FRAME_COUNT_MAX = FRAME_COUNT_MAX;
32
68
  const RAIL_MAX_POINTS = 512;
33
69
  const finite = Number.isFinite;
34
70
 
@@ -159,10 +195,36 @@ function repairShots(entries, frameCount, inheritedCamera = null, ids = new Set(
159
195
  cameraKeys: repairKeys(entry.cameraKeys, startFrame, endFrame, ids),
160
196
  camera: repairCamera(inheritedCamera ?? entry.camera),
161
197
  ...targetModel,
198
+ kind: SHOT_KINDS.has(entry.kind) ? entry.kind : "clip",
199
+ caption: repairCaption(entry.caption),
200
+ cast: repairCast(entry.cast),
201
+ stylizedAssetId: isImageAssetId(entry.stylizedAssetId) ? entry.stylizedAssetId : null,
162
202
  };
163
203
  });
164
204
  }
165
205
 
206
+ function repairCaption(value) {
207
+ return typeof value === "string" ? Array.from(value.trim()).slice(0, CAPTION_MAX).join("") : "";
208
+ }
209
+
210
+ /** Per-shot placement overrides keyed by character id. An entry without a
211
+ * usable id or a finite x/z/rot is dropped; its valid siblings survive. */
212
+ function repairCast(value) {
213
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
214
+ const rows = [];
215
+ for (const [characterId, entry] of Object.entries(value)) {
216
+ if (!characterId.trim() || !entry || typeof entry !== "object" || Array.isArray(entry)) continue;
217
+ if (!finite(entry.x) || !finite(entry.z) || !finite(entry.rot)) continue;
218
+ const row = { x: entry.x, z: entry.z, rot: entry.rot };
219
+ if (POSTURES.has(entry.posture)) row.posture = entry.posture;
220
+ const pose = createCharacterEntry({ pose: entry.pose }).pose;
221
+ if (pose) row.pose = pose;
222
+ rows.push([characterId, row]);
223
+ }
224
+ // fromEntries defines own properties, so a "__proto__" id stays data.
225
+ return Object.fromEntries(rows);
226
+ }
227
+
166
228
  function repairWaypoints(entries, ids = new Set()) {
167
229
  const byFrame = new Map();
168
230
  for (const waypoint of normalizeStableItems(entries, "waypoint", ids)) {
@@ -313,8 +375,9 @@ export function readShotAuthoringDocument(raw) {
313
375
  }
314
376
  const ids = new Set();
315
377
  return {
316
- // A v3 body is structurally current but was authored on the old clock;
317
- // it is rewritten, so it reports as migrated, not valid.
378
+ // A v3 body was authored on the old clock and a v4 body lacks the v5
379
+ // kind/caption/cast/stylizedAssetId defaults; both are rewritten, so they
380
+ // report as migrated, not valid.
318
381
  status: version < SHOT_AUTHORING_VERSION ? "migrated" : "valid",
319
382
  state: { ...repairShared(parsed, frameCount, ids), shots: repairShots(parsed.shots, effectiveFrameCount, null, ids) },
320
383
  };
@@ -18,7 +18,7 @@ function hash(value) {
18
18
  return (result >>> 0).toString(36);
19
19
  }
20
20
 
21
- export class UnknownStableItemIdError extends Error {
21
+ class UnknownStableItemIdError extends Error {
22
22
  constructor(collection, id) {
23
23
  super(`Unknown ${collection} ID: ${id}`);
24
24
  this.name = "UnknownStableItemIdError";
@@ -0,0 +1,169 @@
1
+ import * as THREE from "three";
2
+ import { SCENE_ATTACH_BONES } from "./scene-objects.js";
3
+ import { TRAIL_EFFECTOR_JOINTS } from "./motion-trail.js";
4
+ import { normalizeBoneName } from "./poses.js";
5
+
6
+ /** The rig rows that ARE an attach frame: `rig.<track id>`, the same camel
7
+ * spellings SCENE_ATTACH_BONES uses. The group rows above them (rig.torso,
8
+ * rig.leftArm …) name a limb, not a frame, so they are deliberately absent. */
9
+ export const ATTACH_BONE_ROWS = new Map(SCENE_ATTACH_BONES.map((bone) => [`rig.${bone}`, bone]));
10
+
11
+ /** Attach track id -> the Mixamo bone carrying that frame on the shipped rigs.
12
+ * TRAIL_EFFECTOR_JOINTS names the cskel27 joint, and cskel27 and Mixamo agree
13
+ * everywhere except the spine, which sits one bone up the Mixamo chain (the
14
+ * same shift ardy/playback.js SKINNING_MAP makes). */
15
+ export const ATTACH_BONE_NAMES = Object.fromEntries(SCENE_ATTACH_BONES.map((bone) => {
16
+ const joint = TRAIL_EFFECTOR_JOINTS[bone];
17
+ return [bone, joint === "Spine1" ? "Spine" : joint === "Spine2" ? "Spine1" : joint];
18
+ }));
19
+
20
+ // One scratch set for the whole module: resolving a frame runs per attached
21
+ // prop per frame and must never allocate.
22
+ export const attachPos = new THREE.Vector3();
23
+ export const attachQuat = new THREE.Quaternion();
24
+ export const attachScale = new THREE.Vector3();
25
+ export const attachRootPos = new THREE.Vector3();
26
+ export const attachRootQuat = new THREE.Quaternion();
27
+ export const ATTACH_UNIT_SCALE = new THREE.Vector3(1, 1, 1);
28
+ export const attachWorldMatrix = new THREE.Matrix4();
29
+ export const attachLocalMatrix = new THREE.Matrix4();
30
+ export const attachToMatrix = new THREE.Matrix4();
31
+ export const attachInverseMatrix = new THREE.Matrix4();
32
+ export const placePos = new THREE.Vector3();
33
+ export const placeQuat = new THREE.Quaternion();
34
+ export const placeScale = new THREE.Vector3();
35
+ export const placeEuler = new THREE.Euler();
36
+
37
+ /** The bone a rig offers under `mixamoName`, by the project's matching rule
38
+ * (normalised names equal, or one a suffix of the other; first depth-first
39
+ * match wins — poses.js and ardy/playback.js agree). Cached per rig: this is
40
+ * asked once per attached prop per rendered frame. */
41
+ export const attachBoneCache = new WeakMap();
42
+ export function attachBoneOf(rig, mixamoName) {
43
+ let byName = attachBoneCache.get(rig);
44
+ if (!byName) attachBoneCache.set(rig, (byName = new Map()));
45
+ if (byName.has(mixamoName)) return byName.get(mixamoName);
46
+ const target = normalizeBoneName(mixamoName);
47
+ let found = null;
48
+ rig.traverse((node) => {
49
+ if (found || !node.isBone) return;
50
+ const norm = normalizeBoneName(node.name);
51
+ if (norm === target || norm.endsWith(target)) found = node;
52
+ });
53
+ byName.set(mixamoName, found);
54
+ return found;
55
+ }
56
+
57
+ /**
58
+ * The LIVE world frame an attachment rides, or null when the rig (or that bone)
59
+ * is not there — a dangling attachment then renders where its numbers say,
60
+ * which is exactly what a detached prop does.
61
+ *
62
+ * The frame is deliberately UNSCALED. The shipped rigs are Mixamo centimetres
63
+ * scaled by 0.01 on the model, so a raw bone matrix would push a hundredfold
64
+ * into every carried prop's local numbers and leave the Inspector unreadable.
65
+ * A carried prop takes the bone's place and facing; its size stays its own.
66
+ *
67
+ * `bone === null` is the character's animated ROOT: the hips' travel (during
68
+ * playback the whole root trajectory lives in the bones — see
69
+ * ardy/playback.js) carried at the character's own yaw, so a prop dropped on
70
+ * the character walks with the body instead of rolling with the hips.
71
+ */
72
+ export function attachFrameMatrix(rig, bone, out = new THREE.Matrix4()) {
73
+ if (!rig) return null;
74
+ const name = ATTACH_BONE_NAMES[bone ?? "hips"];
75
+ if (!name) return null;
76
+ const node = attachBoneOf(rig, name);
77
+ if (!node) return null;
78
+ // Ancestors included: the bones are written imperatively by playback and by
79
+ // the export, either of which can land before the renderer's own pass.
80
+ node.updateWorldMatrix(true, false);
81
+ node.matrixWorld.decompose(attachPos, attachQuat, attachScale);
82
+ if (bone != null) return out.compose(attachPos, attachQuat, ATTACH_UNIT_SCALE);
83
+ // The Character group carries the scene placement and the clip-to-scene yaw;
84
+ // the rig model hangs off it, so its parent IS that group.
85
+ const group = rig.parent ?? rig;
86
+ group.updateWorldMatrix(true, false);
87
+ group.matrixWorld.decompose(attachRootPos, attachRootQuat, attachScale);
88
+ return out.compose(attachPos, attachRootQuat, ATTACH_UNIT_SCALE);
89
+ }
90
+
91
+ /** A prop's placement as a matrix — the same compose props.jsx renders from
92
+ * (position, XYZ euler in degrees, per-axis scale). */
93
+ export function sceneObjectMatrix(object, out = new THREE.Matrix4()) {
94
+ return out.compose(
95
+ placePos.set(object.x ?? 0, object.y ?? 0, object.z ?? 0),
96
+ placeQuat.setFromEuler(placeEuler.set(
97
+ (object.rotX ?? 0) * THREE.MathUtils.DEG2RAD,
98
+ (object.rot ?? 0) * THREE.MathUtils.DEG2RAD,
99
+ (object.rotZ ?? 0) * THREE.MathUtils.DEG2RAD,
100
+ )),
101
+ placeScale.set(object.scaleX ?? 1, object.scaleY ?? 1, object.scaleZ ?? 1),
102
+ );
103
+ }
104
+
105
+ /** …and back out into the nine authored channels, or null if the matrix could
106
+ * not be read as a placement at all (a singular frame inverts to zeros, and a
107
+ * zero matrix decomposes to NaN). Rounded, because a matrix round trip
108
+ * otherwise turns a typed 1.5 into 1.4999999999999998 in the Inspector the
109
+ * moment a prop is picked up and put down again. */
110
+ export function sceneObjectPlacement(matrix) {
111
+ matrix.decompose(placePos, placeQuat, placeScale);
112
+ placeEuler.setFromQuaternion(placeQuat);
113
+ const round = (value) => Math.round(value * 1e6) / 1e6;
114
+ const placement = {
115
+ x: round(placePos.x), y: round(placePos.y), z: round(placePos.z),
116
+ rotX: round(placeEuler.x * THREE.MathUtils.RAD2DEG),
117
+ rot: round(placeEuler.y * THREE.MathUtils.RAD2DEG),
118
+ rotZ: round(placeEuler.z * THREE.MathUtils.RAD2DEG),
119
+ scaleX: round(placeScale.x), scaleY: round(placeScale.y), scaleZ: round(placeScale.z),
120
+ };
121
+ return Object.values(placement).every((value) => Number.isFinite(value)) ? placement : null;
122
+ }
123
+
124
+ /**
125
+ * The nine channels rewritten so a prop does not MOVE when it changes frames.
126
+ *
127
+ * `world` is where the prop actually IS — read off the live group in the scene
128
+ * graph, NOT recomputed from its numbers. That distinction is the whole point:
129
+ * an attach->attach drop would otherwise have to reconstruct the world
130
+ * transform through the frame the prop is leaving, and any disagreement
131
+ * between that reconstruction and what the renderer last drew (a rig posed a
132
+ * beat later than the last drawn frame, a frame that no longer resolves) lands
133
+ * on screen as a jump. Reading the group makes every drop the same one-frame
134
+ * conversion the world->attach and attach->world cases already were.
135
+ *
136
+ * Returns null when the frame it needs is missing — converting against a rig
137
+ * that is not on stage would be precisely the jump this exists to prevent —
138
+ * and the caller then refuses the whole drop rather than re-labelling numbers
139
+ * it could not convert.
140
+ */
141
+ export function attachPlacementPatch(world, attach, frameOf) {
142
+ if (!world) return null;
143
+ if (!attach) return sceneObjectPlacement(attachLocalMatrix.copy(world));
144
+ const frame = frameOf(attach.characterId, attach.bone ?? null, attachToMatrix);
145
+ if (!frame) return null;
146
+ return sceneObjectPlacement(attachLocalMatrix.copy(world).premultiply(attachInverseMatrix.copy(frame).invert()));
147
+ }
148
+
149
+ /**
150
+ * Write a computed placement straight onto the record, around
151
+ * updateSceneObject. Deliberate, and the only call site allowed to: those
152
+ * limits are WORLD limits (y floors at the deck, x/z stay inside the room),
153
+ * and while a prop is attached its numbers are a LOCAL transform in a bone
154
+ * frame — a hand rides 1.3 m above the deck, so a carried prop's local y is
155
+ * routinely negative and clamping it to the floor IS the visual jump the
156
+ * conversion exists to prevent. Every value here comes out of a decomposed
157
+ * matrix, so the record shape is sound by construction.
158
+ */
159
+ export function placeSceneObject(objects, id, placement) {
160
+ if (!placement) return objects;
161
+ let changed = false;
162
+ const next = objects.map((object) => {
163
+ if (object.id !== id) return object;
164
+ if (Object.entries(placement).every(([key, value]) => object[key] === value)) return object;
165
+ changed = true;
166
+ return { ...object, ...placement };
167
+ });
168
+ return changed ? next : objects;
169
+ }