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
@@ -1,5 +1,6 @@
1
1
  import { studioActionDeclaration } from "../studio-actions.js";
2
2
  import { fail } from "./shared.js";
3
+ import { PREVIS_MODES } from "../project.js";
3
4
 
4
5
  const input = (properties = {}, required = []) => ({ type: "object", properties, required, additionalProperties: false });
5
6
  const name = { type: "string", minLength: 1, maxLength: 240 };
@@ -8,12 +9,13 @@ const document = (id, label, properties = {}) => ({ id, label, description: labe
8
9
  const rename = { id: "project.rename", label: "Rename project", description: "Rename the project in one retained undo entry.", kind: "mutation", undoDomain: "scenes", input: input({ name }, ["name"]) };
9
10
  const save = { ...studioActionDeclaration("project.save"), input: input({ name }) };
10
11
  const saveAs = { ...save, id: "project.saveAs", label: "Save project as" };
12
+ const exportAsAnimation = studioActionDeclaration("project.exportAsAnimation");
11
13
  const opening = document("project.open", "Open a project file", { serialized: { type: "string", maxLength: 400_000_000 }, handleToken: token, projectToken: token });
12
- const fresh = document("project.new", "Start a new project", { name });
14
+ const fresh = document("project.new", "Start a new project", { name, previsMode: { type: "string", enum: [...PREVIS_MODES] } });
13
15
  const starter = document("project.openStarter", "Open a starter project", { id: token, source: { type: "string", enum: ["starter", "tutorial", "launch"] }, name });
14
16
  const restore = { ...document("project.restore", "Restore the remembered project", { handleToken: token }), exposure: "ui-only" };
15
17
  const browse = { id: "project.browse", label: "Browse projects", description: "Show the project browser.", kind: "transient", exposure: "ui-only", input: input() };
16
- export const declarations = Object.freeze([rename, save, saveAs, opening, fresh, starter, restore, browse]);
18
+ export const declarations = Object.freeze([rename, save, saveAs, exportAsAnimation, opening, fresh, starter, restore, browse]);
17
19
 
18
20
  export function register(registry, ports) {
19
21
  const owner = () => ports.storeDomain?.("scenes");
@@ -44,6 +46,17 @@ export function register(registry, ports) {
44
46
  ? `This browser has no file access, so the project ${saved.name} was downloaded as ${saved.fileName}.`
45
47
  : `Saved the project ${saved.name} to ${saved.fileName}.` };
46
48
  } });
