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
package/src/styles.css CHANGED
@@ -847,6 +847,9 @@ select {
847
847
  display: none;
848
848
  }
849
849
  .app[data-embed-mode="playground"] .tl-view-tools { display: none !important; }
850
+ /* The v2 Sequencer styles sit outside the legacy layer, so the lane rule above
851
+ needs !important to keep only the Shots lane. */
852
+ .app[data-embed-mode="playground"] .tl-track:not(.shots) { display: none !important; }
850
853
  /* Title bar keeps nothing but the pane's own name: the drawn dolly plays
851
854
  full-screen through the shot camera's look-through button, and Esc is the
852
855
  way back to flying. */
@@ -855,6 +858,8 @@ select {
855
858
  /* The plan inset only matters while a rail is being drawn into it. */
856
859
  .app[data-embed-mode="playground"]:not([data-rail-draw="1"]) .vp-inset.plan { display: none !important; }
857
860
  .app[data-embed-mode="playground"][data-rail-draw="1"] .vp-inset.plan { width: 44% !important; height: 62% !important; }
861
+ /* The shot monitor would sit over the map's corner while the rail is drawn. */
862
+ .app[data-embed-mode="playground"][data-rail-draw="1"] .vp-shot-preview { display: none !important; }
858
863
  .app[data-embed-mode="playground"] .bottom-window,
859
864
  .app[data-embed-mode="playground"] .bottom-timeline {
860
865
  flex: 0 0 auto;
@@ -865,7 +870,7 @@ select {
865
870
  .app[data-embed-mode="playground"] .timeline.empty-shots { height: auto !important; min-height: 0 !important; max-height: none; }
866
871
  .app[data-embed-mode="playground"] .tl-body { flex: 0 0 auto; overflow: hidden; }
867
872
  .app[data-embed-mode="playground"] .tl-body { min-height: 0 !important; height: auto !important; }
868
- .app[data-embed-mode="playground"] .tl-surface { height: auto !important; min-height: 0; grid-template-rows: auto auto !important; }
873
+ .app[data-embed-mode="playground"] .tl-surface { height: auto !important; min-height: 0 !important; grid-template-rows: auto auto !important; }
869
874
  /* The Shots lane keeps its full-studio height so the block's hover actions
870
875
  (Duplicate / Delete) sit in a strip above the clickable body instead of
871
876
  covering it. */
@@ -875,7 +880,8 @@ select {
875
880
  /* The shot monitor is the pane the tutorial points at: keep it small. */
876
881
  .app[data-embed-mode="playground"] .vp-shot-preview { width: 22% !important; height: auto !important; aspect-ratio: 16 / 9; right: 12px; bottom: 12px; }
877
882
  .app[data-embed-mode="playground"] .vp-shot-preview-tag { font-size: 12px; }
878
- .app[data-embed-mode="playground"] .vp-look-through { width: 24px; height: 24px; }
883
+ /* The look-through button carries its label now; keep it on one line. */
884
+ .app[data-embed-mode="playground"] .vp-look-through { height: 24px; padding: 0 8px; white-space: nowrap; }
879
885
  .app[data-embed-mode="playground"] .vp-look-through svg { width: 15px; height: 15px; }
880
886
  /* Tutorial hints: the landing page names the control the current step needs. */
881
887
  .app[data-playground-hint="look"] .vp-look-through,
@@ -3032,6 +3038,28 @@ input[type=range]::-moz-range-thumb {
3032
3038
  color: #eafff3;
3033
3039
  }
3034
3040
 
3041
+ /* The selected dot's lean: two small scrub fields, the word as their badge. */
3042
+ .objmo-lean {
3043
+ display: inline-flex;
3044
+ align-items: stretch;
3045
+ gap: 6px;
3046
+ }
3047
+
3048
+ .objmo-lean .number-field {
3049
+ flex: none;
3050
+ width: 104px;
3051
+ }
3052
+
3053
+ .objmo-lean .number-field .axis {
3054
+ width: auto;
3055
+ padding: 0 6px;
3056
+ font-size: 10px;
3057
+ }
3058
+
3059
+ .number-field.disabled { opacity: .45; }
3060
+ .number-field.disabled .axis,
3061
+ .number-field.disabled input { cursor: not-allowed; }
3062
+
3035
3063
  .tl-path-hint {
3036
3064
  margin-left: auto;
3037
3065
  color: #7fae95;
@@ -3136,9 +3164,22 @@ input[type=range]::-moz-range-thumb {
3136
3164
  vector-effect: non-scaling-stroke;
3137
3165
  }
3138
3166
 
3139
- .sg-cut-diamond { fill: #ffb454; }
3167
+ /* The pin head lives outside the SVG: the graph's 1×1 viewBox is stretched to
3168
+ the lane, so anything drawn inside it is as wide as the lane is long. */
3169
+ .sg-cut-pin {
3170
+ position: absolute;
3171
+ top: 3px;
3172
+ width: 9px;
3173
+ height: 9px;
3174
+ padding: 0;
3175
+ border: 0;
3176
+ border-radius: 2px;
3177
+ background: #ffb454;
3178
+ transform: translateX(-50%) rotate(45deg);
3179
+ cursor: pointer;
3180
+ }
3140
3181
 
3141
- .sg-pin.selected .sg-cut-diamond { fill: #ffd08a; }
3182
+ .sg-cut-pin.selected { background: #ffd08a; box-shadow: 0 0 0 2px rgba(255, 208, 138, .35); }
3142
3183
  .sg-pin.selected .sg-cut { stroke-width: 2; stroke-dasharray: none; }
3143
3184
 
3144
3185
  .sg-cut-pick {
@@ -3334,7 +3375,9 @@ input[type=range]::-moz-range-thumb {
3334
3375
  .sg-shot .sg-line { stroke: #b9a1e8; }
3335
3376
  .sg-shot .sg-axis, .sg-shot .sg-average { stroke: rgba(185, 161, 232, .4); }
3336
3377
  .sg-shot .sg-readout { background: #4a3d6e; }
3337
- .sg-shot .sg-scale-top, .sg-shot .sg-scale-avg, .sg-shot .sg-scale-zero { color: #7d6a9e; }
3378
+ /* The box's own width is its clock and it has no room for an axis: a dozen
3379
+ pixels tall, three marks would sit on top of one another. */
3380
+ .sg-bare .sg-scale-top, .sg-bare .sg-scale-avg, .sg-bare .sg-scale-zero { display: none; }
3338
3381
 
3339
3382
 
3340
3383
 
@@ -3390,10 +3433,13 @@ input[type=range]::-moz-range-thumb {
3390
3433
  pointer-events: none;
3391
3434
  }
3392
3435
 
3436
+ /* One axis, one edge: all three scale marks sit on the left. */
3393
3437
  .sg-scale-top,
3394
- .sg-scale-avg {
3438
+ .sg-scale-avg,
3439
+ .sg-scale-zero {
3395
3440
  position: absolute;
3396
3441
  left: 4px;
3442
+ right: auto;
3397
3443
  color: #7fae95;
3398
3444
  font-size: 8px;
3399
3445
  pointer-events: none;
@@ -5992,6 +6038,12 @@ input[type=range]::-moz-range-thumb {
5992
6038
  border-radius: 3px;
5993
6039
  }
5994
6040
 
6041
+ /* an Empty has no colour: a dashed, hollow swatch reads as "nothing to draw" */
6042
+ .add-object-swatch.empty {
6043
+ background: transparent !important;
6044
+ border-style: dashed;
6045
+ }
6046
+
5995
6047
  /* inspector cards clip their overflow, so the menu opens inline there */
5996
6048
  .card .add-object {
5997
6049
  display: block;
@@ -6803,10 +6855,9 @@ input[type=range]::-moz-range-thumb {
6803
6855
 
6804
6856
  /* Keep the timeline focused on the selected workflow. Pose-correction and
6805
6857
  take-generation controls are useful while authoring motion, but they are
6806
- noise during blocking and camera work. The controls stay mounted so the
6807
- mode switch is lossless; only their always-on chrome is hidden. */
6858
+ noise during blocking and camera work. The motion clear action stays
6859
+ visible in Stage so a loaded take is always reversible. */
6808
6860
  .app[data-workflow-mode="scene"] .tl-pose-tools,
6809
- .app[data-workflow-mode="scene"] .tl-motion-tools,
6810
6861
  .app[data-workflow-mode="scene"] .take-bar,
6811
6862
  .app[data-workflow-mode="camera"] .tl-pose-tools,
6812
6863
  .app[data-workflow-mode="camera"] .tl-motion-tools,
@@ -10217,4 +10268,36 @@ input[type=range] {
10217
10268
  margin: 8px 0 0;
10218
10269
  color: var(--muted);
10219
10270
  }
10271
+
10272
+ /* Sculpt mode (#739): the turntable owns the main pane and the shot preview
10273
+ shows the scene, so the Top-View inset has nothing to add. */
10274
+ .app[data-workflow-mode="sculpt"] .vp-inset { display: none !important; }
10275
+
10276
+ /* The empty turntable says what the mode is waiting for, under the mode pill
10277
+ (the dock floats over the pane's foot). Overlay chrome, like the viewport
10278
+ pills: it never takes the pointer from the orbit. */
10279
+ .vp-sculpt-hint {
10280
+ position: absolute;
10281
+ left: 50%;
10282
+ top: calc(2 * var(--sp-2, 12px) + var(--h-control, 28px));
10283
+ z-index: 7;
10284
+ display: grid;
10285
+ gap: var(--sp-1, 4px);
10286
+ max-width: min(420px, calc(100% - 2 * var(--sp-3, 16px)));
10287
+ padding: var(--sp-2, 8px) var(--sp-3, 16px);
10288
+ border-radius: var(--r-2, 6px);
10289
+ background: var(--overlay);
10290
+ color: var(--text-2);
10291
+ font-size: 11px;
10292
+ line-height: 1.45;
10293
+ text-align: center;
10294
+ transform: translateX(-50%);
10295
+ pointer-events: none;
10296
+ }
10297
+
10298
+ .vp-sculpt-hint strong {
10299
+ color: var(--text-1);
10300
+ font-size: 12px;
10301
+ font-weight: 600;
10302
+ }
10220
10303
  }
package/src/ui-scrub.js CHANGED
@@ -28,7 +28,7 @@ export const SCRUB_THRESHOLD_PX = 4;
28
28
 
29
29
  /** Shift buys a quarter-speed pass for fine detail; Alt keeps its old 0.1x. */
30
30
  export const FINE_SCRUB_FACTOR = 0.25;
31
- export const EXTRA_FINE_SCRUB_FACTOR = 0.1;
31
+ const EXTRA_FINE_SCRUB_FACTOR = 0.1;
32
32
 
33
33
  /**
34
34
  * The meaningful span a full drag sweeps when the field does not name one.
@@ -39,8 +39,8 @@ export const SCRUB_RANGE_STEPS = 100;
39
39
 
40
40
  /** The span one full `SCRUB_TRAVEL_PX` drag covers for a field. */
41
41
  export function scrubRangeFor({ scrubRange, step } = {}) {
42
- if (typeof scrubRange === "number" && Number.isFinite(scrubRange) && scrubRange > 0) return scrubRange;
43
- const size = typeof step === "number" && Number.isFinite(step) && step > 0 ? step : 1;
42
+ if (Number.isFinite(scrubRange) && scrubRange > 0) return scrubRange;
43
+ const size = Number.isFinite(step) && step > 0 ? step : 1;
44
44
  return size * SCRUB_RANGE_STEPS;
45
45
  }
46
46
 
@@ -95,7 +95,7 @@ export function createScrubGesture({ x, value, step, precision, scrubRange }) {
95
95
  * 1.7000000000000002 in the input.
96
96
  */
97
97
  export function snapScrubValue(value, { step, precision = 2 } = {}) {
98
- const size = typeof step === "number" && Number.isFinite(step) && step > 0 ? step : 0;
98
+ const size = Number.isFinite(step) && step > 0 ? step : 0;
99
99
  const snapped = size > 0 ? Math.round(value / size) * size : value;
100
100
  const digits = Number.isFinite(precision) ? Math.max(0, Math.min(15, Math.round(precision))) : 2;
101
101
  return Number(snapped.toFixed(digits));
package/src/ui.jsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { createScrubGesture } from "./ui-scrub.js";
4
+ import { isImeComposing } from "./ime.js";
4
5
 
5
6
  // Custom dropdown, structurally matched to the reference (`.dropdown`,
6
7
  // `dd-caret`, `dropdown-menu`, `dropdown-item`, `dd-check`) with the keyboard
@@ -279,7 +280,7 @@ export function Field({ label, children }) {
279
280
  </div>
280
281
  );
281
282
  }
282
- export function NumberField({ label, value, step, precision = 2, scrubRange, onChange, onScrubStart, onScrubEnd, title }) {
283
+ export function NumberField({ label, value, step, precision = 2, scrubRange, onChange, onScrubStart, onScrubEnd, title, disabled = false }) {
283
284
  const [focused, setFocused] = useState(false);
284
285
  const [draft, setDraft] = useState(null);
285
286
  // The draft must be readable synchronously: blur() fires the commit
@@ -335,7 +336,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
335
336
  };
336
337
 
337
338
  const onKeyDown = (e) => {
338
- if (e.key === "Enter") {
339
+ if (e.key === "Enter" && !isImeComposing(e)) {
339
340
  e.preventDefault();
340
341
  commitDraft();
341
342
  // Keep focus and select the committed value so the next keystroke
@@ -357,7 +358,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
357
358
  // alone was a 15 px hotspot nobody found). Sensitivity is a fixed-travel
358
359
  // rate (src/ui-scrub.js), not pixels * step, so every field feels the same.
359
360
  const onFieldPointerDown = (e) => {
360
- if (e.button !== 0) return;
361
+ if (e.button !== 0 || disabled) return;
361
362
  // A press inside an already-focused input is text editing (caret
362
363
  // placement, drag-select), not a scrub — leave it alone.
363
364
  if (focused && e.target === inputRef.current) return;
@@ -460,7 +461,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
460
461
 
461
462
  return (
462
463
  <span
463
- className={`number-field v2-details-number-field${focused ? " editing" : ""}`}
464
+ className={`number-field v2-details-number-field${focused ? " editing" : ""}${disabled ? " disabled" : ""}`}
464
465
  title={title}
465
466
  onPointerDown={onFieldPointerDown}
466
467
  onPointerUp={endFieldDrag}
@@ -472,6 +473,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
472
473
  type="text"
473
474
  inputMode="decimal"
474
475
  value={display}
476
+ disabled={disabled}
475
477
  onFocus={onFocus}
476
478
  onBlur={onBlur}
477
479
  onChange={(e) => setDraftValue(e.target.value)}
@@ -0,0 +1,60 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { ASSET_DELETE_UNDO_MS } from "./app-stage.jsx";
3
+ import { CUTOUT_KIND, MESH_KIND } from "./scene-objects.js";
4
+ import { assetGraphSignature } from "./scene-assets.js";
5
+
6
+ export function useAssetShelf({ scenes, activeSceneId, sceneObjects }) {
7
+ // The imported-pictures region of the Assets shelf. null = the scan has
8
+ // never resolved (the pane shows skeletons, NEVER the empty message); an
9
+ // array = the SOURCE ids to show, mattes and cut renders already filtered
10
+ // out (asset-shelf.js). Scans run only while the shelf is visible, and
11
+ // re-run when a cutout's lineage changes — not on every transform tick, so
12
+ // a gizmo drag never hammers IndexedDB.
13
+ const [shelfImageIds, setShelfImageIds] = useState(null);
14
+ const [shelfMeshIds, setShelfMeshIds] = useState(null);
15
+ const [manageAssetStorage, setManageAssetStorage] = useState(false);
16
+ // A separate scan preserves the source-only placement shelf while the
17
+ // manager exposes every unreachable stored record, including matte and cut
18
+ // derivatives orphaned with a deleted card.
19
+ const [unusedAssetIds, setUnusedAssetIds] = useState(null);
20
+ const [usedAssetIds, setUsedAssetIds] = useState(null);
21
+ const [usageCounts, setUsageCounts] = useState(new Map());
22
+ const assetShelfScanTokenRef = useRef(0);
23
+ const legacyDerivedIdsRef = useRef(new Set());
24
+ // This is deliberately session-only. Each entry is a complete IndexedDB
25
+ // record, so Undo can put it back byte-for-byte until the page is reloaded.
26
+ const [assetTrash, setAssetTrash] = useState([]);
27
+ // Same rule as the object deletion offer: the toast is a window, not a
28
+ // banner. Only the OFFER expires — the trashed record itself is the restore
29
+ // data, so it is deliberately kept for the session and never timed out.
30
+ const [assetUndoOffered, setAssetUndoOffered] = useState(false);
31
+ useEffect(() => {
32
+ if (!assetUndoOffered) return undefined;
33
+ const timer = setTimeout(() => setAssetUndoOffered(false), ASSET_DELETE_UNDO_MS);
34
+ return () => clearTimeout(timer);
35
+ }, [assetUndoOffered]);
36
+ const [deletingAssetId, setDeletingAssetId] = useState(null);
37
+ const cutoutLineage = useMemo(
38
+ () => JSON.stringify(sceneObjects.flatMap((object) => (object.renderer === CUTOUT_KIND ? [[object.assetId, object.sourceAssetId, object.matteAssetId]] : []))),
39
+ [sceneObjects],
40
+ );
41
+ const meshLineage = useMemo(
42
+ () => JSON.stringify(sceneObjects.flatMap((object) => (object.renderer === MESH_KIND ? [object.assetId] : []))),
43
+ [sceneObjects],
44
+ );
45
+ const projectCutoutLineage = useMemo(() => {
46
+ const allScenes = scenes.map((scene) => (scene.id === activeSceneId ? { ...scene, objects: sceneObjects } : scene));
47
+ return JSON.stringify(
48
+ allScenes.flatMap((scene) =>
49
+ (Array.isArray(scene.objects) ? scene.objects : []).flatMap((object) =>
50
+ object.renderer === CUTOUT_KIND ? [[object.assetId, object.sourceAssetId, object.matteAssetId]] : [],
51
+ ),
52
+ ),
53
+ );
54
+ }, [scenes, activeSceneId, sceneObjects]);
55
+ const projectAssetGraphSignature = useMemo(() => {
56
+ const allScenes = scenes.map((scene) => (scene.id === activeSceneId ? { ...scene, objects: sceneObjects } : scene));
57
+ return assetGraphSignature(allScenes);
58
+ }, [scenes, activeSceneId, sceneObjects]);
59
+ return { shelfImageIds, setShelfImageIds, shelfMeshIds, setShelfMeshIds, manageAssetStorage, setManageAssetStorage, unusedAssetIds, setUnusedAssetIds, usedAssetIds, setUsedAssetIds, usageCounts, setUsageCounts, assetShelfScanTokenRef, legacyDerivedIdsRef, assetTrash, setAssetTrash, assetUndoOffered, setAssetUndoOffered, deletingAssetId, setDeletingAssetId, cutoutLineage, meshLineage, projectCutoutLineage, projectAssetGraphSignature };
60
+ }
@@ -0,0 +1,50 @@
1
+ import { useRef, useState } from "react";
2
+ import { cameraTutorialSuppressed } from "./first-shot-handoff.js";
3
+ import { loadProjectSession } from "./project.js";
4
+ import { SCENES_STORAGE_KEY } from "./scenes.js";
5
+
6
+ export function useCameraTutorial({ embedMode, playgroundMode }) {
7
+ // The camera tutorial (#206): the landing page's seven steps, run against
8
+ // the real studio instead of the playground iframe. It opens from
9
+ // /app/?tutorial=camera or from Settings ▾, and it is not offered inside an
10
+ // embed.
11
+
12
+ // Both doors go through startCameraTutorial (#209), which puts the studio in
13
+ // the state the landing page teaches these steps in — the city-block starter
14
+ // scene with the walk take on its character — so Shot / Rail / Play always
15
+ // have something to frame. That function is declared with the project
16
+ // actions further down; the listeners here reach it through a ref so they
17
+ // always call the current render's closure (the confirm reads projectDirty).
18
+ const [cameraTutorialEntry] = useState(() => {
19
+ const params = new URLSearchParams(globalThis.location?.search || "");
20
+ const query = params.get("tutorial") === "camera";
21
+ let firstRun = false;
22
+ try {
23
+ firstRun = !query && !params.get("scene") && !cameraTutorialSuppressed()
24
+ && !loadProjectSession()?.name && !globalThis.localStorage?.getItem(SCENES_STORAGE_KEY);
25
+ } catch { firstRun = false; }
26
+ return !embedMode && !playgroundMode && !cameraTutorialSuppressed() && (query ? "query" : firstRun ? "first-run" : null);
27
+ });
28
+ const cameraTutorialQuery = Boolean(cameraTutorialEntry);
29
+ const [cameraTutorial, setCameraTutorial] = useState(false);
30
+ const cameraTutorialAnalytics = useRef(null);
31
+ const [cameraTutorialAttempt, setCameraTutorialAttempt] = useState(0);
32
+ const [cameraTutorialHandoff, setCameraTutorialHandoff] = useState(null);
33
+ const cameraTutorialCompletedRef = useRef(false);
34
+ const exportMenuTriggerRef = useRef(null);
35
+ const exportShotIdRef = useRef(null);
36
+ // The step the tutorial is on, published on the .app root so styles.css can
37
+ // spotlight the one control that step needs (#211).
38
+ const [cameraTutorialStep, setCameraTutorialStep] = useState(null);
39
+ const startCameraTutorialRef = useRef(null);
40
+ const cameraTutorialStarted = useRef(false);
41
+ // A tutorial restart never reloads the starter over the user's edits.
42
+ const tutorialStarterRef = useRef(false);
43
+ const tutorialLoadingRef = useRef(false);
44
+ const tutorialProjectEpochRef = useRef(0);
45
+ const tutorialSeedEpochRef = useRef(null);
46
+ // Armed once the starter is applied, consumed by the seed effect next to the
47
+ // hosted-demo seed as soon as the new character's rig exists.
48
+ const [tutorialSeedPending, setTutorialSeedPending] = useState(false);
49
+ return { cameraTutorialEntry, cameraTutorialQuery, cameraTutorial, setCameraTutorial, cameraTutorialAnalytics, cameraTutorialAttempt, setCameraTutorialAttempt, cameraTutorialHandoff, setCameraTutorialHandoff, cameraTutorialCompletedRef, exportMenuTriggerRef, exportShotIdRef, cameraTutorialStep, setCameraTutorialStep, startCameraTutorialRef, cameraTutorialStarted, tutorialStarterRef, tutorialLoadingRef, tutorialProjectEpochRef, tutorialSeedEpochRef, tutorialSeedPending, setTutorialSeedPending };
50
+ }
@@ -0,0 +1,12 @@
1
+ import { useRef, useState } from "react";
2
+ import { isPlaygroundEmbed } from "./playground.js";
3
+
4
+ export function useEmbedBridge() {
5
+ const embedMode = ["scene", "playview"].includes(new URLSearchParams(globalThis.location?.search || "").get("embed"));
6
+ // The landing page's try-it iframe: full studio interaction on a preset
7
+ // scene with the project chrome hidden and saving off (see playground.js).
8
+ const playgroundMode = isPlaygroundEmbed(globalThis.location?.search);
9
+ const [playgroundHint, setPlaygroundHint] = useState(null);
10
+ const playgroundExportRef = useRef(null);
11
+ return { embedMode, playgroundMode, playgroundHint, setPlaygroundHint, playgroundExportRef };
12
+ }
@@ -0,0 +1,7 @@
1
+ import { useState } from "react";
2
+
3
+ export function useFirstSuccessPrompt() {
4
+ const [firstSuccessGuideOpen, setFirstSuccessGuideOpen] = useState(false);
5
+ const [useCaseAskOpen, setUseCaseAskOpen] = useState(false);
6
+ return { firstSuccessGuideOpen, setFirstSuccessGuideOpen, useCaseAskOpen, setUseCaseAskOpen };
7
+ }
@@ -0,0 +1,15 @@
1
+ import { useState } from "react";
2
+
3
+ export function useI2vCaptureModal() {
4
+ const [i2vMotionEnabled, setI2vMotionEnabled] = useState(false);
5
+ const [i2vMotionMode, setI2vMotionMode] = useState("interpolate");
6
+ const [i2vMotionCameraUnlocked, setI2vMotionCameraUnlocked] = useState(false);
7
+ return {
8
+ i2vMotionEnabled,
9
+ setI2vMotionEnabled,
10
+ i2vMotionMode,
11
+ setI2vMotionMode,
12
+ i2vMotionCameraUnlocked,
13
+ setI2vMotionCameraUnlocked,
14
+ };
15
+ }
@@ -0,0 +1,95 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { createAgentTransport } from "./workflow/agent-client.js";
3
+ import { requestAgentImage, stylizeErrorMessage } from "./agent-image-client.js";
4
+ import { blueprintErrorMessage, blueprintRequest } from "./sculpt-blueprint.js";
5
+ import { agentSessionReady, createCommandTap, sculptModelChoice, sculptTargetFromSelection, storeSculptModel } from "./sculpt-mode.js";
6
+ import { SCULPT_KIND } from "./scene-objects.js";
7
+
8
+ export function useSculptMode({ workflowMode, sceneObjects, selectedSceneObjectId }) {
9
+ // Sculpt mode (#739): one sculpt on an isolated turntable, the Sculpt panel
10
+ // and its agent run. The target is the sculpt selected on entry, or a new one
11
+ // the first run creates; picking another sculpt while here retargets.
12
+ const sculptMode = workflowMode === "sculpt";
13
+ const sculptCamRef = useRef(null);
14
+ const [sculptTargetId, setSculptTargetId] = useState(null);
15
+ const [sculptView, setSculptView] = useState({ view: "front", nonce: 0 });
16
+ const [sculptReference, setSculptReference] = useState(null);
17
+ const [sculptRun, setSculptRun] = useState(null);
18
+ const sculptRunRef = useRef(null);
19
+ const [sculptAgentStatus, setSculptAgentStatus] = useState("unknown");
20
+ // The model Sculpt runs on (its own pick, remembered apart from the chat model) and the list it comes from.
21
+ const [sculptModels, setSculptModels] = useState({ state: "loading", providers: [], models: [] });
22
+ const [sculptModelPick, setSculptModelPick] = useState({ model: "", effort: null });
23
+ const chooseSculptModel = (next) => setSculptModelPick((current) => {
24
+ const choice = { ...current, ...next };
25
+ storeSculptModel(choice);
26
+ return choice;
27
+ });
28
+ const [sculptTransport] = useState(() => createAgentTransport({ surface: "studio" }));
29
+ const [sculptTap] = useState(createCommandTap);
30
+ const sculptTarget = sceneObjects.find((object) => object.id === sculptTargetId && object.renderer === SCULPT_KIND) ?? null;
31
+ const requestSculptView = (view) => setSculptView((current) => ({ view, nonce: current.nonce + 1 }));
32
+ useEffect(() => {
33
+ if (!sculptMode) return;
34
+ setSculptTargetId(sculptTargetFromSelection(sceneObjects, selectedSceneObjectId));
35
+ requestSculptView("front");
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
37
+ }, [sculptMode]);
38
+ useEffect(() => {
39
+ const selected = sculptMode ? sculptTargetFromSelection(sceneObjects, selectedSceneObjectId) : null;
40
+ if (selected) setSculptTargetId(selected);
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ }, [selectedSceneObjectId]);
43
+ // Start needs the Agent panel's readiness rule (signed in, or a provider
44
+ // key in the sidecar); a sign-in finished in another window lands on focus.
45
+ useEffect(() => {
46
+ if (!sculptMode) return undefined;
47
+ let live = true;
48
+ const read = () => {
49
+ sculptTransport.status().then(
50
+ (status) => { if (live) setSculptAgentStatus(agentSessionReady(status) ? "ready" : "signed-out"); },
51
+ () => { if (live) setSculptAgentStatus("signed-out"); },
52
+ );
53
+ // The Agent panel's catalogue: a key added or removed elsewhere changes what can run.
54
+ sculptTransport.models().then(
55
+ (advertised) => {
56
+ if (!live) return;
57
+ const list = Array.isArray(advertised?.models) ? advertised.models : [];
58
+ setSculptModels({ state: list.length ? "ready" : "failed", providers: advertised?.providers ?? [], models: list });
59
+ setSculptModelPick((current) => sculptModelChoice(advertised, current));
60
+ },
61
+ () => { if (live) setSculptModels((current) => ({ ...current, state: current.models.length ? current.state : "failed" })); },
62
+ );
63
+ };
64
+ read();
65
+ window.addEventListener("focus", read);
66
+ return () => { live = false; window.removeEventListener("focus", read); };
67
+ }, [sculptMode, sculptTransport]);
68
+ // The blueprint step: the reference turned into a four-view model sheet by one image request.
69
+ const [sculptBlueprint, setSculptBlueprint] = useState({ state: "idle", pose: "keep", notes: "", sheet: null, error: null, startedAt: null });
70
+ const blueprintAbortRef = useRef(null);
71
+ async function generateSculptBlueprint() {
72
+ if (!sculptReference) return;
73
+ blueprintAbortRef.current?.abort();
74
+ const controller = new AbortController();
75
+ blueprintAbortRef.current = controller;
76
+ const startedAt = Date.now();
77
+ setSculptBlueprint((current) => ({ ...current, state: "generating", error: null, startedAt }));
78
+ try {
79
+ const result = await requestAgentImage(blueprintRequest({ reference: sculptReference, pose: sculptBlueprint.pose, notes: sculptBlueprint.notes }), { signal: controller.signal });
80
+ if (blueprintAbortRef.current !== controller) return;
81
+ setSculptBlueprint((current) => ({ ...current, state: "ready", sheet: { dataUrl: result.dataUrl, width: result.width, height: result.height, ms: Date.now() - startedAt } }));
82
+ } catch (error) {
83
+ if (blueprintAbortRef.current !== controller) return;
84
+ const cancelled = controller.signal.aborted;
85
+ setSculptBlueprint((current) => ({ ...current, state: current.sheet ? "ready" : "idle", error: cancelled ? null : blueprintErrorMessage(error, stylizeErrorMessage(error).replace(/^Stylize failed — |^스타일화 실패 — /, "")) }));
86
+ }
87
+ }
88
+ function cancelSculptBlueprint() {
89
+ const controller = blueprintAbortRef.current;
90
+ blueprintAbortRef.current = null;
91
+ controller?.abort();
92
+ setSculptBlueprint((current) => ({ ...current, state: current.sheet ? "ready" : "idle" }));
93
+ }
94
+ return { sculptMode, sculptCamRef, sculptView, sculptReference, setSculptReference, sculptRun, setSculptRun, sculptRunRef, sculptAgentStatus, sculptModels, sculptModelPick, chooseSculptModel, sculptTransport, sculptTap, sculptTarget, setSculptTargetId, requestSculptView, sculptBlueprint, setSculptBlueprint, generateSculptBlueprint, cancelSculptBlueprint };
95
+ }