cozyclay 2.0.0 → 2.1.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 (260) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +17 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/bin/agent/agent-routes.mjs +219 -38
  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 +6 -5
  19. package/bin/cozyclay.mjs +12 -3
  20. package/bin/live/cli.mjs +2 -3
  21. package/bin/live/client.mjs +1 -1
  22. package/bin/live/studio.mjs +5 -2
  23. package/bin/mcp-runtime.mjs +12 -2
  24. package/bin/telemetry-state.mjs +3 -3
  25. package/dist/app/index.html +6 -6
  26. package/dist/assets/analytics-B41FmPYI.js +1 -0
  27. package/dist/assets/app-Do-Og5Pu.css +1 -0
  28. package/dist/assets/app-DwP-tl2x.js +4920 -0
  29. package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
  30. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
  31. package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
  32. package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
  33. package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
  34. package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
  35. package/dist/assets/workflow-Br-K7Srs.js +179 -0
  36. package/dist/cozyclay-package.json +1 -1
  37. package/dist/index.html +328 -222
  38. package/dist/sitemap.xml +5 -5
  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 +801 -259
  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 +196 -3
  72. package/src/ardy/timeline.jsx +117 -29
  73. package/src/ardy/to-cskel27.js +0 -2
  74. package/src/ardy/waypoints.js +1 -4
  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 +43 -24
  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 +16 -3
  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 +89 -32
  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/generation.js +1 -1
  127. package/src/motion-store.js +4 -4
  128. package/src/motion-trail.js +5 -11
  129. package/src/multimodel-ingest.js +1 -3
  130. package/src/object-catalog.jsx +7 -5
  131. package/src/object-gizmo.jsx +45 -31
  132. package/src/object-path.js +310 -67
  133. package/src/object-travel.js +224 -0
  134. package/src/otio.js +8 -3
  135. package/src/panels/CharacterTransformPanel.jsx +24 -6
  136. package/src/panels/ObjectTransformPanel.jsx +4 -3
  137. package/src/panels/PoseImagePanel.jsx +205 -0
  138. package/src/panels/PromptBlocksPanel.jsx +3 -1
  139. package/src/panels/ProxyFigurePanel.jsx +56 -0
  140. package/src/panels/ReferenceImageField.jsx +1 -1
  141. package/src/panels/RigControlPanel.jsx +24 -3
  142. package/src/panels/RigPanel.jsx +2 -1
  143. package/src/panels/SculptPanel.jsx +405 -0
  144. package/src/panels/SubjectBox.jsx +1 -1
  145. package/src/panels/SubjectsPanel.jsx +0 -2
  146. package/src/panels/VideoCapturePanel.jsx +2 -1
  147. package/src/panels/pose-image.css +42 -0
  148. package/src/panels/pose.css +3 -0
  149. package/src/panels/sculpt.css +162 -0
  150. package/src/plan-diagram.js +184 -0
  151. package/src/planview.jsx +127 -80
  152. package/src/playground.js +3 -5
  153. package/src/pose-fade.js +77 -0
  154. package/src/pose-gizmo.css +23 -0
  155. package/src/pose-gizmo.js +96 -0
  156. package/src/posestudio.jsx +134 -83
  157. package/src/project-browser.css +53 -0
  158. package/src/project-browser.jsx +52 -3
  159. package/src/project.js +55 -24
  160. package/src/props.jsx +177 -36
  161. package/src/proxy-figure.jsx +69 -0
  162. package/src/range-pin-panel.jsx +0 -1
  163. package/src/receipt-findings.js +20 -0
  164. package/src/render-passes.js +0 -1
  165. package/src/resource-status.jsx +1 -1
  166. package/src/root-path.js +48 -0
  167. package/src/route-owner.js +52 -0
  168. package/src/scene-asset-cache.js +1 -2
  169. package/src/scene-assets.js +12 -4
  170. package/src/scene-fbx.js +1 -1
  171. package/src/scene-mesh-cache.js +0 -1
  172. package/src/scene-objects.js +319 -19
  173. package/src/scenes.js +62 -17
  174. package/src/sculpt-blueprint.js +65 -0
  175. package/src/sculpt-compare.js +384 -0
  176. package/src/sculpt-geometry.js +141 -0
  177. package/src/sculpt-mode.js +168 -0
  178. package/src/sculpt-recipe.js +385 -0
  179. package/src/sculpt-run.js +351 -0
  180. package/src/sculpt-stage.jsx +221 -0
  181. package/src/settings-menu.jsx +1 -1
  182. package/src/shell/BoardSlot.jsx +341 -0
  183. package/src/shell/BottomDock.jsx +58 -14
  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/TopBar.jsx +17 -3
  188. package/src/shell/ViewportToolbar.jsx +18 -3
  189. package/src/shell/dock.css +239 -0
  190. package/src/shell/glass-regions.css +3 -2
  191. package/src/shell/log-store.js +4 -4
  192. package/src/shell/mode.css +20 -0
  193. package/src/shell/studio-shell-context.js +4 -1
  194. package/src/shot-authoring.js +69 -6
  195. package/src/stable-items.js +1 -1
  196. package/src/stage-attach-frames.js +169 -0
  197. package/src/stage-canvas-rigs.jsx +402 -0
  198. package/src/stage-capture.jsx +216 -0
  199. package/src/stage-characters.jsx +302 -0
  200. package/src/stage-editor-gizmos.jsx +1229 -0
  201. package/src/stage-labels-ko.js +204 -0
  202. package/src/stage-startup-defaults.js +182 -0
  203. package/src/starter-scenes.js +1 -1
  204. package/src/storyboard.js +24 -5
  205. package/src/studio-actions.js +42 -9
  206. package/src/studio-agent-commands.js +186 -23
  207. package/src/studio-agent-context.js +11 -9
  208. package/src/studio-agent-motion.js +2 -1
  209. package/src/studio-agent-protocol.js +56 -23
  210. package/src/studio-app-binding.js +52 -23
  211. package/src/studio-contact-sheet.js +3 -3
  212. package/src/studio-elements.js +11 -2
  213. package/src/studio-geometry-facts.js +114 -0
  214. package/src/styles.css +111 -10
  215. package/src/theme.js +1 -1
  216. package/src/timeline-fps.js +13 -0
  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 +110 -20
  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/kimodo/effector-constraints.mjs +4 -4
  248. package/tools/kimodo/generate.mjs +1 -1
  249. package/tools/knip-baseline.json +331 -0
  250. package/tools/loc-baseline.json +126 -0
  251. package/tools/process-supervisor.mjs +1 -1
  252. package/tools/projflow/replay.mjs +4 -4
  253. package/tools/projflow/runner.mjs +3 -3
  254. package/tools/qa/studio-control-count.mjs +2 -0
  255. package/tools/run-tests.mjs +96 -7
  256. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  257. package/dist/assets/app-C1sdVbg4.css +0 -1
  258. package/dist/assets/app-CA55tcI3.js +0 -4876
  259. package/dist/assets/shot-prompt-Yrt99wZx.js +0 -17
  260. package/dist/assets/workflow-dNPGY9xz.js +0 -179