49
+ registry.register({ ...exportAsAnimation, available: () => true,
50
+ run: async (_args, context) => {
51
+ const saved = await owner().exportAsAnimationProject(context);
52
+ if (saved?.cancelled) fail("TARGET_NOT_READY", "Not exported: the user closed the file picker.");
53
+ if (!saved?.saved) fail("TARGET_NOT_READY", saved?.failure === "not-storyboard"
54
+ ? "Export as Animation project is available only for storyboard projects."
55
+ : "Not exported: writing the animation project file failed.");
56
+ return { affectedIds: [], output: { fileName: saved.fileName }, summary: saved.downloaded
57
+ ? `This browser has no file access, so the animation project ${saved.name} was downloaded as ${saved.fileName}.`
58
+ : `Exported the storyboard as ${saved.name} to ${saved.fileName}.` };
59
+ } });
47
60
  for (const declaration of [opening, fresh, starter, restore, browse]) registry.register({ ...declaration,
48
61
  available, run: async (args, context) => {
49
62
  const result = await owner().projectAction(declaration.id, args, context);
@@ -0,0 +1,53 @@
1
+ // The image port behind sculpt.compare: a reference ({ assetId } from the asset
2
+ // store, or { dataUrl }) decoded to RGBA, and an RGBA sheet encoded as a PNG data
3
+ // URL. The editor uses the browser one below; a Node test hands in its own as
4
+ // `ports.sculptImages`, so the scoring runs on the same pixels in both.
5
+ import { getAsset, openAssetDb } from "../scene-assets.js";
6
+
7
+ function canvas(width, height) {
8
+ if (typeof OffscreenCanvas === "function") return new OffscreenCanvas(width, height);
9
+ if (globalThis.document?.createElement) return Object.assign(document.createElement("canvas"), { width, height });
10
+ throw new Error("this browser cannot draw images: no canvas is available");
11
+ }
12
+
13
+ async function referenceBlob(reference) {
14
+ if (reference.dataUrl !== undefined) return (await fetch(reference.dataUrl)).blob();
15
+ const asset = await getAsset(await openAssetDb(), reference.assetId);
16
+ if (!asset || asset.width === undefined) throw new Error(`no image asset ${reference.assetId} in this project`);
17
+ return new Blob([asset.bytes], { type: asset.type });
18
+ }
19
+
20
+ export const browserSculptImages = Object.freeze({
21
+ async decode(reference) {
22
+ if (typeof createImageBitmap !== "function") throw new Error("this browser cannot decode images: createImageBitmap is unavailable");
23
+ const bitmap = await createImageBitmap(await referenceBlob(reference));
24
+ try {
25
+ const context = canvas(bitmap.width, bitmap.height).getContext("2d", { willReadFrequently: true });
26
+ context.drawImage(bitmap, 0, 0);
27
+ const { width, height, data } = context.getImageData(0, 0, bitmap.width, bitmap.height);
28
+ return { width, height, data };
29
+ } finally {
30
+ bitmap.close?.();
31
+ }
32
+ },
33
+ async encodePng({ width, height, data }) {
34
+ const surface = canvas(width, height);
35
+ surface.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength), width, height), 0, 0);
36
+ if (!surface.convertToBlob) return surface.toDataURL("image/png");
37
+ const bytes = new Uint8Array(await (await surface.convertToBlob({ type: "image/png" })).arrayBuffer());
38
+ let binary = "";
39
+ for (let i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
40
+ return `data:image/png;base64,${btoa(binary)}`;
41
+ },
42
+ });
43
+
44
+ /** Nearest-neighbour shrink of an RGBA image by an integer factor. */
45
+ export function shrinkRgba({ width, height, data }, factor) {
46
+ const w = Math.max(1, Math.floor(width / factor)), h = Math.max(1, Math.floor(height / factor));
47
+ const out = new Uint8Array(w * h * 4);
48
+ for (let y = 0; y < h; y += 1) for (let x = 0; x < w; x += 1) {
49
+ const s = ((y * factor) * width + x * factor) * 4;
50
+ out.set(data.subarray(s, s + 4), (y * w + x) * 4);
51
+ }
52
+ return { width: w, height: h, data: out };
53
+ }
@@ -1,20 +1,25 @@
1
1
  // One semantic entry point for timeline, camera controls and agent framing.
2
2
  import { createCameraBlock, updateCameraBlock, removeCameraRail } from '../camera-block.js';
3
- import { addShotAtFrame, cutAtFrame, duplicateShot, removeShot, reorderShot, resizeShot, renameShot, moveCameraKey, removeCameraKey } from '../cuts.js';
3
+ import { addShotAtFrame, createShot, cutAtFrame, duplicateShot, removeShot, reorderShot, resizeShot, renameShot, moveCameraKey, removeCameraKey } from '../cuts.js';
4
+ import { reflowStillShots, stillFrameCount, STILL_FRAME_COUNT_MAX, STILL_HOLD_DEFAULT } from '../shot-authoring.js';
5
+ import { ko } from '../locale.js';
6
+ import { track } from '../analytics.js';
7
+ import { isImageAssetId } from '../scene-assets.js';
4
8
  import { createStableItemId, updateStableItem } from '../stable-items.js';
5
9
  import { railFollowForNewGeometry } from '../camera-rail-schedule.js';
6
- import { studioActionDeclaration } from '../studio-actions.js';
10
+ import { studioActionDeclaration, studioActionRefusal } from '../studio-actions.js';
7
11
  import { STUDIO_TOOL_SCHEMAS, StudioSchemas } from '../studio-agent-protocol.js';
8
12
  import { elementSetSchema, registerElementSet } from './elements.js';
9
13
  import './elements/shot.js';
10
14
  import { changedIds, fail, shotLabel } from './shared.js';
11
15
 
12
16
  const id = StudioSchemas.TargetGuard.properties.targetId;
17
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
13
18
  const number = { type: 'number' };
14
19
  const frame = { type: 'integer', minimum: 0 };
15
20
  const input = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: false });
16
21
  const mutation = (id, label, schema) => ({ id, label, description: label, kind: 'mutation', undoDomain: 'shot', input: schema });
