cozyclay 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +219 -38
- 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 +6 -5
- package/bin/cozyclay.mjs +12 -3
- package/bin/live/cli.mjs +2 -3
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +12 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-B41FmPYI.js +1 -0
- package/dist/assets/app-Do-Og5Pu.css +1 -0
- package/dist/assets/app-DwP-tl2x.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
- package/dist/assets/scene-asset-sync-BCWDJB1d.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-jqMhqL88.js} +1 -1
- package/dist/assets/workflow-Br-K7Srs.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/sitemap.xml +5 -5
- 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 +801 -259
- 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 +196 -3
- package/src/ardy/timeline.jsx +117 -29
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +1 -4
- 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 +43 -24
- 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 +16 -3
- 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 +89 -32
- 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/generation.js +1 -1
- 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 +52 -3
- 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 +62 -17
- 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 +58 -14
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/TopBar.jsx +17 -3
- package/src/shell/ViewportToolbar.jsx +18 -3
- 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/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 +186 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +56 -23
- package/src/studio-app-binding.js +52 -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 +111 -10
- package/src/theme.js +1 -1
- package/src/timeline-fps.js +13 -0
- 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 +110 -20
- 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/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 +96 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-C1sdVbg4.css +0 -1
- package/dist/assets/app-CA55tcI3.js +0 -4876
- package/dist/assets/shot-prompt-Yrt99wZx.js +0 -17
- package/dist/assets/workflow-dNPGY9xz.js +0 -179
package/src/domains/objects.js
CHANGED
|
@@ -20,9 +20,15 @@ import {
|
|
|
20
20
|
duplicateCutoutOptions,
|
|
21
21
|
MESH_KIND,
|
|
22
22
|
duplicateMeshOptions,
|
|
23
|
+
createSculptObject,
|
|
24
|
+
duplicateSculptOptions,
|
|
25
|
+
SCULPT_KIND,
|
|
23
26
|
objectSize,
|
|
24
27
|
setSceneObjectAttach,
|
|
25
28
|
setSceneObjectParent,
|
|
29
|
+
rigidMotionBetween,
|
|
30
|
+
carryPointByMotion,
|
|
31
|
+
tidyAngle,
|
|
26
32
|
} from "../scene-objects.js";
|
|
27
33
|
|
|
28
34
|
import { ko, isKo } from "../locale.js";
|
|
@@ -41,6 +47,7 @@ import { importMeshFile, compressedGlbReason, meshBoundsFromAsset, fitMeshBounds
|
|
|
41
47
|
import { cutOutBackground, maskAsset } from "../matte.js";
|
|
42
48
|
import { parseRigNodeId } from "../hierarchy-model.js";
|
|
43
49
|
import { StudioProtocolError } from "../studio-agent-protocol.js";
|
|
50
|
+
import { assetDataUrl, sculptPlacement } from "../sculpt-mode.js";
|
|
44
51
|
|
|
45
52
|
// A stable owner across scene loads. App's remaining native history readers use
|
|
46
53
|
// the small store facade below; authored writes are owned by the document store.
|
|
@@ -55,12 +62,50 @@ export function createObjectsDomain(appContext, initial) {
|
|
|
55
62
|
};
|
|
56
63
|
const read = () => documentStore.read("objects");
|
|
57
64
|
function write(update) {
|
|
58
|
-
|
|
65
|
+
let moved = null;
|
|
66
|
+
const result = documentStore.write("objects", before => {
|
|
59
67
|
let next = typeof update === "function" ? update(before) : update;
|
|
60
68
|
for (const row of next.filter(row => row.remove === true)) next = removeSceneObject(next, row.id);
|
|
61
69
|
if (JSON.stringify(next) === JSON.stringify(before)) return before;
|
|
70
|
+
moved = { before, next };
|
|
62
71
|
return next;
|
|
63
72
|
});
|
|
73
|
+
if (moved) carryGroupedCharacters(moved.before, moved.next);
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
/** Characters grouped under an object ride it while authoring too: whatever
|
|
77
|
+
* motion an object took in this write (its own move or turn, or carried with
|
|
78
|
+
* its group), the characters under it take the same. A pure move shifts
|
|
79
|
+
* them by the same translation. A turn orbits them about the object's pivot
|
|
80
|
+
* (position by the object's Δ, like any carried child) and adds the yaw
|
|
81
|
+
* part of Δ to their facing — a character has only a yaw, so the pitch or
|
|
82
|
+
* roll of its parent moves where it stands and never tips it over.
|
|
83
|
+
* Joined to the open action, so one undo puts back the object and the
|
|
84
|
+
* riders together. */
|
|
85
|
+
function carryGroupedCharacters(before, next) {
|
|
86
|
+
const cast = appContext.storeDomain("cast");
|
|
87
|
+
if (!cast) return;
|
|
88
|
+
const was = new Map(before.map(row => [row.id, row]));
|
|
89
|
+
const shifts = new Map();
|
|
90
|
+
const turns = new Map();
|
|
91
|
+
for (const row of next) {
|
|
92
|
+
const prior = was.get(row.id);
|
|
93
|
+
if (!prior || row.attach || prior.attach) continue;
|
|
94
|
+
const turn = rigidMotionBetween(prior, row);
|
|
95
|
+
if (turn) { turns.set(row.id, turn); continue; }
|
|
96
|
+
const shift = { x: row.x - prior.x, y: (row.y ?? 0) - (prior.y ?? 0), z: row.z - prior.z };
|
|
97
|
+
if (shift.x || shift.y || shift.z) shifts.set(row.id, shift);
|
|
98
|
+
}
|
|
99
|
+
if (!(shifts.size || turns.size) || !cast.read().some(entry => shifts.has(entry.parent) || turns.has(entry.parent))) return;
|
|
100
|
+
appContext.recordAction("cast", () => cast.write(rows => rows.map(entry => {
|
|
101
|
+
const turn = turns.get(entry.parent);
|
|
102
|
+
if (turn) {
|
|
103
|
+
const at = carryPointByMotion({ x: entry.x, y: entry.y ?? 0, z: entry.z }, turn);
|
|
104
|
+
return { ...entry, x: at.x, y: Math.max(0, at.y), z: at.z, rot: tidyAngle((entry.rot ?? 0) + turn.yaw) };
|
|
105
|
+
}
|
|
106
|
+
const shift = shifts.get(entry.parent);
|
|
107
|
+
return shift ? { ...entry, x: entry.x + shift.x, y: Math.max(0, (entry.y ?? 0) + shift.y), z: entry.z + shift.z } : entry;
|
|
108
|
+
})), null, true);
|
|
64
109
|
}
|
|
65
110
|
const publish = () => { if (appContext.live.state) appContext.patchLive({ objects: read() }); };
|
|
66
111
|
const unsubscribe = documentStore.subscribe(publish);
|
|
@@ -242,6 +287,10 @@ export function useObjects(appContext) {
|
|
|
242
287
|
|
|
243
288
|
const [gizmoMode, setGizmoMode] = useState("move");
|
|
244
289
|
|
|
290
|
+
// An Empty is nothing but its name: the moment one is made, its Outliner
|
|
291
|
+
// row opens for naming (a nonce, so a second Empty re-opens it).
|
|
292
|
+
const [renameRequest, setRenameRequest] = useState(null);
|
|
293
|
+
|
|
245
294
|
// Snap is a preference, not a law: with it on the gizmo blocks on the plan
|
|
246
295
|
// board's grid, and Ctrl/Cmd during a drag gives a free one. Off, it is the
|
|
247
296
|
// other way round. (docs/unity-reference.md §9.5)
|
|
@@ -255,10 +304,11 @@ export function useObjects(appContext) {
|
|
|
255
304
|
const placement = at ?? (camera
|
|
256
305
|
? placementInFront({ x: camera.position.x, z: camera.position.z }, paneYaw)
|
|
257
306
|
: {});
|
|
258
|
-
const receipt = run("object.add", { kind, placement });
|
|
307
|
+
const receipt = run("object.add", { kind, placement, first: true });
|
|
259
308
|
const object = domain.read().find(row => row.id === receipt.affectedIds[0]);
|
|
260
309
|
appContext.shared.markCraftAction("object");
|
|
261
310
|
appContext.shared.setSelectedHierarchyId(`object:${object.id}`);
|
|
311
|
+
if (object.renderer === "empty") setRenameRequest({ id: `object:${object.id}`, nonce: Date.now() });
|
|
262
312
|
// Deliberate divergence from Unity's rename-on-create: creating an object
|
|
263
313
|
// here is followed by placing it, and dropping focus into a text field
|
|
264
314
|
// swallows the very next W/E/R. Renaming stays on F2/Return and the row's
|
|
@@ -385,6 +435,25 @@ export function useObjects(appContext) {
|
|
|
385
435
|
}
|
|
386
436
|
}
|
|
387
437
|
|
|
438
|
+
/** Sculpt mode's Place in scene (#739): the sculpt stands on the ground in
|
|
439
|
+
* front of the scene camera, where + Add puts a new object, and ends up
|
|
440
|
+
* selected. One object.update, so one undo. */
|
|
441
|
+
function placeSculptInScene(id) {
|
|
442
|
+
const camera = (appContext.shared.lookThroughShot ? appContext.shared.shotCamRef : appContext.shared.editorCamRef).current;
|
|
443
|
+
const yaw = (appContext.shared.lookThroughShot ? appContext.shared.look : appContext.shared.editorLook).current.yaw;
|
|
444
|
+
const receipt = run("object.update", { id, patch: camera ? sculptPlacement(camera.position, yaw) : { y: 0 } });
|
|
445
|
+
appContext.shared.setSelectedHierarchyId(`object:${id}`);
|
|
446
|
+
return receipt;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** A sculpt reference goes into the asset store like a cutout's picture;
|
|
450
|
+
* the run reads it back as a data URL of the stored bytes. */
|
|
451
|
+
async function storeSculptReference(file) {
|
|
452
|
+
const prepared = await importImageFile(file);
|
|
453
|
+
const stored = await rememberAsset(prepared);
|
|
454
|
+
return { dataUrl: assetDataUrl(prepared), name: prepared.name || file.name || "reference", assetId: stored.id };
|
|
455
|
+
}
|
|
456
|
+
|
|
388
457
|
function placementInFrontOfShot() {
|
|
389
458
|
const camera = (appContext.shared.lookThroughShot ? appContext.shared.shotCamRef : appContext.shared.editorCamRef).current;
|
|
390
459
|
return camera
|
|
@@ -528,7 +597,9 @@ export function useObjects(appContext) {
|
|
|
528
597
|
? createCutoutObject(duplicateCutoutOptions(object), objects, placement)
|
|
529
598
|
: object.renderer === MESH_KIND
|
|
530
599
|
? createMeshObject(duplicateMeshOptions(object), objects, placement)
|
|
531
|
-
:
|
|
600
|
+
: object.renderer === SCULPT_KIND
|
|
601
|
+
? createSculptObject(duplicateSculptOptions(object), objects, placement).object
|
|
602
|
+
: createSceneObject(object.renderer, objects, placement);
|
|
532
603
|
if (!copy) return;
|
|
533
604
|
// Unity drops the duplicate exactly on top of the original; for blocking,
|
|
534
605
|
// one grid step to the side means you can see that it worked.
|
|
@@ -548,6 +619,25 @@ export function useObjects(appContext) {
|
|
|
548
619
|
);
|
|
549
620
|
}
|
|
550
621
|
|
|
622
|
+
/** Outliner "Group under new Empty": the object's parent node, made on the
|
|
623
|
+
* spot. One command, one write, so one undo takes back the Empty and the
|
|
624
|
+
* reparent together. Selection follows the Empty, the new handle. */
|
|
625
|
+
function groupObjectUnderNewEmpty(id = selectedSceneObjectId) {
|
|
626
|
+
if (!id) return null;
|
|
627
|
+
try {
|
|
628
|
+
const receipt = run("object.groupUnderEmpty", { id });
|
|
629
|
+
const emptyId = receipt.affectedIds[0];
|
|
630
|
+
appContext.shared.setSelectedHierarchyId(`object:${emptyId}`);
|
|
631
|
+
setRenameRequest({ id: `object:${emptyId}`, nonce: Date.now() });
|
|
632
|
+
setGizmoMode("move");
|
|
633
|
+
appContext.notify(ko("Grouped under a new Empty", "빈 오브젝트로 묶었어요"));
|
|
634
|
+
return emptyId;
|
|
635
|
+
} catch (error) {
|
|
636
|
+
appContext.notify(error?.message || ko("Could not group this object", "이 오브젝트는 묶을 수 없어요"));
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
551
641
|
/** In-place rename commit from the hierarchy (F2 / Return / rename on
|
|
552
642
|
* create). The row label lives in the tree; the object name is shared
|
|
553
643
|
* state, so this is just the inspector's rename through another door. */
|
|
@@ -878,7 +968,9 @@ export function useObjects(appContext) {
|
|
|
878
968
|
// Grouping keeps its own rules (self, cycles, unknown ids) — asking the
|
|
879
969
|
// store is the only way to stay honest about them.
|
|
880
970
|
if (targetObjectId) return setSceneObjectParent(sceneObjects, id, targetObjectId) !== sceneObjects;
|
|
881
|
-
|
|
971
|
+
// Props takes an object out of its group (or off a character) and lists
|
|
972
|
+
// it first, so the drop always shows; a top-level object moves up too.
|
|
973
|
+
if (targetRowId === "props") return (object.attach ?? null) !== null || (object.parent ?? null) !== null || sceneObjects[0]?.id !== id;
|
|
882
974
|
const attach = attachTargetForRow(targetRowId);
|
|
883
975
|
if (!attach) return false;
|
|
884
976
|
const current = object.attach ?? null;
|
|
@@ -894,6 +986,14 @@ export function useObjects(appContext) {
|
|
|
894
986
|
// group comes back to world numbers on the way, exactly as the Props
|
|
895
987
|
// row would put it back.
|
|
896
988
|
if (targetObjectId) return run("object.group", { parent: targetObjectId, children: [id] });
|
|
989
|
+
const object = sceneObjects.find((entry) => entry.id === id);
|
|
990
|
+
if (targetRowId === "props" && !object?.attach) {
|
|
991
|
+
// Out of its group and to the top of Props in one write (one undo):
|
|
992
|
+
// records are world-space, so clearing the parent moves nothing.
|
|
993
|
+
const rows = setSceneObjectParent(domain.read(), id, null);
|
|
994
|
+
const moved = rows.find((entry) => entry.id === id);
|
|
995
|
+
return run("objects.replace", { objects: [moved, ...rows.filter((entry) => entry.id !== id)] });
|
|
996
|
+
}
|
|
897
997
|
const attach = targetRowId === "props" ? null : attachTargetForRow(targetRowId);
|
|
898
998
|
appContext.shared.runStudioAction(attach ? "object.attach" : "object.detach", attach
|
|
899
999
|
? { objectId: id, characterId: attach.characterId, ...(attach.bone ? { bone: attach.bone } : {}) }
|
|
@@ -926,7 +1026,8 @@ export function useObjects(appContext) {
|
|
|
926
1026
|
matteBrush, setMatteBrush, matteShrink, setMatteShrink, matteFeather, setMatteFeather, matteMode,
|
|
927
1027
|
setMatteMode, matteStats, setMatteStats, matteBusy, gizmoMode, setGizmoMode, snapEnabled, setSnapEnabled,
|
|
928
1028
|
addSceneObject, importCutout, importCutouts, spawnCutoutAt, persistMeshAsset, importMesh, importMeshes,
|
|
929
|
-
spawnMeshAt, applyMatte, duplicateSelectedSceneObject, frameSelection, renameSceneObject,
|
|
1029
|
+
spawnMeshAt, applyMatte, duplicateSelectedSceneObject, frameSelection, groupObjectUnderNewEmpty, renameRequest, renameSceneObject,
|
|
930
1030
|
sceneObjectWorldMatrix, attachTargetForRow, attachTargetLabel, attachSceneObject,
|
|
1031
|
+
placeSculptInScene, storeSculptReference,
|
|
931
1032
|
};
|
|
932
1033
|
}
|
package/src/domains/scenes.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
readSceneDocument,
|
|
8
8
|
serializeSceneDocument,
|
|
9
9
|
SCENES_VERSION,
|
|
10
|
-
|
|
10
|
+
migrateScenesDocument as migrateToCurrentScenes,
|
|
11
11
|
activeSceneIndex,
|
|
12
12
|
createSceneDocument,
|
|
13
13
|
createSceneStage,
|
|
@@ -37,6 +37,9 @@ import {
|
|
|
37
37
|
requestHandlePermission,
|
|
38
38
|
createWorkflowGraph,
|
|
39
39
|
clearStoredProjectHandle,
|
|
40
|
+
DEFAULT_PREVIS_MODE,
|
|
41
|
+
normalizePrevisMode,
|
|
42
|
+
buildAnimationProjectFromStoryboard,
|
|
40
43
|
} from "../project.js";
|
|
41
44
|
import { playgroundSceneUrl, fetchSceneProject } from "../playground.js";
|
|
42
45
|
import { openAssetDb, referencedAssetIds, getAsset, putAsset } from "../scene-assets.js";
|
|
@@ -45,7 +48,7 @@ import { encodeMotionResource } from "../motion-resources.js";
|
|
|
45
48
|
import { openMotionDb, putMotion, sweepMotions } from "../motion-store.js";
|
|
46
49
|
import { resourceManifest } from "../project-resources.js";
|
|
47
50
|
import { isKo, ko } from "../locale.js";
|
|
48
|
-
import { track, bucketCount, bucketProjectAge } from "../analytics.js";
|
|
51
|
+
import { track, bucketCount, bucketProjectAge, startExportAttempt } from "../analytics.js";
|
|
49
52
|
import { mergeProjectCustomPoses } from "../project-poses.js";
|
|
50
53
|
import { DEFAULT_WORKSPACE_LAYOUT, DEFAULT_DURATION_S, TIMELINE_FPS } from "../app-stage.jsx";
|
|
51
54
|
import { saveCustomPoses } from "../poses.js";
|
|
@@ -55,9 +58,11 @@ import { createIkState } from "../ardy/ik.js";
|
|
|
55
58
|
|
|
56
59
|
// The scene list and project identity share one history. Dirty is derived from
|
|
57
60
|
// the saved checkpoint, not an authored edit, and has its own non-history slice.
|
|
58
|
-
|
|
61
|
+
// previsMode is project identity too: it is set only by a document boundary
|
|
62
|
+
// (blank creation, starter/file open, session restore), never by an edit.
|
|
63
|
+
function createScenesDomain(appContext, initial, name, previsMode = DEFAULT_PREVIS_MODE) {
|
|
59
64
|
const ordered = rows => rows.map((scene, order) => ({ ...scene, order }));
|
|
60
|
-
let native = createDocumentStore({ owned: { scenes: ordered(initial.scenes), project: { name, activeSceneId: initial.activeSceneId } } });
|
|
65
|
+
let native = createDocumentStore({ owned: { scenes: ordered(initial.scenes), project: { name, activeSceneId: initial.activeSceneId, previsMode: normalizePrevisMode(previsMode) } } });
|
|
61
66
|
const listeners = new Set();
|
|
62
67
|
const notify = () => { for (const listener of listeners) listener(); };
|
|
63
68
|
let release = native.subscribe(notify);
|
|
@@ -93,7 +98,7 @@ export function createScenesDomain(appContext, initial, name) {
|
|
|
93
98
|
appContext.publishScenes(read());
|
|
94
99
|
appContext.shared.activeSceneIdRef.current = metadata().activeSceneId;
|
|
95
100
|
if (appContext.live.state) appContext.patchLive({ scenes: read(), activeSceneId: metadata().activeSceneId });
|
|
96
|
-
storeProjectSession(metadata().name);
|
|
101
|
+
storeProjectSession(metadata().name, metadata().previsMode);
|
|
97
102
|
domain.persist?.();
|
|
98
103
|
domain.refreshDirty?.();
|
|
99
104
|
};
|
|
@@ -117,9 +122,9 @@ export function createScenesDomain(appContext, initial, name) {
|
|
|
117
122
|
},
|
|
118
123
|
// Session cache/file loads are explicit non-authored boundaries. They
|
|
119
124
|
// retire scene history, just as opening a scene retired native history.
|
|
120
|
-
replaceDocument(scenes, activeSceneId, name = metadata().name) {
|
|
125
|
+
replaceDocument(scenes, activeSceneId, name = metadata().name, previsMode = metadata().previsMode) {
|
|
121
126
|
release(); native.dispose();
|
|
122
|
-
native = createDocumentStore({ owned: { scenes: ordered(scenes), project: { name, activeSceneId } } });
|
|
127
|
+
native = createDocumentStore({ owned: { scenes: ordered(scenes), project: { name, activeSceneId, previsMode: normalizePrevisMode(previsMode) } } });
|
|
123
128
|
release = native.subscribe(notify); notify();
|
|
124
129
|
},
|
|
125
130
|
setDirty(value) {
|
|
@@ -135,9 +140,13 @@ export function createScenesDomain(appContext, initial, name) {
|
|
|
135
140
|
}
|
|
136
141
|
|
|
137
142
|
export function useScenes(appContext) {
|
|
138
|
-
const [domain] = useState(() =>
|
|
143
|
+
const [domain] = useState(() => {
|
|
144
|
+
if (appContext.storeDomain("scenes")) return appContext.storeDomain("scenes");
|
|
145
|
+
const session = loadProjectSession();
|
|
146
|
+
return createScenesDomain(appContext, appContext.shared.startup.document, session?.name ?? null, session?.previsMode);
|
|
147
|
+
});
|
|
139
148
|
const scenes = useDocumentDomain(domain.documentStore, "scenes");
|
|
140
|
-
const { activeSceneId, name: projectName } = useDocumentDomain(domain.documentStore, "project");
|
|
149
|
+
const { activeSceneId, name: projectName, previsMode } = useDocumentDomain(domain.documentStore, "project");
|
|
141
150
|
const projectDirty = useDocumentDomain(domain.dirtyStore, "projectDirty");
|
|
142
151
|
const runtimeValues = useRef(new Map());
|
|
143
152
|
async function runProject(id, args = {}) {
|
|
@@ -185,8 +194,7 @@ export function useScenes(appContext) {
|
|
|
185
194
|
}
|
|
186
195
|
}
|
|
187
196
|
|
|
188
|
-
/*
|
|
189
|
-
* Game-engine workflow: the authoring state (scenes + cast + layers,
|
|
197
|
+
/* Game-engine workflow: the authoring state (scenes + cast + layers,
|
|
190
198
|
* workspace layout, custom poses) round-trips through a real
|
|
191
199
|
* `.cclayproject` file. localStorage stays as the always-on session
|
|
192
200
|
* cache; the file is the portable, user-owned document. */
|
|
@@ -222,6 +230,7 @@ export function useScenes(appContext) {
|
|
|
222
230
|
customPoses: appContext.shared.projectStateRef.current.customPoses,
|
|
223
231
|
workflow: loadWorkflowGraph(),
|
|
224
232
|
name,
|
|
233
|
+
previsMode: domain.metadata().previsMode,
|
|
225
234
|
};
|
|
226
235
|
}
|
|
227
236
|
|
|
@@ -296,7 +305,7 @@ export function useScenes(appContext) {
|
|
|
296
305
|
appContext.shared.projectSnapshotRef.current = snapshot;
|
|
297
306
|
if (domain.metadata().name === previousName) domain.nameSaved(name);
|
|
298
307
|
refreshProjectDirty();
|
|
299
|
-
storeProjectSession(domain.metadata().name);
|
|
308
|
+
storeProjectSession(domain.metadata().name, domain.metadata().previsMode);
|
|
300
309
|
}
|
|
301
310
|
|
|
302
311
|
function projectProblemsNotice(problems) {
|
|
@@ -391,6 +400,40 @@ export function useScenes(appContext) {
|
|
|
391
400
|
}
|
|
392
401
|
}
|
|
393
402
|
|
|
403
|
+
async function exportAsAnimationProject(context) {
|
|
404
|
+
if (domain.metadata().previsMode !== "storyboard") return { saved: false, failure: "not-storyboard" };
|
|
405
|
+
const attempt = startExportAttempt({ export_kind: "animation_project", format: "cclayproject", surface: "studio" });
|
|
406
|
+
try {
|
|
407
|
+
const source = readProjectDocument(await collectProjectSerialized(domain.metadata().name ?? "Untitled"));
|
|
408
|
+
context.check();
|
|
409
|
+
if (!source.ok) throw new Error(`Could not read the storyboard project: ${source.reason}`);
|
|
410
|
+
const animation = buildAnimationProjectFromStoryboard(source.project);
|
|
411
|
+
const serialized = JSON.stringify(animation, null, 2);
|
|
412
|
+
let downloaded = false;
|
|
413
|
+
if (hasFileSystemAccess()) {
|
|
414
|
+
const handle = await pickProjectFileForSave(animation.name);
|
|
415
|
+
context.check();
|
|
416
|
+
await writeProjectFile(handle, serialized);
|
|
417
|
+
await rememberRecentProject(handle, animation.name);
|
|
418
|
+
} else {
|
|
419
|
+
downloadProjectFallback(serialized, animation.name);
|
|
420
|
+
downloaded = true;
|
|
421
|
+
}
|
|
422
|
+
attempt.succeed();
|
|
423
|
+
return {
|
|
424
|
+
saved: true,
|
|
425
|
+
name: animation.name,
|
|
426
|
+
fileName: `${animation.name}${PROJECT_EXTENSION}`,
|
|
427
|
+
downloaded,
|
|
428
|
+
};
|
|
429
|
+
} catch (error) {
|
|
430
|
+
attempt.fail(error, error?.name === "AbortError" ? "aborted" : "unknown");
|
|
431
|
+
if (error?.name === "AbortError") return { saved: false, cancelled: true };
|
|
432
|
+
appContext.notify(ko("Could not export the animation project", "애니메이션 프로젝트를 내보내지 못했어요"));
|
|
433
|
+
return { saved: false, failure: error?.code ?? error?.name ?? "error" };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
394
437
|
function applyProject(project, authorized = false) {
|
|
395
438
|
if (!authorized) return runWithValue("project.open", "projectToken", project);
|
|
396
439
|
const checked = readSceneDocument(JSON.stringify(project.scenesDocument));
|
|
@@ -408,11 +451,10 @@ export function useScenes(appContext) {
|
|
|
408
451
|
// A project FILE carries its own scene document and never passes the
|
|
409
452
|
// storage reader, so the 20 fps → 24 fps clock migration is applied here
|
|
410
453
|
// too — otherwise an older .cozyclay would open a sixth too fast.
|
|
411
|
-
const doc =
|
|
412
|
-
? { ...source, version: SCENES_VERSION, scenes: source.scenes.map((scene) => ({ ...scene, stage: migrateStageFrames(scene.stage) })) }
|
|
413
|
-
: source;
|
|
454
|
+
const doc = migrateToCurrentScenes(source);
|
|
414
455
|
const mergedCustomPoses = mergeProjectCustomPoses(appContext.shared.customPoses, project.customPoses);
|
|
415
|
-
|
|
456
|
+
const projectPrevisMode = normalizePrevisMode(project.previsMode);
|
|
457
|
+
domain.replaceDocument(doc.scenes, doc.activeSceneId, project.name, projectPrevisMode);
|
|
416
458
|
if (project.workspaceLayout) appContext.shared.setWorkspaceLayout({ ...DEFAULT_WORKSPACE_LAYOUT, ...project.workspaceLayout });
|
|
417
459
|
appContext.bus.run('cast.setCustomPoses', { poses: mergedCustomPoses });
|
|
418
460
|
const resolvedWorkflow = resolveWorkflowOutputs(normalizeWorkflowGraph(project.workflow), new Map((project.assets ?? []).map((asset) => [asset.id, asset])));
|
|
@@ -423,7 +465,7 @@ export function useScenes(appContext) {
|
|
|
423
465
|
appContext.shared.projectSnapshotRef.current = collectProjectSnapshot(project.name);
|
|
424
466
|
domain.pendingCheckpoint = { clock: appContext.undoClock, name: project.name };
|
|
425
467
|
domain.setDirty(false);
|
|
426
|
-
storeProjectSession(project.name);
|
|
468
|
+
storeProjectSession(project.name, projectPrevisMode);
|
|
427
469
|
setProjectStartupOpen(false);
|
|
428
470
|
// Whatever document this is, it is no longer the scene the tutorial opened
|
|
429
471
|
// for itself; startCameraTutorial re-arms the flag after its own open.
|
|
@@ -435,7 +477,8 @@ export function useScenes(appContext) {
|
|
|
435
477
|
/** Open a bundled starter scene as a fresh, saveable project. Used by the
|
|
436
478
|
* first-run dialog and by `npx cozyclay --scene <id>` (`?scene=`), which is
|
|
437
479
|
* how the landing-page tutorial hands people into the local studio. */
|
|
438
|
-
async function openStarterScene(id, source = "starter", context = null) {
|
|
480
|
+
async function openStarterScene(id, source = "starter", context = null, name = null) {
|
|
481
|
+
if (source === "starter" && domain.dirtyStore.read("projectDirty") && !window.confirm(ko("Discard unsaved changes and start a new project?", "저장되지 않은 변경사항을 버리고 새 프로젝트를 시작할까요?"))) return false;
|
|
439
482
|
const before = source === "tutorial" ? collectProjectSnapshot("Tutorial") : null;
|
|
440
483
|
const epoch = appContext.shared.tutorialProjectEpochRef.current;
|
|
441
484
|
const url = playgroundSceneUrl(`?scene=${encodeURIComponent(id)}`);
|
|
@@ -448,7 +491,7 @@ export function useScenes(appContext) {
|
|
|
448
491
|
return false;
|
|
449
492
|
}
|
|
450
493
|
context?.check();
|
|
451
|
-
applyProject({ ...project, savedAt: null }, true);
|
|
494
|
+
applyProject({ ...project, name: name?.trim() || project.name, savedAt: null }, true);
|
|
452
495
|
appContext.shared.projectHandleRef.current = null;
|
|
453
496
|
track("scene:loaded", { scene_source: source });
|
|
454
497
|
return true;
|
|
@@ -524,27 +567,36 @@ export function useScenes(appContext) {
|
|
|
524
567
|
}
|
|
525
568
|
}
|
|
526
569
|
|
|
527
|
-
function requestNewProject(authorized = false) {
|
|
528
|
-
if (!authorized) return runProject("project.new");
|
|
570
|
+
function requestNewProject(authorized = false, { previsMode } = {}) {
|
|
571
|
+
if (!authorized) return runProject("project.new", previsMode === undefined ? undefined : { previsMode });
|
|
529
572
|
if (domain.dirtyStore.read("projectDirty") && !window.confirm(ko("Discard unsaved changes and start a new project?", "저장되지 않은 변경사항을 버리고 새 프로젝트를 시작할까요?"))) return;
|
|
530
|
-
|
|
573
|
+
// The mode chosen on the start screen rides on the name dialog until its
|
|
574
|
+
// submit commits the project.
|
|
575
|
+
setProjectNameDialog({ kind: "new", initialName: projectName ?? "My Project", ...(previsMode === undefined ? {} : { previsMode }) });
|
|
531
576
|
}
|
|
532
577
|
|
|
533
|
-
function newProject(name, authorized = false) {
|
|
534
|
-
if (!authorized)
|
|
535
|
-
|
|
578
|
+
function newProject(name, authorized = false, { previsMode } = {}) {
|
|
579
|
+
if (!authorized) {
|
|
580
|
+
if (previsMode === undefined && projectNameDialog?.kind === "new") previsMode = projectNameDialog.previsMode;
|
|
581
|
+
return runProject("project.new", { ...(typeof name === "string" ? { name } : {}), ...(previsMode === undefined ? {} : { previsMode }) });
|
|
582
|
+
}
|
|
583
|
+
if (typeof name !== "string") return requestNewProject(true, { previsMode });
|
|
536
584
|
name = name.trim() || "My Project";
|
|
585
|
+
previsMode = normalizePrevisMode(previsMode);
|
|
537
586
|
setProjectNameDialog(null);
|
|
538
587
|
const fresh = createSceneDocument(ko("SCENE 01", "씬 01"));
|
|
539
588
|
storeWorkflowGraph(createWorkflowGraph());
|
|
540
|
-
|
|
589
|
+
appContext.shared.projectMotionsRef.current = new Map();
|
|
590
|
+
appContext.shared.motionFullRef.current.clear();
|
|
591
|
+
domain.replaceDocument(fresh.scenes, fresh.activeSceneId, name, previsMode);
|
|
541
592
|
persistScenes(fresh.scenes, fresh.activeSceneId);
|
|
542
593
|
openScene(fresh.scenes[0], fresh.scenes);
|
|
543
594
|
appContext.shared.projectHandleRef.current = null;
|
|
544
595
|
clearStoredProjectHandle();
|
|
545
596
|
appContext.shared.projectSnapshotRef.current = collectProjectSnapshot(name);
|
|
546
597
|
domain.setDirty(false);
|
|
547
|
-
storeProjectSession(name);
|
|
598
|
+
storeProjectSession(name, previsMode);
|
|
599
|
+
track("scene:created", { scene_source: "new_project", previs_mode: previsMode });
|
|
548
600
|
setProjectStartupOpen(false);
|
|
549
601
|
appContext.shared.setFirstSuccessGuideOpen(true);
|
|
550
602
|
domain.pendingCheckpoint = { clock: appContext.undoClock, name };
|
|
@@ -658,7 +710,7 @@ export function useScenes(appContext) {
|
|
|
658
710
|
const target = nextScenes[nextScenes.length - 1];
|
|
659
711
|
persistScenes(nextScenes, target.id);
|
|
660
712
|
openScene(target, nextScenes);
|
|
661
|
-
track("scene:created", { scene_source: "ui" });
|
|
713
|
+
track("scene:created", { scene_source: "ui", previs_mode: domain.metadata().previsMode });
|
|
662
714
|
}
|
|
663
715
|
|
|
664
716
|
function duplicateSceneDocument(sceneId) {
|
|
@@ -763,6 +815,9 @@ export function useScenes(appContext) {
|
|
|
763
815
|
domain.pendingCheckpoint = null;
|
|
764
816
|
if (checkpoint.clock === appContext.undoClock && checkpoint.name === name) appContext.shared.projectSnapshotRef.current = collectProjectSnapshot(name);
|
|
765
817
|
}
|
|
818
|
+
if (name !== null && !appContext.shared.projectSnapshotRef.current) {
|
|
819
|
+
appContext.shared.projectSnapshotRef.current = collectProjectSnapshot(name);
|
|
820
|
+
}
|
|
766
821
|
const dirty = name !== null && collectProjectSnapshot(name) !== appContext.shared.projectSnapshotRef.current;
|
|
767
822
|
domain.setDirty(dirty);
|
|
768
823
|
setProjectSaveState((current) => current === "saving" ? current : dirty ? "dirty" : "saved");
|
|
@@ -772,6 +827,7 @@ export function useScenes(appContext) {
|
|
|
772
827
|
domain.fileState = () => ({ name: domain.metadata().name, hasFile: Boolean(appContext.shared.projectHandleRef.current),
|
|
773
828
|
fileAccess: hasFileSystemAccess(), gesture: globalThis.navigator?.userActivation?.isActive === true });
|
|
774
829
|
domain.save = (args, context) => saveProject(args.saveAs, args.name ?? null, context);
|
|
830
|
+
domain.exportAsAnimationProject = context => exportAsAnimationProject(context);
|
|
775
831
|
domain.loadScenes = args => loadLiveScenes(args, true);
|
|
776
832
|
domain.projectAction = async (id, args, context) => {
|
|
777
833
|
const runtime = key => {
|
|
@@ -781,8 +837,8 @@ export function useScenes(appContext) {
|
|
|
781
837
|
return value;
|
|
782
838
|
};
|
|
783
839
|
if (id === "project.browse") { setProjectStartupOpen(false); setProjectBrowserOpen(true); return; }
|
|
784
|
-
if (id === "project.new") return newProject(args.name, true);
|
|
785
|
-
if (id === "project.openStarter") return openStarterScene(args.id, args.source, context);
|
|
840
|
+
if (id === "project.new") return newProject(args.name, true, { previsMode: args.previsMode });
|
|
841
|
+
if (id === "project.openStarter") return openStarterScene(args.id, args.source, context, args.name);
|
|
786
842
|
if (id === "project.restore") return restoreStoredProject({ handle: runtime("handleToken") }, context);
|
|
787
843
|
if (args.handleToken) return openProjectByHandle(runtime("handleToken"), context);
|
|
788
844
|
if (args.projectToken) { applyProject(runtime("projectToken"), true); return true; }
|
|
@@ -801,13 +857,14 @@ export function useScenes(appContext) {
|
|
|
801
857
|
domain.refreshDirty = () => refreshProjectDirty(false);
|
|
802
858
|
return {
|
|
803
859
|
...domain, applyExternalScene, loadLiveScenes, refreshProjectDirty,
|
|
804
|
-
scenes, activeSceneId, sceneSaveError, snapshotActiveScene, persistScenes, projectName,
|
|
860
|
+
scenes, activeSceneId, sceneSaveError, snapshotActiveScene, persistScenes, projectName, previsMode,
|
|
805
861
|
projectDirty, setProjectDirty: domain.setDirty, projectSaveState, setProjectSaveState, projectMenuOpen,
|
|
806
862
|
setProjectMenuOpen, projectBrowserOpen, setProjectBrowserOpen, projectNameDialog, setProjectNameDialog,
|
|
807
863
|
projectStartupOpen, setProjectStartupOpen, projectManifest, setProjectManifest, saveBlockedReasons,
|
|
808
864
|
setSaveBlockedReasons, workflowRevision, setWorkflowRevision, collectProjectSnapshot,
|
|
809
865
|
collectProjectSerialized, projectProblemsNotice, rehydrateProjectAssets, saveProject, applyProject,
|
|
810
|
-
|
|
866
|
+
exportAsAnimationProject,
|
|
867
|
+
openStarterScene: async (id, source = "starter", name = null) => (await runProject("project.openStarter", { id, source, ...(name ? { name } : {}) })).output?.opened === true,
|
|
811
868
|
openProject, openProjectByHandle, requestNewProject, newProject, restoreOffer,
|
|
812
869
|
setRestoreOffer, restoreStoredProject, flushScenes, openScene, selectSceneDocument,
|
|
813
870
|
createSceneDocumentFromUi, duplicateSceneDocumentFromUi, renameSceneDocumentFromUi,
|
package/src/domains/shots.js
CHANGED
|
@@ -30,7 +30,7 @@ function normalizeState(value) {
|
|
|
30
30
|
|
|
31
31
|
// The stable handle survives scene loads. Camera pose belongs to the same
|
|
32
32
|
// history entry as the shot it frames; only the persisted shots are projected.
|
|
33
|
-
|
|
33
|
+
function createShotsDomain(appContext, initial = {}) {
|
|
34
34
|
const initialState = value => normalizeState({ shots: [], frameCount: DEFAULT_DURATION_S * TIMELINE_FPS,
|
|
35
35
|
cameraMove: CAMERA_MOVES[1], fovDeg: PRESETS.medium.fov, camera: null, manual: false, ...value });
|
|
36
36
|
let native = createDocumentStore({ owned: { shot: initialState(initial) } });
|
|
@@ -140,6 +140,25 @@ export function createShotsDomain(appContext, initial = {}) {
|
|
|
140
140
|
const before = state(), filmback = appContext.live.state.filmback;
|
|
141
141
|
writeState({ ...before, fovDeg, camera: before.camera && { ...before.camera, focalMm: fovToFocalMm(fovDeg * Math.PI / 180, filmback.sensorId, filmback.aspectRatio) } });
|
|
142
142
|
}
|
|
143
|
+
function setCastOverride(shotId, characterId, override) {
|
|
144
|
+
if (!read().some(shot => shot.id === shotId)) throw new StudioProtocolError('STALE_TARGET', `Shot ${shotId} is not in this scene.`);
|
|
145
|
+
if (!appContext.live.characters.some(entry => entry.id === characterId)) throw new StudioProtocolError('STALE_TARGET', `Character ${characterId} is not in this scene.`);
|
|
146
|
+
if (override !== null && (!override || !Number.isFinite(override.x) || !Number.isFinite(override.z) || !Number.isFinite(override.rot)
|
|
147
|
+
|| (override.posture !== undefined && !['stand', 'sit', 'lie'].includes(override.posture))
|
|
148
|
+
|| (override.pose !== undefined && (!override.pose || typeof override.pose !== 'object' || Array.isArray(override.pose))))) {
|
|
149
|
+
throw new StudioProtocolError('INVALID_ARGUMENT', 'Cast override requires finite x, z and rot and valid posture/pose fields.');
|
|
150
|
+
}
|
|
151
|
+
return writeState(before => ({
|
|
152
|
+
...before,
|
|
153
|
+
shots: before.shots.map(shot => {
|
|
154
|
+
if (shot.id !== shotId) return shot;
|
|
155
|
+
const cast = { ...shot.cast };
|
|
156
|
+
if (override === null) delete cast[characterId];
|
|
157
|
+
else cast[characterId] = structuredClone(override);
|
|
158
|
+
return { ...shot, cast };
|
|
159
|
+
}),
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
143
162
|
function placeCamera(args) {
|
|
144
163
|
const before = appContext.ports.read().camera ?? state().camera;
|
|
145
164
|
const position = { ...before.position, ...Object.fromEntries(['x', 'y', 'z'].filter(key => args[key] !== undefined).map(key => [key, args[key]])) };
|
|
@@ -181,7 +200,7 @@ export function createShotsDomain(appContext, initial = {}) {
|
|
|
181
200
|
}
|
|
182
201
|
return run('run.update', { txId: gesture.txId, args });
|
|
183
202
|
}
|
|
184
|
-
const domain = { documentStore, state, read, write, writeState, beginAction, run, edit, beginGesture, finishGesture,
|
|
203
|
+
const domain = { documentStore, state, read, write, writeState, setCastOverride, beginAction, run, edit, beginGesture, finishGesture,
|
|
185
204
|
bindRender(context) { appContext = context; },
|
|
186
205
|
capture: () => appContext.shared.captureCurrentFraming(), frame, captureCamera, placeCamera, setLens, renderCamera,
|
|
187
206
|
canUndo: id => documentStore.canUndo(id), stepHistory: redo => { finishGesture(); return Boolean((redo ? documentStore.redo : documentStore.undo)()); },
|
|
@@ -345,7 +364,9 @@ export function useShots(appContext) {
|
|
|
345
364
|
const travel = Math.ceil(curve.length / speed * tlFps) + Math.round(tlFps * 0.5);
|
|
346
365
|
const endFrame = Math.min(tlFrameCount - 1, activeShot.startFrame + Math.max(travel, activeShot.endFrame - activeShot.startFrame));
|
|
347
366
|
setTimelineShotRange(activeShot.id, activeShot.startFrame, endFrame);
|
|
348
|
-
|
|
367
|
+
// The landing tutorial's last step is pressing ▶ (#602): open the player
|
|
368
|
+
// on the shot camera, but leave playback to the visitor.
|
|
369
|
+
appContext.shared.enterPreview(); appContext.shared.setTlPlaying(false); setTlFrame(activeShot.startFrame);
|
|
349
370
|
appContext.notify(isKo ? '레일 완성 — 샷 카메라 시점으로 전환했습니다. ▶ 로 재생, Esc 로 복귀' : 'Rail drawn — you are looking through the shot camera. Press ▶ to ride it; Esc goes back to flying.'); return;
|
|
350
371
|
}
|
|
351
372
|
appContext.notify(isKo ? `카메라 레일 완성 — ${curve ? curve.length.toFixed(1) : '?'} m, 제어점 ${simplified.length}개` : `Camera rail drawn — ${curve ? curve.length.toFixed(1) : '?'} m, ${simplified.length} control points`);
|
|
@@ -371,18 +392,23 @@ export function useShots(appContext) {
|
|
|
371
392
|
setTlFrameCount(span); if (extent > 0) setTlFrame(frame => Math.min(frame, span - 1));
|
|
372
393
|
}
|
|
373
394
|
function finishCameraMove(finalFov) { setMovePlaying(false); setFovDeg(Math.round(finalFov * 10) / 10); }
|
|
374
|
-
|
|
395
|
+
// scope "timeline" records every frame of the timeline, shot boxes and all;
|
|
396
|
+
// "shot" records one shot (shotId, or the one under the playhead). Without a
|
|
397
|
+
// scope the old rule stands: a named shot, or the playhead's shot when there
|
|
398
|
+
// is no motion, else the whole record.
|
|
399
|
+
async function exportShotVideo({ download = true, shotId = null, scope = null } = {}, commandContext = null) {
|
|
375
400
|
if (appContext.shared.recRef.current) return null;
|
|
376
401
|
const atPlayhead = shotIndexAtFrame(shots, tlFrame);
|
|
377
402
|
const target = shotId ? shots.find(entry => entry.id === shotId) : shots[atPlayhead >= 0 ? atPlayhead : 0] ?? null;
|
|
378
403
|
if (shotId && !target) return null;
|
|
379
404
|
let exportShots = shots;
|
|
380
|
-
if (target && target.cameraKeys.length === 0 && !shotId) {
|
|
405
|
+
if (target && target.cameraKeys.length === 0 && !shotId && scope !== 'timeline') {
|
|
381
406
|
appContext.bus.run('shot.addKey', { shotId: target.id, frame: target.startFrame });
|
|
382
407
|
exportShots = appContext.storeDomain('shot').read();
|
|
383
408
|
}
|
|
384
|
-
const
|
|
385
|
-
|
|
409
|
+
const oneShot = scope === 'shot' ? Boolean(target) : scope === 'timeline' ? false : Boolean(target && (shotId || !appContext.shared.motion));
|
|
410
|
+
const range = oneShot ? { startFrame: target.startFrame, endFrame: target.endFrame }
|
|
411
|
+
: { startFrame: 0, endFrame: Math.max(0, appContext.shared.currentRecordFrameCount() - 1, ...(scope === 'timeline' ? shots.map(entry => entry.endFrame) : [])) };
|
|
386
412
|
return appContext.shared.executeExportRequest(appContext.shared.exportRequest('video', async job => {
|
|
387
413
|
const abort = () => job.controller.abort(commandContext.signal.reason);
|
|
388
414
|
commandContext?.signal.addEventListener('abort', abort, { once: true });
|
package/src/error-boundary.jsx
CHANGED
|
@@ -31,7 +31,7 @@ export default class ErrorBoundary extends Component {
|
|
|
31
31
|
<div className="crash-screen" role="alert">
|
|
32
32
|
<h1>{ko("The studio hit a render error", "스튜디오에 렌더링 오류가 발생했어요")}</h1>
|
|
33
33
|
<p>{ko("Scenes autosave as you work, so reloading resumes from the last saved state.", "작업 중 장면은 자동 저장되므로, 새로고침하면 마지막 저장 상태에서 이어집니다.")}</p>
|
|
34
|
-
<pre className="crash-detail">{String(
|
|
34
|
+
<pre className="crash-detail">{String(this.state.error.message || this.state.error)}</pre>
|
|
35
35
|
<button type="button" onClick={() => window.location.reload()}>
|
|
36
36
|
{ko("Reload the studio", "스튜디오 새로고침")}
|
|
37
37
|
</button>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { bucketMs, track, EXECUTION_TELEMETRY_VALUES } from "./analytics.js";
|
|
2
|
-
export { AGENT_TOOL_CATEGORIES,
|
|
2
|
+
export { AGENT_TOOL_CATEGORIES, EXECUTION_TELEMETRY_EVENTS, EXECUTION_TELEMETRY_PROPERTY_KEYS, EXECUTION_TELEMETRY_VALUES } from "./analytics.js";
|
|
3
3
|
const WORKFLOW_FAILURE_CODES = new Set(["aborted", "capture_failed", "generation_failed", "unknown"]);
|
|
4
4
|
const AGENT_FAILURE_CODES = new Set(["aborted", "auth", "rate_limited", "tool_failed", "upstream", "unknown"]);
|
|
5
5
|
function secureId() {
|
|
@@ -31,7 +31,7 @@ function readNow(now) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
function executionFailureCode(error, channel, fallbackCode = "unknown") {
|
|
35
35
|
const allowed = channel === "agent" ? AGENT_FAILURE_CODES : WORKFLOW_FAILURE_CODES;
|
|
36
36
|
try {
|
|
37
37
|
if (error?.name === "AbortError") return "aborted";
|
|
@@ -48,8 +48,8 @@ export function mcpToolCategory(name) {
|
|
|
48
48
|
if (name === "set_camera" || name === "frame_shot" || name === "mark_camera_move" || name === "describe_camera_move") return "camera";
|
|
49
49
|
if (name === "set_prompt_blocks") return "prompt_authoring";
|
|
50
50
|
if (name === "save_project" || name === "open_project") return "project_io";
|
|
51
|
-
if (name === "describe_scene" || name === "describe_shot" || name === "render_prompt" || name === "live_status") return "read";
|
|
52
|
-
if (name === "add_character" || name === "place_character" || name === "remove_character" || name === "focus_character" || name === "place_object" || name === "import_mesh" || name === "group_objects" || name === "update_object" || name === "remove_object" || name === "apply_batch" || name === "add_scene" || name === "switch_scene") return "scene_write";
|
|
51
|
+
if (name === "describe_scene" || name === "describe_shot" || name === "render_prompt" || name === "live_status" || name === "compare_sculpt") return "read";
|
|
52
|
+
if (name === "add_character" || name === "place_character" || name === "remove_character" || name === "focus_character" || name === "place_object" || name === "import_mesh" || name === "sculpt_object" || name === "group_objects" || name === "update_object" || name === "remove_object" || name === "apply_batch" || name === "add_scene" || name === "switch_scene") return "scene_write";
|
|
53
53
|
return "other";
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -61,7 +61,7 @@ export function startWorkflowExecution(metadata, { capture = track, now = () =>
|
|
|
61
61
|
const emit = (event, props) => {
|
|
62
62
|
if (runId) safeCapture(capture, event, props);
|
|
63
63
|
};
|
|
64
|
-
|
|
64
|
+
emit("workflow:run_requested", {
|
|
65
65
|
surface: "workflow",
|
|
66
66
|
...(EXECUTION_TELEMETRY_VALUES.node_count_bucket.has(metadata.node_count_bucket) ? { node_count_bucket: metadata.node_count_bucket } : {}),
|
|
67
67
|
run_id: runId,
|
package/src/facing-marks.js
CHANGED
|
@@ -30,7 +30,7 @@ import * as THREE from "three";
|
|
|
30
30
|
* these cannot split segmentation. The COCO nose keypoint is the one the
|
|
31
31
|
* estimator keys facing on; the wedge centres the face below the eyes.
|
|
32
32
|
*/
|
|
33
|
-
|
|
33
|
+
const EYE_SEPARATION = 6;
|
|
34
34
|
|
|
35
35
|
export function facingMarkSpecs() {
|
|
36
36
|
const eye = (x) => ({
|
|
@@ -73,3 +73,16 @@ export function addFacingMarks(clone) {
|
|
|
73
73
|
head.add(mesh);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
export function proxyFacingMark(material = null) {
|
|
78
|
+
const mesh = new THREE.Mesh(
|
|
79
|
+
new THREE.ConeGeometry(0.055, 0.16, 4),
|
|
80
|
+
material ?? new THREE.MeshStandardMaterial({ color: "#1C1C1C", roughness: 0.7, metalness: 0 }),
|
|
81
|
+
);
|
|
82
|
+
mesh.position.set(0, -0.015, 0.115);
|
|
83
|
+
mesh.rotation.set(Math.PI / 2, Math.PI / 4, 0);
|
|
84
|
+
mesh.castShadow = true;
|
|
85
|
+
mesh.frustumCulled = false;
|
|
86
|
+
mesh.userData.facingMark = true;
|
|
87
|
+
return mesh;
|
|
88
|
+
}
|