@@ -43,10 +43,10 @@ const OVERLAY_ALPHA = 0.62;
43
43
  const STAGE_GROUND = "#141819";
44
44
  /** Interaction runs on a preview this wide at most — the paint underneath it
45
45
  * stays at the picture's own resolution. */
46
- export const EDIT_PREVIEW_MAX = 512;
46
+ const EDIT_PREVIEW_MAX = 512;
47
47
  /** Zoom range. 1 is the whole picture — there is no reason to look at less of
48
48
  * it than that — and 16 puts a preview pixel under a fingertip. */
49
- export const MIN_ZOOM = 1;
49
+ const MIN_ZOOM = 1;
50
50
  export const MAX_ZOOM = 16;
51
51
  /** How many edits back you can walk. Each step is a full-resolution copy of
52
52
  * the selection, so this is a memory budget as much as a usability one: seven
@@ -251,8 +251,6 @@ export function createMatteEditor(canvas, {
251
251
  report();
252
252
  }
253
253
 
254
- /* --------------------------------------------------------- pointer --- */
255
-
256
254
  /** Where a pointer is, in FULL-resolution picture pixels: stage → picture,
257
255
  * through the same placement the paint is drawn with. */
258
256
  function at(event) {
@@ -370,8 +368,6 @@ export function createMatteEditor(canvas, {
370
368
  growFromStroke();
371
369
  if (canvas.hasPointerCapture?.(event.pointerId)) canvas.releasePointerCapture(event.pointerId);
372
370
  };
373
- /* ------------------------------------------------------------ view --- */
374
-
375
371
  /** Zoom about a point on screen, so what is under the cursor stays under
376
372
  * the cursor — the only zoom that lets you keep working on the thing you
377
373
  * were looking at. */
@@ -416,8 +412,6 @@ export function createMatteEditor(canvas, {
416
412
  canvas.addEventListener("keydown", onKeyDown);
417
413
  canvas.addEventListener("keyup", onKeyUp);
418
414
 
419
- /* ------------------------------------------------------------- api --- */
420
-
421
415
  return {
422
416
  /**
423
417
  * Decode an asset and show it AS IT IS. Nothing is marked until someone
package/src/matte.js CHANGED
@@ -109,15 +109,6 @@ export function borderSeeds({ data, width, height }, points = null) {
109
109
  return { seeds: Float64Array.from(seeds), starts };
110
110
  }
111
111
 
112
- /**
113
- * Drop the background, in place on a copy.
114
- *
115
- * `tolerance` (0–1) is the one control a person should ever need: it scales
116
- * both the bound against the seed colours and the local step. `shrink` eats
117
- * the fringe of background-coloured pixels that survives any threshold, and
118
- * `feather` softens what is left so the card's edge does not read as a
119
- * cut-out-with-scissors line against the set.
120
- */
121
112
  /**
122
113
  * `within`, when given, fences the growth: a pixel can only be entered if it is
123
114
  * marked there. That is what lets the eraser be the same tool as the brush —
@@ -177,8 +168,6 @@ export function backgroundMask(pixels, { points = null, tolerance = 0.18, within
177
168
  return background;
178
169
  }
179
170
 
180
- /* -------------------------------------------------------------- brush ---- */
181
-
182
171
  /**
183
172
  * A round stroke into the paint layer: `value` 1 means "this is background
184
173
  * after all", -1 means "put that back". The paint layer is kept SEPARATE from
@@ -235,8 +224,6 @@ export function combineMask(auto, paint, { width, height, paintWidth = width, pa
235
224
  return out;
236
225
  }
237
226
 
238
- /* -------------------------------------------------------------- apply ---- */
239
-
240
227
  /**
241
228
  * Turn a mask into transparency: erode the fringe, soften the boundary, write
242
229
  * the alpha. Split from the growth above so the editor can show a mask, let
@@ -257,7 +244,6 @@ function dropEdgeLines(data, width, height) {
257
244
  let changed = 0;
258
245
  for (let i = 0; i < total; i += 1) {
259
246
  if (data[i * 4 + 3] < 8 || visited[i]) continue;
260
- // flood this kept component
261
247
  stack.length = 0;
262
248
  stack.push(i);
263
249
  visited[i] = 1;
@@ -336,7 +322,6 @@ export function applyMask(pixels, mask, { shrink = 1, feather = 1 } = {}) {
336
322
  grown = next;
337
323
  }
338
324
 
339
- /* -- alpha, then a soft edge that only touches the boundary ------------- */
340
325
  const alpha = new Float32Array(total);
341
326
  let removed = 0;
342
327
  for (let pixel = 0; pixel < total; pixel++) {
@@ -387,8 +372,6 @@ export function removeBackground(pixels, { points = null, tolerance = 0.18, shri
387
372
  return applyMask(pixels, grown, { shrink, feather });
388
373
  }
389
374
 
390
- /* --------------------------------------------------------------- trim ---- */
391
-
392
375
  /** The box the picture still occupies, or null when nothing survived. */
393
376
  export function opaqueBounds({ data, width, height }, threshold = 8) {
394
377
  let minX = width;
@@ -422,8 +405,6 @@ export function cropPixels({ data, width, height }, box) {
422
405
  return { data: out, width: box.width, height: box.height };
423
406
  }
424
407
 
425
- /* ------------------------------------------------------------- browser ---- */
426
-
427
408
  function defaultCanvas(width, height) {
428
409
  if (typeof OffscreenCanvas === "function") return new OffscreenCanvas(width, height);
429
410
  throw new Error("this browser cannot edit images — OffscreenCanvas is unavailable");
@@ -5,6 +5,10 @@
5
5
  * a Mixamo-as-statue (or any skinned GLB) can bind to the wrong bones. Graphs
6
6
  * that actually contain a SkinnedMesh go through SkeletonUtils; everything
7
7
  * else keeps the cheap deep clone.
8
+ *
9
+ * Both clones share materials with the cache, so each instance gets its own
10
+ * copies (marked `instanceOwned`, disposed with the instance): a per-object
11
+ * write such as `object.opacity` must not leak into other copies.
8
12
  */
9
13
  import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
10
14
 
@@ -18,6 +22,16 @@ export function graphHasSkinnedMesh(root) {
18
22
 
19
23
  export function cloneMeshGraph(source) {
20
24
  if (!source) return source;
21
- if (graphHasSkinnedMesh(source)) return cloneSkeleton(source);
22
- return source.clone(true);
25
+ const root = graphHasSkinnedMesh(source) ? cloneSkeleton(source) : source.clone(true);
26
+ const copies = new Map();
27
+ const own = (material) => {
28
+ if (!copies.has(material)) copies.set(material, material.clone());
29
+ return copies.get(material);
30
+ };
31
+ root.traverse((node) => {
32
+ if (!node.isMesh || !node.material) return;
33
+ node.material = Array.isArray(node.material) ? node.material.map(own) : own(node.material);
34
+ node.userData.instanceOwned = true;
35
+ });
36
+ return root;
23
37
  }
package/src/mesh-sniff.js CHANGED
@@ -61,7 +61,6 @@ export function looksLikeObj(bytes) {
61
61
  const line = raw.trim();
62
62
  if (!/^v\s+/.test(line)) continue;
63
63
  const parts = line.split(/\s+/);
64
- if (parts[0] !== "v") continue;
65
64
  if ([parts[1], parts[2], parts[3]].every((part) => Number.isFinite(Number(part)))) return true;
66
65
  }
67
66
  return false;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A monotone piecewise cubic through authored knots (harmonic-mean tangents,
3
+ * Fritsch–Carlson family): it hits every knot exactly, never overshoots between
4
+ * two of them, runs flat across a local extremum and degrades to the exact
5
+ * straight lerp for two knots. The camera crane's height profile and a route's
6
+ * lean (bank / pitch) marks share it, so a dot means the same thing on both rails.
7
+ *
8
+ * @param {object[]} knots ascending by `t`; at least one
9
+ * @param {number} x position along the profile (clamped to the knots' span)
10
+ * @param {(knot: object) => number} read the knot's value; defaults to `knot.v`
11
+ */
12
+ export function monotoneCubicAt(knots, x, read = (knot) => knot.v) {
13
+ const last = knots.length - 1;
14
+ const clamped = Math.max(knots[0].t, Math.min(knots[last].t, x));
15
+ if (clamped <= knots[0].t) return read(knots[0]);
16
+ if (clamped >= knots[last].t) return read(knots[last]);
17
+ let i = 0;
18
+ while (i < knots.length - 2 && clamped > knots[i + 1].t) i += 1;
19
+ const p0 = knots[i];
20
+ const p1 = knots[i + 1];
21
+ const h = p1.t - p0.t;
22
+ if (h < 1e-9) return read(p1);
23
+ const v0 = read(p0);
24
+ const v1 = read(p1);
25
+ const secant = (a, b) => (read(b) - read(a)) / Math.max(b.t - a.t, 1e-9);
26
+ const d = secant(p0, p1);
27
+ // harmonic mean of neighbouring secants; zero across a local extremum
28
+ const mono = (sa, sb) => (sa * sb <= 0 ? 0 : (2 * sa * sb) / (sa + sb));
29
+ const m0 = i > 0 ? mono(secant(knots[i - 1], p0), d) : d;
30
+ const m1 = i < knots.length - 2 ? mono(d, secant(p1, knots[i + 2])) : d;
31
+ const u = (clamped - p0.t) / h;
32
+ const u2 = u * u;
33
+ const u3 = u2 * u;
34
+ return (
35
+ (2 * u3 - 3 * u2 + 1) * v0 +
36
+ (u3 - 2 * u2 + u) * h * m0 +
37
+ (-2 * u3 + 3 * u2) * v1 +
38
+ (u3 - u2) * h * m1
39
+ );
40
+ }
@@ -1,7 +1,7 @@
1
1
  // Pure request construction. The caller supplies one rolled seed and sampled
2
2
  // poses; no rig, store, random source, notification or network is touched here.
3
3
  import { StudioProtocolError, compileStudioBeats } from '../studio-agent-protocol.js';
4
- import { TIMELINE_FRAME_FPS as FPS } from '../scenes.js';
4
+ import { TIMELINE_FRAME_FPS as FPS } from '../timeline-fps.js';
5
5
  import { judgeAuthoredPath, alignArdyPath } from '../ardy/waypoints.js';
6
6
  import { planPosePin, PIN_BLOCKED } from '../ardy/pose-pin.js';
7
7
  import { blocksFromRequest, replayPayload, replayTruncated } from '../take-recipe.js';
@@ -1,10 +1,10 @@
1
- export const MOTION_DB_NAME = "cozyclay.motions";
2
- export const MOTION_DB_VERSION = 1;
3
- export const MOTION_STORE_NAME = "motions";
1
+ const MOTION_DB_NAME = "cozyclay.motions";
2
+ const MOTION_DB_VERSION = 1;
3
+ const MOTION_STORE_NAME = "motions";
4
4
 
5
5
  const validId = (id) => typeof id === "string" && /^[0-9a-f]{64}$/i.test(id);
6
6
 
7
- export function normalizeMotion(record) {
7
+ function normalizeMotion(record) {
8
8
  if (!record || typeof record !== "object" || !validId(record.motionId)) return null;
9
9
  if (record.encoding !== "base64" || typeof record.data !== "string") return null;
10
10
  return { ...record, motionId: record.motionId.toLowerCase(), stored: true };
@@ -28,10 +28,11 @@ const LIMB_CHAINS = {
28
28
  };
29
29
 
30
30
  /**
31
- * The always-drawn trail tracks: root plus every IK chain endpoint and the
32
- * head, coloured exactly like their viewport IK/FK handles (posestudio.jsx
33
- * coding: arms orange, legs blue, torso yellow, head purple). Pick order is
34
- * limbs-first so an overlapping grab prefers the finer target; hips last.
31
+ * The available trail tracks: root plus every IK chain endpoint and the head,
32
+ * coloured exactly like their viewport IK/FK handles (posestudio.jsx coding:
33
+ * arms orange, legs blue, torso yellow, head purple). The viewport can focus
34
+ * one track after selection; pick order is limbs-first so an overlapping grab
35
+ * prefers the finer target; hips last.
35
36
  */
36
37
  export const TRAIL_TRACKS = [
37
38
  { id: "leftHand", joint: "LeftHand", color: "#ff8a3d" },
@@ -295,13 +296,6 @@ const matVec = (m, v) => [
295
296
  m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
296
297
  m[6] * v[0] + m[7] * v[1] + m[8] * v[2],
297
298
  ];
298
- /** Inverse of a row-major 3x3 (callers keep it well conditioned). */
299
- function inverse3(m) {
300
- const [a, b, c, d, e, f, g, h, i] = m;
301
- const A = e * i - f * h, B = f * g - d * i, C = d * h - e * g;
302
- const det = a * A + b * B + c * C;
303
- return [A, c * h - b * i, b * f - c * e, B, a * i - c * g, c * d - a * f, C, b * g - a * h, a * e - b * d].map((v) => v / det);
304
- }
305
299
  /** Shortest-arc rotation taking unit `a` onto unit `b` (Rodrigues). */
306
300
  function arcRotation(a, b) {
307
301
  const c = dot(a, b);
@@ -8,8 +8,6 @@
8
8
  // and a silent fallback to a plausible-looking default is how footage that was
9
9
  // never read ends up looking ingested.
10
10
 
11
- export const INGEST_STAGES = Object.freeze(["idle", "fetching", "probing", "ready", "error"]);
12
-
13
11
  /** A successful extraction is not proof that vertical recovery was applied. */
14
12
  export function trajectoryReceipt(report, korean = false) {
15
13
  if (!report) return "";
@@ -242,7 +240,7 @@ export function fpsFromFrameTimes(times, fallback = 30) {
242
240
  return { fps: Math.round(1 / median), measured: true };
243
241
  }
244
242
 
245
- export function formatBytes(bytes) {
243
+ function formatBytes(bytes) {
246
244
  if (!Number.isFinite(bytes) || bytes < 0) return "";
247
245
  if (bytes < 1024) return `${bytes} B`;
248
246
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
  import { ko, isKo } from "./locale.js";
3
- import { OBJECT_LIBRARY } from "./scene-objects.js";
3
+ import { EMPTY_KIND, OBJECT_MENU_ENTRIES } from "./scene-objects.js";
4
4
 
5
5
  const GROUP_LABELS_KO = {
6
6
  Primitives: "기본 도형",
@@ -8,6 +8,7 @@ const GROUP_LABELS_KO = {
8
8
  };
9
9
 
10
10
  const OBJECT_LABELS_KO = {
11
+ Empty: "빈 오브젝트",
11
12
  Cube: "큐브",
12
13
  Sphere: "구",
13
14
  Capsule: "캡슐",
@@ -19,7 +20,7 @@ const OBJECT_LABELS_KO = {
19
20
  "Plane (aircraft)": "비행기",
20
21
  };
21
22
 
22
- export function displayObjectGroupName(name) {
23
+ function displayObjectGroupName(name) {
23
24
  return isKo ? (GROUP_LABELS_KO[name] ?? name) : name;
24
25
  }
25
26
 
@@ -39,9 +40,9 @@ export function displayObjectLabel(label) {
39
40
  /** The catalogue grouped by section. Shared with the hierarchy's right-click
40
41
  * create menu so both entry points list the same objects
41
42
  * (docs/unity-reference.md §9.7). */
42
- export function buildObjectMenuGroups() {
43
+ function buildObjectMenuGroups() {
43
44
  const groups = [];
44
- for (const entry of OBJECT_LIBRARY) {
45
+ for (const entry of OBJECT_MENU_ENTRIES) {
45
46
  const group = groups.find((item) => item.name === entry.group);
46
47
  if (group) group.entries.push(entry);
47
48
  else groups.push({ name: entry.group, entries: [entry] });
@@ -65,7 +66,8 @@ export function CatalogueEntries({ onPick }) {
65
66
  >
66
67
  <span className={`add-object-swatch ${entry.kind}`} style={{ background: entry.color }} aria-hidden="true" />
67
68
  <span>{displayObjectLabel(entry.label)}</span>
68
- <small>{entry.footprint.width} × {entry.footprint.depth} m</small>
69
+ {/* An empty has no size to quote: say what it is instead of "0 × 0 m". */}
70
+ <small>{entry.kind === EMPTY_KIND ? ko("node only", "노드 전용") : `${entry.footprint.width} × ${entry.footprint.depth} m`}</small>
69
71
  </button>
70
72
  ))}
71
73
  </div>
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { useFrame, useThree } from "@react-three/fiber";
3
3
  import * as THREE from "three";
4
4
  import { GIZMO_LAYER, SHOT_ASPECT, fitAspect } from "./dualview.jsx";
@@ -228,21 +228,6 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
228
228
  rootRef.current?.traverse((node) => node.layers?.set(GIZMO_LAYER));
229
229
  });
230
230
 
231
- /** pointer -> NDC inside the rect the shot camera actually rendered into */
232
- const pointerRay = (event) => {
233
- const pane = paneRef.current;
234
- const camera = camRef.current;
235
- if (!pane || !camera) return null;
236
- const bounds = pane.getBoundingClientRect();
237
- if (bounds.width < 2 || bounds.height < 2) return null;
238
- const rect = imageRect(bounds);
239
- tools.ndc.set(
240
- ((event.clientX - rect.x) / rect.w) * 2 - 1,
241
- -((event.clientY - rect.y) / rect.h) * 2 + 1,
242
- );
243
- return Math.abs(tools.ndc.x) <= 1 && Math.abs(tools.ndc.y) <= 1 ? tools.ndc : null;
244
- };
245
-
246
231
  /** the scene object under the pointer, if any (meshes carry the id on an
247
232
  * ancestor group, so the hit walks up to find it) */
248
233
  const pickObject = () => {
@@ -256,9 +241,18 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
256
241
  // whichever surface is nearer.
257
242
  tools.raycaster.layers.set(GIZMO_LAYER);
258
243
  let ghostId = null;
244
+ let ghostIsEmpty = false;
259
245
  const ghostHit = tools.raycaster.intersectObjects(scene.children, true).find((entry) => {
260
246
  if (!entry.object.isMesh) return false;
261
247
  for (let node = entry.object; node; node = node.parent) {
248
+ // An Empty's marker: its pick volume is a gizmo-layer mesh under
249
+ // the object's own group, so the owning record is the first
250
+ // ancestor carrying a sceneObjectId.
251
+ if (entry.object.userData?.emptyPick && node.userData?.sceneObjectId) {
252
+ ghostId = node.userData.sceneObjectId;
253
+ ghostIsEmpty = true;
254
+ return true;
255
+ }
262
256
  if (node.userData?.shotCameraPick) {
263
257
  ghostId = "__shotcam__";
264
258
  return true;
@@ -271,7 +265,9 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
271
265
  return false;
272
266
  });
273
267
  tools.raycaster.layers.set(0);
274
- if (ghostHit && (!hit || ghostHit.distance < hit.distance)) return { id: ghostId, point: ghostHit.point.clone() };
268
+ // The Empty's marker is drawn over the set (no depth test), so what you
269
+ // see is what you click: it wins over whatever surface is behind it.
270
+ if (ghostHit && (ghostIsEmpty || !hit || ghostHit.distance < hit.distance)) return { id: ghostId, point: ghostHit.point.clone() };
275
271
  if (!hit) return null;
276
272
  for (let node = hit.object; node; node = node.parent) {
277
273
  if (node.userData?.sceneObjectId) return { id: node.userData.sceneObjectId, point: hit.point.clone() };
@@ -478,9 +474,15 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
478
474
  * null for the uniform-scale knob and for plane handles, which carry
479
475
  * their own `plane` axes instead. */
480
476
  const beginDrag = (kind, axis, dir, camera) => {
481
- const live = stateRef.current.object;
482
- if (!live) return false;
483
- tools.origin.set(live.x, gizmoHeight(live), live.z);
477
+ const shown = stateRef.current.object;
478
+ if (!shown) return false;
479
+ // The gizmo can be SHOWN on the pose a routed record is drawn at, while
480
+ // every channel a drag writes starts from the AUTHORED record: a start
481
+ // taken from the drawn numbers would land them in the authored ones
482
+ // (a jump of however far the car has driven). The pivot, and the yaw
483
+ // the card's corners are seen at, are the shown ones.
484
+ const live = stateRef.current.objects.find((entry) => entry.id === shown.id) ?? shown;
485
+ tools.origin.set(shown.x, gizmoHeight(shown), shown.z);
484
486
  camera.getWorldDirection(tools.eye);
485
487
  let plane;
486
488
  let drag;
@@ -525,7 +527,7 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
525
527
  // The card's own plane, so the grab tracks the picture rather than a
526
528
  // world axis: a standee that has been turned is still resized by the
527
529
  // corner the eye sees.
528
- const yaw = ((live.rot ?? 0) * Math.PI) / 180;
530
+ const yaw = ((shown.rot ?? 0) * Math.PI) / 180;
529
531
  const right = new THREE.Vector3(Math.cos(yaw), 0, -Math.sin(yaw));
530
532
  const up = new THREE.Vector3(0, 1, 0);
531
533
  const normal = new THREE.Vector3().crossVectors(right, up).normalize();
@@ -631,17 +633,22 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
631
633
  // side-effect free: it only answers "would you grab here?".
632
634
  if (stateRef.current.claimPointer?.(event)) return;
633
635
  const grabbed = pickHandle(event);
634
- // A press ON the key-light sun outranks any handle overlapping it:
635
- // the puck's own body-drag is the primary interaction there, and the
636
- // centre plane-square would otherwise silently claim the grab.
636
+ // A press ON the key-light sun, or on a route or crane dot, outranks
637
+ // any handle overlapping it: the puck's own body-drag and the dot's
638
+ // own grab are the primary interaction there, and the centre
639
+ // plane-square would otherwise silently claim the grab. (A routed
640
+ // object drawn at frame 0 sits exactly on its route's start dot.)
637
641
  if (grabbed && rayFrom(event)) {
638
642
  tools.raycaster.layers.set(GIZMO_LAYER);
639
- const sunClaims = tools.raycaster.intersectObjects(scene.children, true).some((entry) => {
640
- for (let node = entry.object; node; node = node.parent) if (node.userData?.keyLightPick) return true;
643
+ const dotClaims = tools.raycaster.intersectObjects(scene.children, true).some((entry) => {
644
+ for (let node = entry.object; node; node = node.parent) {
645
+ const data = node.userData;
646
+ if (data?.keyLightPick || data?.pathIndex !== undefined || data?.pathRing || data?.craneIndex !== undefined) return true;
647
+ }
641
648
  return false;
642
649
  });
643
650
  tools.raycaster.layers.set(0);
644
- if (sunClaims) return;
651
+ if (dotClaims) return;
645
652
  }
646
653
  if (
647
654
  grabbed &&
@@ -769,9 +776,9 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
769
776
 
770
777
  // QA hook: where each handle sits on screen, through the exact rect the
771
778
  // picker builds its ray from. Headless checks drive real pointer events
772
- // with it (harmless in normal use).
773
- if (typeof window !== "undefined") {
774
- if (!pickOnly) window.__gizmoHandles = () => {
779
+ // with it (harmless in normal use). The character gizmo is a separate
780
+ // pick-only instance, so it needs its own mirror hook.
781
+ const getHandles = useCallback(() => {
775
782
  const camera = camRef?.current;
776
783
  const pane = paneRef?.current;
777
784
  if (!camera || !pane) return [];
@@ -799,8 +806,15 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
799
806
  y: rect.y + ((1 - point.y) / 2) * rect.h,
800
807
  };
801
808
  });
809
+ }, [camRef, paneRef, shotAspect]);
810
+ if (typeof window !== "undefined" && !pickOnly) window.__gizmoHandles = getHandles;
811
+ useEffect(() => {
812
+ if (typeof window === "undefined" || !pickOnly) return undefined;
813
+ window.__characterGizmoHandles = getHandles;
814
+ return () => {
815
+ if (window.__characterGizmoHandles === getHandles) delete window.__characterGizmoHandles;
802
816
  };
803
- }
817
+ }, [getHandles, pickOnly]);
804
818
 
805
819
  if (!enabled || !object) {
806
820
  handlesRef.current.clear();