17
- const existing = ['shot.create', 'shot.split', 'shot.duplicate', 'shot.remove', 'shot.setRange', 'shot.setCameraRail', 'shot.clearCameraRail', 'shot.reorder'].map(studioActionDeclaration);
22
+ const existing = ['shot.create', 'shot.createStill', 'shot.setCaption', 'shot.setCastOverride', 'shot.setHold', 'shot.split', 'shot.duplicate', 'shot.remove', 'shot.setRange', 'shot.setCameraRail', 'shot.clearCameraRail', 'shot.reorder'].map(studioActionDeclaration);
18
23
  const extra = [
19
24
  mutation('shot.set', 'Set shot fields', elementSetSchema('shot')),
20
25
  mutation('shot.rename', 'Rename shot', input({ shotId: id, name: { type: 'string', maxLength: 240 } })),
@@ -28,6 +33,9 @@ const extra = [
28
33
  mutation('shot.frame', 'Frame the shot', { ...input({ ...STUDIO_TOOL_SCHEMAS.frame_shot.properties, preset: { type: 'string' } }, []), oneOf: [STUDIO_TOOL_SCHEMAS.frame_shot, input({ preset: { type: 'string' } })] }),
29
34
  { ...mutation('shot.replace', 'Replace shot authoring', input({ shots: { type: 'array', items: { type: 'object', properties: {}, additionalProperties: true } } })), exposure: 'ui-only' },
30
35
  { ...mutation('shot.captureCamera', 'Capture camera framing', input({ shotId: id }, [])), exposure: 'ui-only' },
36
+ // The Board's Stylize result (or null to clear it). UI-only: the picture has
37
+ // to be in the asset store first, which only the Stylize flow does.
38
+ { ...mutation('shot.setStylized', 'Set stylized panel image', input({ shotId: id, assetId: { oneOf: [{ type: 'string' }, { type: 'null' }] } })), exposure: 'ui-only' },
31
39
  { ...mutation('shot.placeCamera', 'Place camera', input(Object.fromEntries(['x', 'y', 'z', 'lookAtX', 'lookAtY', 'lookAtZ', 'focalMm'].map(key => [key, number])), [])), exposure: 'ui-only' },
32
40
  ];
33
41
  export const declarations = Object.freeze([extra[0], ...existing, ...extra.slice(1).map(entry => entry.id === 'shot.frame' ? entry : { ...entry, exposure: 'ui-only' })]);
@@ -37,16 +45,75 @@ export function register(registry, ports) {
37
45
  const mounted = () => Boolean(ports.storeDomain?.('shot')) || 'The shots document owner is not mounted.';
38
46
  const shotOf = shotId => owner().read().find(shot => shot.id === shotId) ?? fail('STALE_TARGET', `Shot ${shotId} is not in this scene.`);
39
47
  const patchShot = (shotId, update) => { shotOf(shotId); owner().write(current => updateStableItem(current, shotId, update, 'shots')); };
48
+ const storyboard = () => ports.storeDomain?.('scenes')?.metadata?.().previsMode === 'storyboard';
49
+ const stillOf = shotId => {
50
+ const shot = shotOf(shotId);
51
+ return shot.kind === 'still' ? shot : fail('INVALID_ARGUMENT', `${shot.name} is a clip shot; only stills have a hold and a place in the still order.`);
52
+ };
53
+ // Every still edit is one write: the stills (in their new array order) are
54
+ // laid end to end and the timeline is sized to them, or nothing changes.
55
+ function writeStills(shots) {
56
+ const next = reflowStillShots(shots), frameCount = stillFrameCount(next);
57
+ if (frameCount > STILL_FRAME_COUNT_MAX) {
58
+ const en = `Storyboard is full (${STILL_FRAME_COUNT_MAX} frames); shorten a hold or remove a panel.`;
59
+ throw studioActionRefusal('INVALID_ARGUMENT', en, ko(en, `스토리보드가 가득 찼습니다 (${STILL_FRAME_COUNT_MAX}프레임). 홀드를 줄이거나 패널을 지우세요.`));
60
+ }
61
+ const clip = next.find(shot => shot.kind !== 'still' && next.some(still => still.kind === 'still' && still.startFrame <= shot.endFrame && still.endFrame >= shot.startFrame));
62
+ if (clip) fail('TARGET_NOT_READY', `The stills would overlap the clip shot ${clip.name} (frames ${clip.startFrame}-${clip.endFrame}); move or remove it first.`);
63
+ owner().writeState(before => ({ ...before, shots: next, frameCount }));
64
+ return next;
65
+ }
66
+ function createStill({ caption = '', hold = STILL_HOLD_DEFAULT } = {}) {
67
+ const current = owner().read(), framing = owner().capture(), last = current.findLastIndex(shot => shot.kind === 'still');
68
+ const keys = framing ? [{ id: createStableItemId('camera-key'), frame: 0, framing }] : [];
69
+ const still = { ...createShot(`Shot ${current.length + 1}`, 0, hold - 1, keys), kind: 'still', caption };
70
+ const written = writeStills([...current.slice(0, last + 1), still, ...current.slice(last + 1)]);
71
+ // Land the playhead in the new panel (the timeline.seek path) so the placement
72
+ // actions that follow write this panel's cast override, not the previous one's.
73
+ const created = written.find(shot => shot.id === still.id), { selection, view } = ports.readView();
74
+ ports.publishView({ selection, view: { ...view, frame: created.startFrame }, shotId: created.id });
75
+ }
76
+ function duplicate(shotId) {
77
+ const source = shotOf(shotId), copied = { kind: source.kind, caption: source.caption, cast: structuredClone(source.cast), stylizedAssetId: source.stylizedAssetId };
78
+ if (source.kind !== 'still') {
79
+ const before = owner().read(), next = duplicateShot(before, shotId, owner().state().frameCount);
80
+ return owner().write(next.map(shot => before.includes(shot) ? shot : { ...shot, ...copied }));
81
+ }
82
+ const current = owner().read(), index = current.indexOf(source);
83
+ const copy = { ...createShot(`${source.name} copy`, source.startFrame, source.endFrame,
84
+ source.cameraKeys.map(key => ({ ...key, id: createStableItemId('camera-key') })), source.camera), ...copied };
85
+ writeStills([...current.slice(0, index + 1), copy, ...current.slice(index + 1)]);
86
+ }
87
+ function reorder({ shotId, startFrame, index }) {
88
+ if ((startFrame === undefined) === (index === undefined)) fail('INVALID_ARGUMENT', 'shot.reorder takes exactly one of startFrame or index.');
89
+ const shot = shotOf(shotId);
90
+ if (shot.kind !== 'still') {
91
+ if (index !== undefined) stillOf(shotId);
92
+ return owner().write(current => reorderShot(current, shotId, startFrame, owner().state().frameCount));
93
+ }
94
+ const current = owner().read(), others = current.filter(row => row.kind === 'still' && row.id !== shotId);
95
+ const target = index ?? others.filter(row => row.startFrame < startFrame).length;
96
+ const order = [...others.slice(0, target), shot, ...others.slice(target)];
97
+ let next = 0;
98
+ writeStills(current.map(row => row.kind === 'still' ? order[next++] : row));
99
+ }
40
100
  const methods = {
41
- 'shot.create': () => owner().write(current => addShotAtFrame(current, ports.state().frame, owner().state().frameCount, owner().capture())),
101
+ 'shot.create': () => storyboard() ? createStill() : owner().write(current => addShotAtFrame(current, ports.state().frame, owner().state().frameCount, owner().capture())),
102
+ 'shot.createStill': args => createStill(args),
103
+ 'shot.setCaption': ({ shotId, caption }) => patchShot(shotId, shot => ({ ...shot, caption })),
104
+ 'shot.setCastOverride': ({ shotId, characterId, override }) => owner().setCastOverride(shotId, characterId, override),
105
+ 'shot.setHold': ({ shotId, hold }) => {
106
+ const shot = stillOf(shotId), endFrame = shot.startFrame + hold - 1;
107
+ writeStills(owner().read().map(row => row === shot ? { ...row, endFrame, cameraKeys: row.cameraKeys.filter(key => key.frame <= endFrame) } : row));
108
+ },
42
109
  'shot.split': ({ shotId }) => {
43
110
  const shot = shotOf(shotId), { frame } = ports.state();
44
111
  if (frame <= shot.startFrame || frame > shot.endFrame) fail('TARGET_NOT_READY', `The playhead (frame ${frame}) is not inside ${shot.name} after its first frame.`);
45
112
  owner().write(current => cutAtFrame(current, shotId, frame, owner().capture()));
46
113
  },
47
- 'shot.duplicate': ({ shotId }) => { shotOf(shotId); owner().write(current => duplicateShot(current, shotId, owner().state().frameCount)); },
114
+ 'shot.duplicate': ({ shotId }) => duplicate(shotId),
48
115
  'shot.remove': ({ shotId }) => { shotOf(shotId); owner().write(current => removeShot(current, shotId)); },
49
- 'shot.reorder': ({ shotId, startFrame }) => { shotOf(shotId); owner().write(current => reorderShot(current, shotId, startFrame, owner().state().frameCount)); },
116
+ 'shot.reorder': args => reorder(args),
50
117
  'shot.setRange': ({ shotId, range }) => {
51
118
  const shot = shotOf(shotId), startFrame = range.startFrame, endFrame = range.endFrameExclusive - 1;
52
119
  const edges = startFrame > shot.endFrame ? [['end', endFrame], ['start', startFrame]] : [['start', startFrame], ['end', endFrame]];
@@ -77,24 +144,35 @@ export function register(registry, ports) {
77
144
  'shot.setLens': ({ fovDeg }) => owner().setLens(fovDeg),
78
145
  'shot.replace': ({ shots }) => owner().write(shots),
79
146
  'shot.captureCamera': args => owner().captureCamera(args.shotId),
147
+ 'shot.setStylized': ({ shotId, assetId }) => {
148
+ if (assetId !== null && !isImageAssetId(assetId)) fail('INVALID_ARGUMENT', `${assetId} is not an image asset id.`);
149
+ patchShot(shotId, shot => ({ ...shot, stylizedAssetId: assetId }));
150
+ },
80
151
  'shot.placeCamera': args => owner().placeCamera(args),
81
152
  };
82
153
  const hasShots = state => state.shots.length > 0 || 'There are no shots yet; add one with shot.create.';
83
154
  const availability = {
84
- 'shot.create': state => addShotAtFrame(state.shots, state.frame, state.frameCount, null) !== state.shots || `There is no free room for a new shot at the playhead (frame ${state.frame}); move it with operate_studio { frame } or shorten a shot.`,
155
+ 'shot.createStill': () => true,
156
+ 'shot.create': state => storyboard() || addShotAtFrame(state.shots, state.frame, state.frameCount, null) !== state.shots || `There is no free room for a new shot at the playhead (frame ${state.frame}); move it with operate_studio { frame } or shorten a shot.`,
85
157
  'shot.split': state => state.shots.some(shot => state.frame > shot.startFrame && state.frame <= shot.endFrame) || `The playhead (frame ${state.frame}) is not inside a shot after its first frame; move it with operate_studio { frame }.`,
86
158
  'shot.clearCameraRail': state => state.shots.some(shot => createCameraBlock(shot.camera).cameraRail) || 'No shot has a camera rail; lay one with shot.setCameraRail.',
87
159
  };
88
160
  registerElementSet(registry, ports, extra[0]);
89
161
  for (const declaration of declarations.filter(row => row.id !== 'shot.set')) registry.register({ ...declaration,
90
162
  available: state => mounted() === true ? (availability[declaration.id] ?? (existing.includes(declaration) ? hasShots : () => true))(state) : mounted(),
91
- run(args) {
163
+ run(args, context) {
92
164
  if (declaration.id === 'shot.frame') {
93
165
  const plan = owner().frame(args);
94
166
  return { affectedIds: plan.affectedIds, summary: 'Framed the shot.' };
95
167
  }
96
- const before = owner().read(); methods[declaration.id](args);
97
- const after = owner().read(), changed = changedIds(before, after);
168
+ const before = owner().read(), newPanel = declaration.id === 'shot.createStill' || (declaration.id === 'shot.create' && storyboard());
169
+ methods[declaration.id](args);
170
+ // Only the origin travels: never the caption, name or id of the panel.
171
+ if (newPanel) track('storyboard:panel_created', { source: context?.origin && context.origin !== 'ui' ? 'agent' : 'manual' });
172
+ // The owner re-normalizes every shot on write, so compare content, not
173
+ // identity; a storyboard reflow can still move more shots than a receipt
174
+ // names (100), and the first 100 stand for the batch.
175
+ const after = owner().read(), changed = changedIds(before.map(row => after.find(next => next.id === row.id && same(next, row)) ?? row), after).slice(0, 100);
98
176
  const affectedIds = changed.length ? changed : [args.shotId ?? ports.state().activeSceneId];
99
177
  return { affectedIds, summary: changed.length ? `${declaration.label}: ${changed.map(id => after.find(row => row.id === id)).filter(Boolean).map(shotLabel).join('; ')}.` : `${declaration.label}.` };
100
178
  },
@@ -1,7 +1,9 @@
1
1
  // Viewer preferences: transient, like the View menu they mirror.
2
- import { studioActionDeclaration } from "../studio-actions.js";
3
- import { StudioProtocolError, STUDIO_TOOL_SCHEMAS } from '../studio-agent-protocol.js';
2
+ import { studioActionDeclaration, studioActionRefusal } from "../studio-actions.js";
3
+ import { STUDIO_TOOL_SCHEMAS } from '../studio-agent-protocol.js';
4
+ import { ko } from '../locale.js';
4
5
  import { shotAtFrame } from '../cuts.js';
6
+ import { fail } from './shared.js';
5
7
 
6
8
  const fields = STUDIO_TOOL_SCHEMAS.operate_studio.properties;
7
9
  const groups = { 'view.select': ['selection', 'shotId'], 'timeline.seek': ['frame'], 'timeline.play': ['playing'], 'view.setMode': ['mode', 'view'] };
@@ -10,16 +12,18 @@ const operations = Object.entries({ ...groups, 'view.update': Object.keys(fields
10
12
  input: { type: 'object', properties: Object.fromEntries(keys.map(key => [key, fields[key]])), required: [], additionalProperties: false },
11
13
  }));
12
14
  export const declarations = Object.freeze([...operations, ...["view.setPartColours", "view.setGuideMode", "view.setInset"].map(studioActionDeclaration)]);
15
+ // A storyboard project has no Motion department; every door says so the same way.
16
+ export const STORYBOARD_MOTION_REFUSAL = Object.freeze({ en: 'Motion tools are not part of a Storyboard project.', ko: '모션 도구는 스토리보드 프로젝트에 없어요.' });
13
17
  export const viewCommand = args => Object.entries(groups).find(([, keys]) => Object.keys(args).every(key => keys.includes(key)))?.[0] ?? 'view.update';
14
18
 
15
19
  export function register(registry, ports) {
16
20
  registry.registerToolAlias('operate_studio', viewCommand);
17
21
  for (const declaration of operations) registry.register({ ...declaration, available: () => true, run(args) {
18
- const fail = (code, message) => { throw new StudioProtocolError(code, message); };
19
22
  if (!Object.keys(args).length || (args.view && !Object.keys(args.view).length)) fail('INVALID_ARGUMENT', 'Specify a view operation.');
20
23
  const state = ports.readView();
21
24
  // Pose mode is IK editing: without a rig to solve there is nothing to pose.
22
25
  if (args.mode === 'pose' && args.mode !== state.view.mode && !ports.canPose()) fail('TARGET_NOT_READY', 'Pose mode needs a loaded character rig to edit with IK.');
26
+ if (args.mode === 'motion' && state.previsMode === 'storyboard') throw studioActionRefusal('TARGET_NOT_READY', STORYBOARD_MOTION_REFUSAL.en, ko(STORYBOARD_MOTION_REFUSAL.en, STORYBOARD_MOTION_REFUSAL.ko));
23
27
  const selection = args.selection === undefined ? state.selection : args.selection;
24
28
  if (selection) {
25
29
  const found = selection.kind === 'scene' ? selection.id === state.host.sceneId : selection.kind === 'camera' ? selection.id === 'camera'
@@ -31,7 +35,9 @@ export function register(registry, ports) {
31
35
  const frame = args.frame ?? shot?.startFrame ?? state.view.frame;
32
36
  if (frame >= Math.max(1, state.frameCount)) fail('INVALID_RANGE', 'Frame is outside the timeline.');
33
37
  const view = { ...state.view, ...args.view, frame, mode: args.mode ?? state.view.mode, playing: args.playing ?? state.view.playing };
34
- ports.publishView({ selection, view, shotId: args.shotId ?? shotAtFrame(state.shots, frame)?.id ?? null });
38
+ // modeSet: only a command that names a mode moves the editor out of a
39
+ // UI-only mode (Sculpt, #739), which the published view reads as Stage.
40
+ ports.publishView({ selection, view, shotId: args.shotId ?? shotAtFrame(state.shots, frame)?.id ?? null, modeSet: args.mode !== undefined });
35
41
  return { affectedIds: [state.host.sceneId], summary: 'Updated editor view.' };
36
42
  } });
37
43
  const viewAction = (id, run) => registry.register({ ...studioActionDeclaration(id), available: () => true,
@@ -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
  }