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
@@ -0,0 +1,216 @@
1
+ import * as THREE from "three";
2
+
3
+ /**
4
+ * Depth ranks for coplanar faces of scene-object primitives.
5
+ *
6
+ * Agent-built assemblies stack parts whose same-facing faces lie in exactly
7
+ * one plane (a hood and a grille sharing a top at y=1.05). The depth test
8
+ * cannot order them, so as the camera moves the winner flips every frame and
9
+ * the strip sparkles. The ranks computed here let the renderer pull the
10
+ * later-created part toward the camera by a polygon offset, which holds
11
+ * whatever the camera does. The records are never edited.
12
+ */
13
+
14
+ const DEG = Math.PI / 180;
15
+ /** Planes closer than this are one plane. */
16
+ const PLANE_EPS = 1e-3;
17
+ /** Normals this aligned (cosine) face the same way. */
18
+ const NORMAL_DOT = 0.9999;
19
+ /** Overlap shallower than this is two faces touching along an edge. */
20
+ const OVERLAP_EPS = 1e-4;
21
+ /** Bounds the offset: a deeper chain shares the top rank. */
22
+ export const MAX_DEPTH_RANK = 6;
23
+
24
+ // Planar faces per kind, in the primitive's local frame BEFORE scale: the axis
25
+ // the face is perpendicular to, which side, and where along that axis it sits
26
+ // (cube/cylinder/cone sit on y in [0, 1] — see Primitive in props.jsx).
27
+ const BOX_FACES = [
28
+ { axis: 0, side: 1, at: 0.5 }, { axis: 0, side: -1, at: -0.5 },
29
+ { axis: 1, side: 1, at: 1 }, { axis: 1, side: -1, at: 0 },
30
+ { axis: 2, side: 1, at: 0.5 }, { axis: 2, side: -1, at: -0.5 },
31
+ ];
32
+ const CAP_TOP = { axis: 1, side: 1, at: 1 };
33
+ const CAP_BASE = { axis: 1, side: -1, at: 0 };
34
+ const FACES_BY_KIND = {
35
+ cube: BOX_FACES,
36
+ cylinder: [CAP_TOP, CAP_BASE],
37
+ cone: [CAP_BASE],
38
+ // 2x2, a hair above the floor, drawn double-sided
39
+ plane: [{ axis: 1, side: 1, at: 0.004, flat: true }, { axis: 1, side: -1, at: 0.004, flat: true }],
40
+ };
41
+ // Local centre of the box the faces live on, per axis (x/z centred, y from 0).
42
+ const MID = [0, 0.5, 0];
43
+ const HALF = [0.5, 0.5, 0.5];
44
+ const PLANE_HALF = [1, 0, 1];
45
+
46
+ const rot = new THREE.Matrix4();
47
+ const euler = new THREE.Euler();
48
+
49
+ /** World-space rectangular faces of one object, or null when it has none. */
50
+ function facesOf(object) {
51
+ const kind = object.renderer ?? object.kind;
52
+ const table = FACES_BY_KIND[kind];
53
+ if (!table || object.attach || object.hidden === true) return null;
54
+ euler.set((object.rotX ?? 0) * DEG, (object.rot ?? 0) * DEG, (object.rotZ ?? 0) * DEG);
55
+ rot.makeRotationFromEuler(euler);
56
+ const e = rot.elements;
57
+ // column a of the rotation = where local axis a points in the world
58
+ const axes = [[e[0], e[1], e[2]], [e[4], e[5], e[6]], [e[8], e[9], e[10]]];
59
+ const scale = [object.scaleX ?? 1, object.scaleY ?? 1, object.scaleZ ?? 1];
60
+ const pos = [object.x ?? 0, object.y ?? 0, object.z ?? 0];
61
+ if (![...scale, ...pos, ...axes[0]].every(Number.isFinite)) return null;
62
+ const faces = [];
63
+ for (const face of table) {
64
+ const a = face.axis;
65
+ const u = (a + 1) % 3;
66
+ const v = (a + 2) % 3;
67
+ // a mirrored scale turns the outward normal around with it
68
+ const flip = face.side * (scale[a] < 0 ? -1 : 1);
69
+ const half = face.flat ? PLANE_HALF : HALF;
70
+ const centre = [0, 0, 0];
71
+ for (let i = 0; i < 3; i += 1) {
72
+ const local = i === a ? face.at : face.flat ? 0 : MID[i];
73
+ for (let k = 0; k < 3; k += 1) centre[k] += axes[i][k] * local * scale[i];
74
+ }
75
+ faces.push({
76
+ n: [axes[a][0] * flip, axes[a][1] * flip, axes[a][2] * flip],
77
+ c: [centre[0] + pos[0], centre[1] + pos[1], centre[2] + pos[2]],
78
+ u: axes[u],
79
+ v: axes[v],
80
+ hu: half[u] * Math.abs(scale[u]),
81
+ hv: half[v] * Math.abs(scale[v]),
82
+ });
83
+ }
84
+ // world box of the faces, for the pair prefilter
85
+ const min = [Infinity, Infinity, Infinity];
86
+ const max = [-Infinity, -Infinity, -Infinity];
87
+ for (const f of faces) {
88
+ for (let k = 0; k < 3; k += 1) {
89
+ const reach = Math.abs(f.u[k]) * f.hu + Math.abs(f.v[k]) * f.hv;
90
+ min[k] = Math.min(min[k], f.c[k] - reach);
91
+ max[k] = Math.max(max[k], f.c[k] + reach);
92
+ }
93
+ }
94
+ return { faces, min, max };
95
+ }
96
+
97
+ const dot = (p, q) => p[0] * q[0] + p[1] * q[1] + p[2] * q[2];
98
+
99
+ // Overlap of two rectangles lying in one plane, by separating axes in the
100
+ // plane: the rectangles' own edge directions are the only candidates.
101
+ function overlaps(a, b) {
102
+ const rel = [b.c[0] - a.c[0], b.c[1] - a.c[1], b.c[2] - a.c[2]];
103
+ for (const axis of [a.u, a.v, b.u, b.v]) {
104
+ const reachA = Math.abs(dot(axis, a.u)) * a.hu + Math.abs(dot(axis, a.v)) * a.hv;
105
+ const reachB = Math.abs(dot(axis, b.u)) * b.hu + Math.abs(dot(axis, b.v)) * b.hv;
106
+ if (Math.abs(dot(axis, rel)) >= reachA + reachB - OVERLAP_EPS) return false;
107
+ }
108
+ return true;
109
+ }
110
+
111
+ /** Every pair of same-facing faces of a and b that share a plane and overlap. */
112
+ function conflictingFaces(a, b) {
113
+ const found = [];
114
+ for (const fa of a.faces) {
115
+ for (const fb of b.faces) {
116
+ if (dot(fa.n, fb.n) <= NORMAL_DOT) continue;
117
+ const gap = dot(fa.n, [fb.c[0] - fa.c[0], fb.c[1] - fa.c[1], fb.c[2] - fa.c[2]]);
118
+ if (Math.abs(gap) > PLANE_EPS) continue;
119
+ if (overlaps(fa, fb)) found.push(fa);
120
+ }
121
+ }
122
+ return found;
123
+ }
124
+
125
+ function conflict(a, b) {
126
+ for (const fa of a.faces) {
127
+ for (const fb of b.faces) {
128
+ if (dot(fa.n, fb.n) <= NORMAL_DOT) continue;
129
+ const gap = dot(fa.n, [fb.c[0] - fa.c[0], fb.c[1] - fa.c[1], fb.c[2] - fa.c[2]]);
130
+ if (Math.abs(gap) > PLANE_EPS) continue;
131
+ if (overlaps(fa, fb)) return true;
132
+ }
133
+ }
134
+ return false;
135
+ }
136
+
137
+ /**
138
+ * Which objects need to win the depth test over an earlier one.
139
+ *
140
+ * Two planar faces conflict when they face the same way, share a plane to
141
+ * within a millimetre and overlap with real area; touching boxes (opposite
142
+ * facing) never do. The later object in `objects` is the later-made detail, so
143
+ * each conflict is an edge earlier -> later, and an object's rank is the length
144
+ * of the longest chain ending at it (capped). Only ranked objects (> 0) get an
145
+ * entry. Run it on the AUTHORED records: a routed group moves rigidly, so
146
+ * relative geometry inside it does not change with the frame.
147
+ *
148
+ * Skipped: spheres, capsules, models, cutouts, props carried by a character
149
+ * and hidden props. Cylinder and cone caps are approximated by their bounding
150
+ * rectangle, which can over-report a conflict near a rim; that costs a
151
+ * sub-pixel pull on a prop and nothing else.
152
+ *
153
+ * @param {Array<object>} objects scene-object records, in creation order
154
+ * @returns {Map<string, number>} id -> rank
155
+ */
156
+ export function coplanarDepthRanks(objects) {
157
+ const ranks = new Map();
158
+ if (!Array.isArray(objects) || objects.length < 2) return ranks;
159
+ const shapes = objects.map((object) => (object && typeof object === "object" ? facesOf(object) : null));
160
+ const rank = new Array(objects.length).fill(0);
161
+ for (let j = 1; j < objects.length; j += 1) {
162
+ const b = shapes[j];
163
+ if (!b) continue;
164
+ for (let i = 0; i < j; i += 1) {
165
+ const a = shapes[i];
166
+ if (!a || rank[i] + 1 <= rank[j]) continue;
167
+ if (
168
+ a.min[0] > b.max[0] + PLANE_EPS || b.min[0] > a.max[0] + PLANE_EPS ||
169
+ a.min[1] > b.max[1] + PLANE_EPS || b.min[1] > a.max[1] + PLANE_EPS ||
170
+ a.min[2] > b.max[2] + PLANE_EPS || b.min[2] > a.max[2] + PLANE_EPS
171
+ ) continue;
172
+ if (conflict(a, b)) rank[j] = Math.min(MAX_DEPTH_RANK, rank[i] + 1);
173
+ }
174
+ if (rank[j] > 0) ranks.set(objects[j].id, rank[j]);
175
+ }
176
+ return ranks;
177
+ }
178
+
179
+ const AXES = ["x", "y", "z"];
180
+ const FACE_NAMES = { "y+": "tops", "y-": "bottoms" };
181
+ const round = (value) => Number(value.toFixed(4));
182
+
183
+ /**
184
+ * The coplanar conflicts coplanarDepthRanks resolves by offset, named: for
185
+ * each pair of objects with same-facing faces in one plane, the shared planes
186
+ * ("tops at y=1.05"). Informational for a receipt; it does not decide ranks.
187
+ *
188
+ * @param {Array<object>} objects scene-object records
189
+ * @returns {Array<{a: string, b: string, planes: Array<{face: string, axis: string|null, at: number}>}>}
190
+ */
191
+ export function coplanarConflicts(objects) {
192
+ const pairs = [];
193
+ if (!Array.isArray(objects) || objects.length < 2) return pairs;
194
+ const shapes = objects.map((object) => (object && typeof object === "object" ? facesOf(object) : null));
195
+ for (let j = 1; j < objects.length; j += 1) {
196
+ const b = shapes[j];
197
+ if (!b) continue;
198
+ for (let i = 0; i < j; i += 1) {
199
+ const a = shapes[i];
200
+ if (!a) continue;
201
+ if (
202
+ a.min[0] > b.max[0] + PLANE_EPS || b.min[0] > a.max[0] + PLANE_EPS ||
203
+ a.min[1] > b.max[1] + PLANE_EPS || b.min[1] > a.max[1] + PLANE_EPS ||
204
+ a.min[2] > b.max[2] + PLANE_EPS || b.min[2] > a.max[2] + PLANE_EPS
205
+ ) continue;
206
+ const planes = conflictingFaces(a, b).map((face) => {
207
+ const axis = [0, 1, 2].find((k) => Math.abs(face.n[k]) > NORMAL_DOT);
208
+ if (axis === undefined) return { face: "tilted faces", axis: null, at: round(dot(face.n, face.c)) };
209
+ const sign = face.n[axis] > 0 ? "+" : "-";
210
+ return { face: FACE_NAMES[`${AXES[axis]}${sign}`] ?? `${sign}${AXES[axis]} faces`, axis: AXES[axis], at: round(face.c[axis]) };
211
+ });
212
+ if (planes.length) pairs.push({ a: objects[i].id, b: objects[j].id, planes });
213
+ }
214
+ }
215
+ return pairs;
216
+ }
package/src/cuts.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  updateStableItem,
12
12
  } from "./stable-items.js";
