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.
- package/CHANGELOG.md +61 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +211 -36
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +4 -4
- package/bin/live/cli.mjs +23 -8
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +2 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-C9pMpW7s.js +1 -0
- package/dist/assets/app-B_2XB9rw.css +1 -0
- package/dist/assets/app-DAuvKPP8.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-CvKuBgy5.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-msLI71g1.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-ChUvB_zL.js} +1 -1
- package/dist/assets/scene-asset-sync-DKkpjKBV.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-BPk2Ni95.js} +1 -1
- package/dist/assets/workflow-jJRBR94Z.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/llms.txt +14 -0
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +813 -278
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +184 -6
- package/src/ardy/timeline.jsx +121 -36
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +0 -3
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +6 -7
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +15 -2
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +79 -28
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +49 -4
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +58 -9
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +60 -16
- package/src/shell/CompositionGuideButton.jsx +20 -0
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/ShotPreviewHeader.jsx +17 -0
- package/src/shell/TopBar.jsx +18 -3
- package/src/shell/ViewportToolbar.jsx +36 -31
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shell/viewport.css +12 -0
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +213 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +60 -24
- package/src/studio-app-binding.js +56 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +92 -9
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +106 -18
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/gt-render/browser.mjs +23 -0
- package/tools/headless-studio.mjs +85 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +95 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-BxBLC6GT.js +0 -4876
- package/dist/assets/app-CJdxvnrl.css +0 -1
- package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
6807
|
-
|
|
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
|
-
|
|
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 (
|
|
43
|
-
const size =
|
|
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 =
|
|
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
|
+
}
|