13
13
 
14
- export const DEFAULT_SHOT_FRAMES = 40;
14
+ const DEFAULT_SHOT_FRAMES = 40;
15
15
 
16
16
  const frameNumber = (value, fallback = 0) => Number.isFinite(value) ? Math.round(value) : fallback;
17
17
 
@@ -48,6 +48,12 @@ export function createShot(name = "Shot", startFrame = 0, endFrame = startFrame
48
48
  endFrame: end,
49
49
  cameraKeys: uniqueKeys(cameraKeys, start, end),
50
50
  camera: createCameraBlock(camera),
51
+ // v5 shot-document defaults, so a fresh shot already has the shape the
52
+ // document normalizer gives it (and an undo restores).
53
+ kind: "clip",
54
+ caption: "",
55
+ cast: {},
56
+ stylizedAssetId: null,
51
57
  };
52
58
  }
53
59
 
@@ -133,11 +139,6 @@ export function resizeShot(shots, shotId, edge, rawFrame, frameCount) {
133
139
  .sort((a, b) => a.startFrame - b.startFrame);
134
140
  }
135
141
 
136
- /** Compatibility name for callers that move the left edge. */
137
- export function moveBoundary(shots, shotId, newStartFrame, frameCount) {
138
- return resizeShot(shots, shotId, "start", newStartFrame, frameCount);
139
- }
140
-
141
142
  export function removeShot(shots, shotId) {
142
143
  return removeStableItem(shots, shotId, "shots");
143
144
  }
@@ -12,7 +12,8 @@ import {
12
12
  captureHipsOffset,
13
13
  saveCustomPoses,
14
14
  } from "../poses.js";
15
- import { createCharacterEntry, createCharacterLayer } from "../scenes.js";
15
+ import { createCharacterEntry, createCharacterLayer, isProxyFigure, PROXY_FIGURE_MODEL } from "../scenes.js";
16
+ import { kindRefusal } from "../character-kind.js";
16
17
  import {
17
18
  DEFAULT_SUBJECT,
18
19
  DEFAULT_SUBJECT2,
@@ -28,12 +29,12 @@ import { judgeNextWaypoint } from "../ardy/waypoints.js";
28
29
  import { StudioProtocolError } from "../studio-agent-protocol.js";
29
30
  import { studioActionRefusal } from "../studio-actions.js";
30
31
  import { createStableItemId, removeStableItem } from "../stable-items.js";
31
- import { trackFeature } from "../analytics.js";
32
+ import { track, trackFeature } from "../analytics.js";
32
33
  import { requestBridgeExtract } from "../multimodel-ingest.js";
33
34
  import { loadMotionFromUrl } from "../ardy/npz.js";
34
35
  import { snapshotPlaybackBones, applyMotionFrame, restorePlaybackBones } from "../ardy/playback.js";
35
36
 
36
- import { HISTORY_LIMIT } from "../history.js";
37
+ import { shotAtFrame } from "../cuts.js";
37
38
 
38
39
  const sameCastValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
39
40
 
@@ -43,7 +44,7 @@ export function createCastDomain(appContext, initial, customPoses = []) {
43
44
  let native = createDocumentStore({ owned: { cast: normalize(initial) } });
44
45
  const listeners = new Set(), motions = new Map();
45
46
  const notify = () => { for (const listener of listeners) listener(); };
46
- let release = native.subscribe(notify), loading = false;
47
+ let release = native.subscribe(notify);
47
48
  const documentStore = { ...Object.fromEntries(Object.keys(native).map(key => [key, (...args) => native[key](...args)])),
48
49
  subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); } };
49
50
  const state = () => documentStore.read('cast'), read = () => state().characters;
@@ -74,6 +75,25 @@ export function createCastDomain(appContext, initial, customPoses = []) {
74
75
  function write(update) {
75
76
  return writeState(before => ({ ...before, characters: typeof update === 'function' ? update(before.characters) : update }));
76
77
  }
78
+ function update(characterId, patch) {
79
+ const entry = read().find(entry => entry.id === characterId);
80
+ if (!entry) throw new StudioProtocolError('STALE_TARGET', `Character ${characterId} is not in this scene.`);
81
+ const raw = appContext.ports.read(), shot = shotAtFrame(raw.shots, raw.view.frame);
82
+ const fields = ['x', 'z', 'rot', 'posture', 'pose'];
83
+ const panelPatch = Object.fromEntries(Object.entries(patch).filter(([key, value]) => fields.includes(key) && value !== undefined));
84
+ if (shot?.kind === 'still' && !raw.targets?.get?.(characterId)?.motion && Object.keys(panelPatch).length) {
85
+ const current = { ...entry, ...shot.cast[characterId] };
86
+ const normalized = createCharacterEntry({ ...current, ...panelPatch });
87
+ const override = { x: normalized.x, z: normalized.z, rot: normalized.rot };
88
+ for (const key of ['posture', 'pose']) if (normalized[key] != null) override[key] = normalized[key];
89
+ // Enlist the shot owner in the already-open command/drag session:
90
+ // even a mixed scale + placement edit is one shared Undo entry.
91
+ const shots = appContext.storeDomain('shot');
92
+ appContext.recordAction('shot', () => shots.setCastOverride(shot.id, characterId, override), null, true);
93
+ patch = Object.fromEntries(Object.entries(patch).filter(([key]) => !fields.includes(key)));
94
+ }
95
+ if (Object.keys(patch).length) write(rows => rows.map(row => row.id === characterId ? { ...row, ...patch } : row));
96
+ }
77
97
  function publishMotion(characterId, motion) {
78
98
  if (motion) motions.set(characterId, motion); else motions.delete(characterId);
79
99
  publish(); notify();
@@ -109,7 +129,7 @@ export function createCastDomain(appContext, initial, customPoses = []) {
109
129
  }
110
130
  return run('run.update', { txId: gesture.txId, args });
111
131
  }
112
- const domain = { documentStore, state, read, write, writeState, run, edit, beginGesture, finishGesture,
132
+ const domain = { documentStore, state, read, write, update, writeState, run, edit, beginGesture, finishGesture,
113
133
  activeId: read()[0]?.id ?? null, projection: () => projection, publishMotion, normalizeCharacters: rows => rows.map(createCharacterEntry),
114
134
  bindRender(context) { appContext = context; },
115
135
  beginAction: () => documentStore.beginAction('cast'), canUndo: id => documentStore.canUndo(id),
@@ -121,13 +141,11 @@ export function createCastDomain(appContext, initial, customPoses = []) {
121
141
  write(plan.draft); return plan;
122
142
  },
123
143
  load(value) {
124
- finishGesture(true); loading = true;
125
- try {
126
- release(); native.dispose(); motions.clear();
127
- const next = normalize(Array.isArray(value) ? { characters: value, customPoses: state().customPoses } : value);
128
- native = createDocumentStore({ owned: { cast: next } }); release = native.subscribe(notify);
129
- domain.activeId = read()[0]?.id ?? null; domain.loadView?.(); notify();
130
- } finally { loading = false; }
144
+ finishGesture(true);
145
+ release(); native.dispose(); motions.clear();
146
+ const next = normalize(Array.isArray(value) ? { characters: value, customPoses: state().customPoses } : value);
147
+ native = createDocumentStore({ owned: { cast: next } }); release = native.subscribe(notify);
148
+ domain.activeId = read()[0]?.id ?? null; domain.loadView?.(); notify();
131
149
  },
132
150
  dispose() { finishGesture(true); unregister(); unsubscribe(); release(); native.dispose(); listeners.clear(); },
133
151
  };
@@ -164,7 +182,6 @@ export function useCast(appContext) {
164
182
 
165
183
  const [poseRevision, setPoseTick] = useState(0);
166
184
 
167
- /* --------------------- derived cast view + shims ---------------------- */
168
185
  // Fallback second slot mirrors the old charB defaults so preset math and
169
186
  // the two-subject inspector never see a hole before B exists.
170
187
  const charA = characters[0] ?? createCharacterEntry(null, 0);
@@ -181,29 +198,12 @@ export function useCast(appContext) {
181
198
 
182
199
  const subject2 = charB.subject ?? DEFAULT_SUBJECT2;
183
200
 
184
- const rigA = rigs[charA.id] ?? null;
185
-
186
- const rigB = (characters[1] ? rigs[charB.id] : null) ?? null;
187
-
188
201
  function updateCharacterAt(index, next) {
189
- const entry = domain.read()[index];
202
+ const base = domain.read()[index], raw = appContext.ports.read();
203
+ const entry = base && { ...base, ...shotAtFrame(raw.shots, raw.view.frame)?.cast[base.id] };
190
204
  if (entry) return domain.edit('character.update', { characterId: entry.id, patch: typeof next === 'function' ? next(entry) : next });
191
205
  }
192
206
 
193
- // The shims keep the legacy call sites (inspector sliders, presets, pose
194
- // studio, prompts) untouched while the list stays the source of truth.
195
- const setCharA = (next) => updateCharacterAt(0, next);
196
-
197
- const setCharB = (next) => updateCharacterAt(1, next);
198
-
199
- const setPoseA = (pose) => updateCharacterAt(0, (entry) => ({ pose: typeof pose === "function" ? pose(entry.pose ?? DEFAULT_POSE) : pose }));
200
-
201
- const setPoseB = (pose) => updateCharacterAt(1, (entry) => ({ pose: typeof pose === "function" ? pose(entry.pose ?? DEFAULT_POSE) : pose }));
202
-
203
- const setSubject = (value) => updateCharacterAt(0, (entry) => ({ subject: typeof value === "function" ? value(entry.subject) : value }));
204
-
205
- const setSubject2 = (value) => updateCharacterAt(1, (entry) => ({ subject: typeof value === "function" ? value(entry.subject) : value }));
206
-
207
207
  function setShowB(next) {
208
208
  return domain.run('cast.showExtras', { show: typeof next === 'function' ? next(domain.read().some((entry, i) => i > 0 && !entry.hidden)) : next });
209
209
  }
@@ -221,7 +221,8 @@ export function useCast(appContext) {
221
221
  }
222
222
 
223
223
  function moveCharacter(charId, next) {
224
- const entry = domain.read().find(entry => entry.id === charId);
224
+ const base = domain.read().find(entry => entry.id === charId), raw = appContext.ports.read();
225
+ const entry = { ...base, ...shotAtFrame(raw.shots, raw.view.frame)?.cast[charId] };
225
226
  return domain.edit('character.update', { characterId: charId, patch: typeof next === 'function' ? next(entry) : next });
226
227
  }
227
228
 
@@ -258,6 +259,7 @@ export function useCast(appContext) {
258
259
  const spawnCharacter = (model, x, z) => {
259
260
  const id = nextCharacterId(domain.read());
260
261
  domain.run('character.add', { character: { id, model, x, z, pose: DEFAULT_POSE, subject: 'a person' } });
262
+ if (model === PROXY_FIGURE_MODEL) track("cast:proxy_added", { surface: "assets" });
261
263
  appContext.shared.setSelectedHierarchyId(`character:${id}`);
262
264
  appContext.notify(ko("Character added to the scene", "인물을 씬에 추가했어요"));
263
265
  };
@@ -270,7 +272,6 @@ export function useCast(appContext) {
270
272
  return `character:${key.startsWith("char:") ? key.slice(5) : key}`;
271
273
  };
272
274
 
273
- /* ------------------- active character (motion layer) ------------------- */
274
275
  // Every character owns an animation layer (root path, prompt blocks,
275
276
  // generated clip, IK keys). The studio's motion machinery edits ONE layer
276
277
  // at a time — the ACTIVE character's — and selection decides who that is.
@@ -332,9 +333,6 @@ export function useCast(appContext) {
332
333
  waypointFrames: (entry.layer?.waypoints ?? []).map((waypoint) => waypoint.frame),
333
334
  }]), [characters, activeChar.id]);
334
335
 
335
- // Undo/redo (plan §6.5). The store settles any open drag first, so a
336
- // mid-drag press commits that drag as one entry and then steps past it.
337
- // After a step the selection can point at a deleted object — drop it to
338
336
  /* Cast authoring uses the registered store. The native compatibility
339
337
  * snapshot remains for motion/IK until its owner migrates; it must not
340
338
  * restore the owned cast over newer character edits. */
@@ -424,10 +422,9 @@ export function useCast(appContext) {
424
422
  const posedRig = () => rigs[posing] ?? null;
425
423
 
426
424
  const setPosed = (pose) => {
427
- if (posingIndex >= 0) updateCharacterAt(posingIndex, { pose: typeof pose === "function" ? pose(posingChar?.pose ?? DEFAULT_POSE) : pose });
425
+ if (posingIndex >= 0) updateCharacterAt(posingIndex, entry => ({ pose: typeof pose === "function" ? pose(entry.pose ?? DEFAULT_POSE) : pose }));
428
426
  };
429
427
 
430
- /* ------------------------- waypoint workspace --------------------------- */
431
428
  // A walking pace turns clicked distance into clip time, so pins land at
432
429
  // frames the character can actually reach without ice-skating.
433
430
  const WALK_SPEED_MPS = 1.4;
@@ -681,6 +678,23 @@ export function useCast(appContext) {
681
678
  : ko(`Saved the current pose to the library as “${pose.label}”`, `지금 자세를 “${pose.label}”로 라이브러리에 저장했어요`));
682
679
  }
683
680
 
681
+ /** A handle drag is an authored pose. When it ends, the rig as it now
682
+ * stands becomes the posed character's own pose — so the scene autosaves
683
+ * it and undo can step it back — instead of living only on the rig until
684
+ * a reload re-applies the stored pose over it. The library is untouched;
685
+ * Save pose still bottles a pose there. */
686
+ function commitPosedPose() {
687
+ const rig = posedRig();
688
+ if (!rig || !posingChar) return;
689
+ domain.run('character.setPose', { characterId: posingChar.id, clearMotion: false, pose: {
690
+ id: `edited:${posingChar.id}`,
691
+ label: ko("Edited pose", "편집한 포즈"),
692
+ prompt: "in the exact body pose shown in the blocking frame",
693
+ bones: capturePose(rig),
694
+ rootY: captureHipsOffset(rig),
695
+ } });
696
+ }
697
+
684
698
  function savePose() {
685
699
  const rig = posedRig();
686
700
  if (!rig) return;
@@ -720,6 +734,8 @@ export function useCast(appContext) {
720
734
  setPhotoPoseState("running");
721
735
  setPhotoPoseError("");
722
736
  try {
737
+ // A capsule figure has no bones for a photo to pose.
738
+ if (isProxyFigure(posingChar ?? activeChar)) throw new StudioProtocolError("TARGET_NOT_READY", kindRefusal("photoPose", isKo));
723
739
  if (!rig) throw new Error("rig-not-loaded");
724
740
  objectUrl = URL.createObjectURL(file);
725
741
  let bones = null;
@@ -907,7 +923,9 @@ export function useCast(appContext) {
907
923
  const live = appContext.live.state;
908
924
  const patch = finitePatch(args, ["x", "z", "rot"]);
909
925
  const id = nextCharacterId(live.characters);
910
- replaceCharacters([...live.characters, createCharacterEntry({ id, model: args.model, subject: args.subject, pose: DEFAULT_POSE, ...patch }, live.characters.length)]);
926
+ const entry = createCharacterEntry({ id, model: args.model, subject: args.subject, pose: DEFAULT_POSE, ...patch }, live.characters.length);
927
+ replaceCharacters([...live.characters, entry]);
928
+ if (isProxyFigure(entry)) track("cast:proxy_added", { surface: "mcp" });
911
929
  return { id };
912
930
  }
913
931
  function updateLiveCharacter(args) {
@@ -947,7 +965,6 @@ export function useCast(appContext) {
947
965
  if (typeof block.text !== "string" || !block.text.trim()) throw new Error(`Block ${i + 1} needs text`);
948
966
  return { id: `prompt-${stamp}-${i}`, startFrame, endFrame, text: block.text.trim() };
949
967
  });
950
- const live = appContext.live.state;
951
968
  domain.run('character.setPromptBlocks', { characterId: domain.activeId, blocks: clips });
952
969
  return { blocks: clips.length };
953
970
  }
@@ -982,14 +999,14 @@ export function useCast(appContext) {
982
999
  const motion = appContext.storeDomain('motion');
983
1000
  if (motion) motion.clear(characterId); else appContext.shared.motionDomain.clearMotionNative();
984
1001
  }, characterId, true);
985
- domain.write(rows => rows.map(entry => entry.id === characterId ? { ...entry, pose } : entry));
1002
+ domain.update(characterId, { pose });
986
1003
  };
987
1004
  appContext.updateActionPorts({ addCharacterWaypoint, moveCharacterWaypoint, removeCharacterWaypoint, clearCharacterWaypoints, setWaypointMode });
988
1005
  return {
989
1006
  ...domain,
990
1007
  applyExternalCharacters, publishStudioCharacters, syncStudioLayerBuffer, undoScene, redoScene, snapshotStudioDomain, removeLegacyRootWaypoint, switchActiveCharacterLayer, createLegacyCastHandlers, toggleCharacterHidden,
991
1008
  characters, editCharacters, customPoses, posing, setPosing, posingClosing,
992
- studioPick, setStudioPick, rigs, rigMountEpoch, setRigMountEpoch, setPoseTick, charA, charB, showB,
1009
+ studioPick, setStudioPick, commitPosedPose, rigs, rigMountEpoch, setRigMountEpoch, setPoseTick, charA, charB, showB,
993
1010
  poseA, poseB, subject, subject2, updateCharacterAt, setShowB, moveCharacter, removeCharacter, reportRig,
994
1011
  spawnCharacter, charKeyToHierarchyId, charIdFromHierarchyId, activeCharacterId, setActiveCharacterId,
995
1012
  rowIdForCharIndex, activeChar, selectActiveCharacterInHierarchy, activeCharIndex, activeRig, waitForRig,
@@ -0,0 +1,50 @@
1
+ import * as THREE from "three";
2
+
3
+ const sameMotionIntent = (a, b) => JSON.stringify(a) === JSON.stringify(b);
4
+ const emptyMotionLayer = id => ({ id, take: null, fullTake: null, ikKeys: [], ikPins: [], ikPinResiduals: [], committedIkEdits: [], takeRecipe: null, takeVersions: [] });
5
+ const keyVector = p => ({ x: p.x, y: p.y, z: p.z });
6
+ const keyQuaternion = q => ({ ...keyVector(q), w: q.w });
7
+ function encodeMotionKeys(keys) {
8
+ return [...keys].sort(([a], [b]) => a - b).map(([frame, tracks]) => ({ frame, tracks: Object.fromEntries([...tracks].map(([track, key]) => [track, {
9
+ ...(key.q ? { q: key.q.map(keyQuaternion) } : {}), ...(key.p ? { p: keyVector(key.p) } : {}),
10
+ ...(key.baseQ ? { baseQ: key.baseQ.map(keyQuaternion) } : {}), ...(key.basePos ? { basePos: keyVector(key.basePos) } : {}),
11
+ ...(key.chainP ? { chainP: key.chainP.map(keyVector) } : {}), ...(key.keepTranslations ? { keepTranslations: true } : {}),
12
+ ...(key.correctionRange ? { correctionRange: { start: key.correctionRange.start, end: key.correctionRange.end } } : {}),
13
+ ...(key.blend != null ? { blend: key.blend } : {}),
14
+ ...(key.pin ? { pin: key.pin } : {}),
15
+ }])) }));
16
+ }
17
+ function decodeMotionKeys(rows) {
18
+ const q = value => new THREE.Quaternion(value.x, value.y, value.z, value.w).normalize();
19
+ const p = value => new THREE.Vector3(value.x, value.y, value.z);
20
+ return new Map(rows.map(row => [row.frame, new Map(Object.entries(row.tracks).map(([track, key]) => [track, {
21
+ q: key.q?.map(q) ?? null, p: key.p ? p(key.p) : null,
22
+ ...(key.baseQ ? { baseQ: key.baseQ.map(q) } : {}), ...(key.basePos ? { basePos: p(key.basePos) } : {}),
23
+ ...(key.chainP ? { chainP: key.chainP.map(p) } : {}), ...(key.keepTranslations ? { keepTranslations: true } : {}),
24
+ ...(key.correctionRange ? { correctionRange: { start: key.correctionRange.start, end: key.correctionRange.end } } : {}),
25
+ ...(key.blend != null ? { blend: key.blend } : {}),
26
+ ...(key.pin ? { pin: key.pin } : {}),
27
+ }]))]));
28
+ }
29
+ function cloneRangePin(pin) {
30
+ return {
31
+ ...pin,
32
+ target: pin.target.space === "world"
33
+ ? { space: "world", position: [...pin.target.position] }
34
+ : { space: "object", objectId: pin.target.objectId, local: [...pin.target.local] },
35
+ };
36
+ }
37
+ function encodeRangePins(pins) {
38
+ return [...(pins instanceof Map ? pins.values() : pins ?? [])].map(cloneRangePin);
39
+ }
40
+ function decodeRangePins(rows) {
41
+ return new Map((rows ?? []).map((pin) => [pin.id, cloneRangePin(pin)]));
42
+ }
43
+ function encodeRangePinResiduals(values) {
44
+ return [...(values instanceof Map ? values.entries() : values ?? [])].map(([id, entries]) => [id, (entries ?? []).map((entry) => ({ ...entry }))]);
45
+ }
46
+ function decodeRangePinResiduals(rows) {
47
+ return new Map((rows ?? []).map(([id, entries]) => [id, (entries ?? []).map((entry) => ({ ...entry }))]));
48
+ }
49
+
50
+ export { sameMotionIntent, emptyMotionLayer, encodeMotionKeys, decodeMotionKeys, cloneRangePin, encodeRangePins, decodeRangePins, encodeRangePinResiduals, decodeRangePinResiduals };
@@ -0,0 +1,23 @@
1
+ import { rangePinTracks } from "../../ardy/range-pin.js";
2
+ import { ko } from "../../locale.js";
3
+
4
+ export function ensureRangePinState(state) {
5
+ if (!state) return state;
6
+ if (!(state.pins instanceof Map)) state.pins = new Map();
7
+ if (!(state.pinResiduals instanceof Map)) state.pinResiduals = new Map();
8
+ return state;
9
+ }
10
+ export function rangePinConflictFrames(draft, state) {
11
+ if (!Number.isInteger(draft?.startFrame) || !Number.isInteger(draft?.endFrame) || draft.endFrame < draft.startFrame) return [];
12
+ const tracks = rangePinTracks(draft), conflicts = [];
13
+ for (let frame = draft.startFrame; frame <= draft.endFrame; frame += 1) {
14
+ if (tracks.some((track) => { const key = state.keys.get(frame)?.get(track); return key && key.pin !== draft.id; })) conflicts.push(frame);
15
+ }
16
+ return conflicts;
17
+ }
18
+ export function overlappingRangePin(draft, state) {
19
+ return [...ensureRangePinState(state).pins.values()].find((pin) => pin.id !== draft.id && rangePinTracks(pin).some((track) => rangePinTracks(draft).includes(track)) && Math.max(pin.startFrame, draft.startFrame) <= Math.min(pin.endFrame, draft.endFrame));
20
+ }
21
+ export function rangePinOverlapMessage(pin) {
22
+ return pin ? ko(`A required body or limb track already has an overlapping pin (${pin.startFrame}–${pin.endFrame}).`, `필요한 몸통 또는 팔다리 파츠에 겹치는 고정이 있어요 (${pin.startFrame}–${pin.endFrame}).`) : "";
23
+ }