cozyclay 2.0.2 → 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 +56 -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 +2 -3
- 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-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 +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 +789 -263
- 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 +113 -28
- 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 +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 +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/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 +94 -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/domains/cast.js
CHANGED
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
captureHipsOffset,
|
|
13
13
|
saveCustomPoses,
|
|
14
14
|
} from "../poses.js";
|
|
15
|
-
import { createCharacterEntry, createCharacterLayer } from "../scenes.js";
|
|
15
|
+
import { createCharacterEntry, createCharacterLayer, isProxyFigure, PROXY_FIGURE_MODEL } from "../scenes.js";
|
|
16
|
+
import { kindRefusal } from "../character-kind.js";
|
|
16
17
|
import {
|
|
17
18
|
DEFAULT_SUBJECT,
|
|
18
19
|
DEFAULT_SUBJECT2,
|
|
@@ -28,12 +29,12 @@ import { judgeNextWaypoint } from "../ardy/waypoints.js";
|
|
|
28
29
|
import { StudioProtocolError } from "../studio-agent-protocol.js";
|
|
29
30
|
import { studioActionRefusal } from "../studio-actions.js";
|
|
30
31
|
import { createStableItemId, removeStableItem } from "../stable-items.js";
|
|
31
|
-
import { trackFeature } from "../analytics.js";
|
|
32
|
+
import { track, trackFeature } from "../analytics.js";
|
|
32
33
|
import { requestBridgeExtract } from "../multimodel-ingest.js";
|
|
33
34
|
import { loadMotionFromUrl } from "../ardy/npz.js";
|
|
34
35
|
import { snapshotPlaybackBones, applyMotionFrame, restorePlaybackBones } from "../ardy/playback.js";
|
|
35
36
|
|
|
36
|
-
import {
|
|
37
|
+
import { shotAtFrame } from "../cuts.js";
|
|
37
38
|
|
|
38
39
|
const sameCastValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
39
40
|
|
|
@@ -43,7 +44,7 @@ export function createCastDomain(appContext, initial, customPoses = []) {
|
|
|
43
44
|
let native = createDocumentStore({ owned: { cast: normalize(initial) } });
|
|
44
45
|
const listeners = new Set(), motions = new Map();
|
|
45
46
|
const notify = () => { for (const listener of listeners) listener(); };
|
|
46
|
-
let release = native.subscribe(notify)
|
|
47
|
+
let release = native.subscribe(notify);
|
|
47
48
|
const documentStore = { ...Object.fromEntries(Object.keys(native).map(key => [key, (...args) => native[key](...args)])),
|
|
48
49
|
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); } };
|
|
49
50
|
const state = () => documentStore.read('cast'), read = () => state().characters;
|
|
@@ -74,6 +75,25 @@ export function createCastDomain(appContext, initial, customPoses = []) {
|
|
|
74
75
|
function write(update) {
|
|
75
76
|
return writeState(before => ({ ...before, characters: typeof update === 'function' ? update(before.characters) : update }));
|
|
76
77
|
}
|
|
78
|
+
function update(characterId, patch) {
|
|
79
|
+
const entry = read().find(entry => entry.id === characterId);
|
|
80
|
+
if (!entry) throw new StudioProtocolError('STALE_TARGET', `Character ${characterId} is not in this scene.`);
|
|
81
|
+
const raw = appContext.ports.read(), shot = shotAtFrame(raw.shots, raw.view.frame);
|
|
82
|
+
const fields = ['x', 'z', 'rot', 'posture', 'pose'];
|
|
83
|
+
const panelPatch = Object.fromEntries(Object.entries(patch).filter(([key, value]) => fields.includes(key) && value !== undefined));
|
|
84
|
+
if (shot?.kind === 'still' && !raw.targets?.get?.(characterId)?.motion && Object.keys(panelPatch).length) {
|
|
85
|
+
const current = { ...entry, ...shot.cast[characterId] };
|
|
86
|
+
const normalized = createCharacterEntry({ ...current, ...panelPatch });
|
|
87
|
+
const override = { x: normalized.x, z: normalized.z, rot: normalized.rot };
|
|
88
|
+
for (const key of ['posture', 'pose']) if (normalized[key] != null) override[key] = normalized[key];
|
|
89
|
+
// Enlist the shot owner in the already-open command/drag session:
|
|
90
|
+
// even a mixed scale + placement edit is one shared Undo entry.
|
|
91
|
+
const shots = appContext.storeDomain('shot');
|
|
92
|
+
appContext.recordAction('shot', () => shots.setCastOverride(shot.id, characterId, override), null, true);
|
|
93
|
+
patch = Object.fromEntries(Object.entries(patch).filter(([key]) => !fields.includes(key)));
|
|
94
|
+
}
|
|
95
|
+
if (Object.keys(patch).length) write(rows => rows.map(row => row.id === characterId ? { ...row, ...patch } : row));
|
|
96
|
+
}
|
|
77
97
|
function publishMotion(characterId, motion) {
|
|
78
98
|
if (motion) motions.set(characterId, motion); else motions.delete(characterId);
|
|
79
99
|
publish(); notify();
|
|
@@ -109,7 +129,7 @@ export function createCastDomain(appContext, initial, customPoses = []) {
|
|
|
109
129
|
}
|
|
110
130
|
return run('run.update', { txId: gesture.txId, args });
|
|
111
131
|
}
|
|
112
|
-
const domain = { documentStore, state, read, write, writeState, run, edit, beginGesture, finishGesture,
|
|
132
|
+
const domain = { documentStore, state, read, write, update, writeState, run, edit, beginGesture, finishGesture,
|
|
113
133
|
activeId: read()[0]?.id ?? null, projection: () => projection, publishMotion, normalizeCharacters: rows => rows.map(createCharacterEntry),
|
|
114
134
|
bindRender(context) { appContext = context; },
|
|
115
135
|
beginAction: () => documentStore.beginAction('cast'), canUndo: id => documentStore.canUndo(id),
|
|
@@ -121,13 +141,11 @@ export function createCastDomain(appContext, initial, customPoses = []) {
|
|
|
121
141
|
write(plan.draft); return plan;
|
|
122
142
|
},
|
|
123
143
|
load(value) {
|
|
124
|
-
finishGesture(true);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
domain.activeId = read()[0]?.id ?? null; domain.loadView?.(); notify();
|
|
130
|
-
} finally { loading = false; }
|
|
144
|
+
finishGesture(true);
|
|
145
|
+
release(); native.dispose(); motions.clear();
|
|
146
|
+
const next = normalize(Array.isArray(value) ? { characters: value, customPoses: state().customPoses } : value);
|
|
147
|
+
native = createDocumentStore({ owned: { cast: next } }); release = native.subscribe(notify);
|
|
148
|
+
domain.activeId = read()[0]?.id ?? null; domain.loadView?.(); notify();
|
|
131
149
|
},
|
|
132
150
|
dispose() { finishGesture(true); unregister(); unsubscribe(); release(); native.dispose(); listeners.clear(); },
|
|
133
151
|
};
|
|
@@ -164,7 +182,6 @@ export function useCast(appContext) {
|
|
|
164
182
|
|
|
165
183
|
const [poseRevision, setPoseTick] = useState(0);
|
|
166
184
|
|
|
167
|
-
/* --------------------- derived cast view + shims ---------------------- */
|
|
168
185
|
// Fallback second slot mirrors the old charB defaults so preset math and
|
|
169
186
|
// the two-subject inspector never see a hole before B exists.
|
|
170
187
|
const charA = characters[0] ?? createCharacterEntry(null, 0);
|
|
@@ -181,29 +198,12 @@ export function useCast(appContext) {
|
|
|
181
198
|
|
|
182
199
|
const subject2 = charB.subject ?? DEFAULT_SUBJECT2;
|
|
183
200
|
|
|
184
|
-
const rigA = rigs[charA.id] ?? null;
|
|
185
|
-
|
|
186
|
-
const rigB = (characters[1] ? rigs[charB.id] : null) ?? null;
|
|
187
|
-
|
|
188
201
|
function updateCharacterAt(index, next) {
|
|
189
|
-
const
|
|
202
|
+
const base = domain.read()[index], raw = appContext.ports.read();
|
|
203
|
+
const entry = base && { ...base, ...shotAtFrame(raw.shots, raw.view.frame)?.cast[base.id] };
|
|
190
204
|
if (entry) return domain.edit('character.update', { characterId: entry.id, patch: typeof next === 'function' ? next(entry) : next });
|
|
191
205
|
}
|
|
192
206
|
|
|
193
|
-
// The shims keep the legacy call sites (inspector sliders, presets, pose
|
|
194
|
-
// studio, prompts) untouched while the list stays the source of truth.
|
|
195
|
-
const setCharA = (next) => updateCharacterAt(0, next);
|
|
196
|
-
|
|
197
|
-
const setCharB = (next) => updateCharacterAt(1, next);
|
|
198
|
-
|
|
199
|
-
const setPoseA = (pose) => updateCharacterAt(0, (entry) => ({ pose: typeof pose === "function" ? pose(entry.pose ?? DEFAULT_POSE) : pose }));
|
|
200
|
-
|
|
201
|
-
const setPoseB = (pose) => updateCharacterAt(1, (entry) => ({ pose: typeof pose === "function" ? pose(entry.pose ?? DEFAULT_POSE) : pose }));
|
|
202
|
-
|
|
203
|
-
const setSubject = (value) => updateCharacterAt(0, (entry) => ({ subject: typeof value === "function" ? value(entry.subject) : value }));
|
|
204
|
-
|
|
205
|
-
const setSubject2 = (value) => updateCharacterAt(1, (entry) => ({ subject: typeof value === "function" ? value(entry.subject) : value }));
|
|
206
|
-
|
|
207
207
|
function setShowB(next) {
|
|
208
208
|
return domain.run('cast.showExtras', { show: typeof next === 'function' ? next(domain.read().some((entry, i) => i > 0 && !entry.hidden)) : next });
|
|
209
209
|
}
|
|
@@ -221,7 +221,8 @@ export function useCast(appContext) {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
function moveCharacter(charId, next) {
|
|
224
|
-
const
|
|
224
|
+
const base = domain.read().find(entry => entry.id === charId), raw = appContext.ports.read();
|
|
225
|
+
const entry = { ...base, ...shotAtFrame(raw.shots, raw.view.frame)?.cast[charId] };
|
|
225
226
|
return domain.edit('character.update', { characterId: charId, patch: typeof next === 'function' ? next(entry) : next });
|
|
226
227
|
}
|
|
227
228
|
|
|
@@ -258,6 +259,7 @@ export function useCast(appContext) {
|
|
|
258
259
|
const spawnCharacter = (model, x, z) => {
|
|
259
260
|
const id = nextCharacterId(domain.read());
|
|
260
261
|
domain.run('character.add', { character: { id, model, x, z, pose: DEFAULT_POSE, subject: 'a person' } });
|
|
262
|
+
if (model === PROXY_FIGURE_MODEL) track("cast:proxy_added", { surface: "assets" });
|
|
261
263
|
appContext.shared.setSelectedHierarchyId(`character:${id}`);
|
|
262
264
|
appContext.notify(ko("Character added to the scene", "인물을 씬에 추가했어요"));
|
|
263
265
|
};
|
|
@@ -270,7 +272,6 @@ export function useCast(appContext) {
|
|
|
270
272
|
return `character:${key.startsWith("char:") ? key.slice(5) : key}`;
|
|
271
273
|
};
|
|
272
274
|
|
|
273
|
-
/* ------------------- active character (motion layer) ------------------- */
|
|
274
275
|
// Every character owns an animation layer (root path, prompt blocks,
|
|
275
276
|
// generated clip, IK keys). The studio's motion machinery edits ONE layer
|
|
276
277
|
// at a time — the ACTIVE character's — and selection decides who that is.
|
|
@@ -332,9 +333,6 @@ export function useCast(appContext) {
|
|
|
332
333
|
waypointFrames: (entry.layer?.waypoints ?? []).map((waypoint) => waypoint.frame),
|
|
333
334
|
}]), [characters, activeChar.id]);
|
|
334
335
|
|
|
335
|
-
// Undo/redo (plan §6.5). The store settles any open drag first, so a
|
|
336
|
-
// mid-drag press commits that drag as one entry and then steps past it.
|
|
337
|
-
// After a step the selection can point at a deleted object — drop it to
|
|
338
336
|
/* Cast authoring uses the registered store. The native compatibility
|
|
339
337
|
* snapshot remains for motion/IK until its owner migrates; it must not
|
|
340
338
|
* restore the owned cast over newer character edits. */
|
|
@@ -424,10 +422,9 @@ export function useCast(appContext) {
|
|
|
424
422
|
const posedRig = () => rigs[posing] ?? null;
|
|
425
423
|
|
|
426
424
|
const setPosed = (pose) => {
|
|
427
|
-
if (posingIndex >= 0) updateCharacterAt(posingIndex, { pose: typeof pose === "function" ? pose(
|
|
425
|
+
if (posingIndex >= 0) updateCharacterAt(posingIndex, entry => ({ pose: typeof pose === "function" ? pose(entry.pose ?? DEFAULT_POSE) : pose }));
|
|
428
426
|
};
|
|
429
427
|
|
|
430
|
-
/* ------------------------- waypoint workspace --------------------------- */
|
|
431
428
|
// A walking pace turns clicked distance into clip time, so pins land at
|
|
432
429
|
// frames the character can actually reach without ice-skating.
|
|
433
430
|
const WALK_SPEED_MPS = 1.4;
|
|
@@ -681,6 +678,23 @@ export function useCast(appContext) {
|
|
|
681
678
|
: ko(`Saved the current pose to the library as “${pose.label}”`, `지금 자세를 “${pose.label}”로 라이브러리에 저장했어요`));
|
|
682
679
|
}
|
|
683
680
|
|
|
681
|
+
/** A handle drag is an authored pose. When it ends, the rig as it now
|
|
682
|
+
* stands becomes the posed character's own pose — so the scene autosaves
|
|
683
|
+
* it and undo can step it back — instead of living only on the rig until
|
|
684
|
+
* a reload re-applies the stored pose over it. The library is untouched;
|
|
685
|
+
* Save pose still bottles a pose there. */
|
|
686
|
+
function commitPosedPose() {
|
|
687
|
+
const rig = posedRig();
|
|
688
|
+
if (!rig || !posingChar) return;
|
|
689
|
+
domain.run('character.setPose', { characterId: posingChar.id, clearMotion: false, pose: {
|
|
690
|
+
id: `edited:${posingChar.id}`,
|
|
691
|
+
label: ko("Edited pose", "편집한 포즈"),
|
|
692
|
+
prompt: "in the exact body pose shown in the blocking frame",
|
|
693
|
+
bones: capturePose(rig),
|
|
694
|
+
rootY: captureHipsOffset(rig),
|
|
695
|
+
} });
|
|
696
|
+
}
|
|
697
|
+
|
|
684
698
|
function savePose() {
|
|
685
699
|
const rig = posedRig();
|
|
686
700
|
if (!rig) return;
|
|
@@ -720,6 +734,8 @@ export function useCast(appContext) {
|
|
|
720
734
|
setPhotoPoseState("running");
|
|
721
735
|
setPhotoPoseError("");
|
|
722
736
|
try {
|
|
737
|
+
// A capsule figure has no bones for a photo to pose.
|
|
738
|
+
if (isProxyFigure(posingChar ?? activeChar)) throw new StudioProtocolError("TARGET_NOT_READY", kindRefusal("photoPose", isKo));
|
|
723
739
|
if (!rig) throw new Error("rig-not-loaded");
|
|
724
740
|
objectUrl = URL.createObjectURL(file);
|
|
725
741
|
let bones = null;
|
|
@@ -907,7 +923,9 @@ export function useCast(appContext) {
|
|
|
907
923
|
const live = appContext.live.state;
|
|
908
924
|
const patch = finitePatch(args, ["x", "z", "rot"]);
|
|
909
925
|
const id = nextCharacterId(live.characters);
|
|
910
|
-
|
|
926
|
+
const entry = createCharacterEntry({ id, model: args.model, subject: args.subject, pose: DEFAULT_POSE, ...patch }, live.characters.length);
|
|
927
|
+
replaceCharacters([...live.characters, entry]);
|
|
928
|
+
if (isProxyFigure(entry)) track("cast:proxy_added", { surface: "mcp" });
|
|
911
929
|
return { id };
|
|
912
930
|
}
|
|
913
931
|
function updateLiveCharacter(args) {
|
|
@@ -947,7 +965,6 @@ export function useCast(appContext) {
|
|
|
947
965
|
if (typeof block.text !== "string" || !block.text.trim()) throw new Error(`Block ${i + 1} needs text`);
|
|
948
966
|
return { id: `prompt-${stamp}-${i}`, startFrame, endFrame, text: block.text.trim() };
|
|
949
967
|
});
|
|
950
|
-
const live = appContext.live.state;
|
|
951
968
|
domain.run('character.setPromptBlocks', { characterId: domain.activeId, blocks: clips });
|
|
952
969
|
return { blocks: clips.length };
|
|
953
970
|
}
|
|
@@ -982,14 +999,14 @@ export function useCast(appContext) {
|
|
|
982
999
|
const motion = appContext.storeDomain('motion');
|
|
983
1000
|
if (motion) motion.clear(characterId); else appContext.shared.motionDomain.clearMotionNative();
|
|
984
1001
|
}, characterId, true);
|
|
985
|
-
domain.
|
|
1002
|
+
domain.update(characterId, { pose });
|
|
986
1003
|
};
|
|
987
1004
|
appContext.updateActionPorts({ addCharacterWaypoint, moveCharacterWaypoint, removeCharacterWaypoint, clearCharacterWaypoints, setWaypointMode });
|
|
988
1005
|
return {
|
|
989
1006
|
...domain,
|
|
990
1007
|
applyExternalCharacters, publishStudioCharacters, syncStudioLayerBuffer, undoScene, redoScene, snapshotStudioDomain, removeLegacyRootWaypoint, switchActiveCharacterLayer, createLegacyCastHandlers, toggleCharacterHidden,
|
|
991
1008
|
characters, editCharacters, customPoses, posing, setPosing, posingClosing,
|
|
992
|
-
studioPick, setStudioPick, rigs, rigMountEpoch, setRigMountEpoch, setPoseTick, charA, charB, showB,
|
|
1009
|
+
studioPick, setStudioPick, commitPosedPose, rigs, rigMountEpoch, setRigMountEpoch, setPoseTick, charA, charB, showB,
|
|
993
1010
|
poseA, poseB, subject, subject2, updateCharacterAt, setShowB, moveCharacter, removeCharacter, reportRig,
|
|
994
1011
|
spawnCharacter, charKeyToHierarchyId, charIdFromHierarchyId, activeCharacterId, setActiveCharacterId,
|
|
995
1012
|
rowIdForCharIndex, activeChar, selectActiveCharacterInHierarchy, activeCharIndex, activeRig, waitForRig,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
const sameMotionIntent = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
4
|
+
const emptyMotionLayer = id => ({ id, take: null, fullTake: null, ikKeys: [], ikPins: [], ikPinResiduals: [], committedIkEdits: [], takeRecipe: null, takeVersions: [] });
|
|
5
|
+
const keyVector = p => ({ x: p.x, y: p.y, z: p.z });
|
|
6
|
+
const keyQuaternion = q => ({ ...keyVector(q), w: q.w });
|
|
7
|
+
function encodeMotionKeys(keys) {
|
|
8
|
+
return [...keys].sort(([a], [b]) => a - b).map(([frame, tracks]) => ({ frame, tracks: Object.fromEntries([...tracks].map(([track, key]) => [track, {
|
|
9
|
+
...(key.q ? { q: key.q.map(keyQuaternion) } : {}), ...(key.p ? { p: keyVector(key.p) } : {}),
|
|
10
|
+
...(key.baseQ ? { baseQ: key.baseQ.map(keyQuaternion) } : {}), ...(key.basePos ? { basePos: keyVector(key.basePos) } : {}),
|
|
11
|
+
...(key.chainP ? { chainP: key.chainP.map(keyVector) } : {}), ...(key.keepTranslations ? { keepTranslations: true } : {}),
|
|
12
|
+
...(key.correctionRange ? { correctionRange: { start: key.correctionRange.start, end: key.correctionRange.end } } : {}),
|
|
13
|
+
...(key.blend != null ? { blend: key.blend } : {}),
|
|
14
|
+
...(key.pin ? { pin: key.pin } : {}),
|
|
15
|
+
}])) }));
|
|
16
|
+
}
|
|
17
|
+
function decodeMotionKeys(rows) {
|
|
18
|
+
const q = value => new THREE.Quaternion(value.x, value.y, value.z, value.w).normalize();
|
|
19
|
+
const p = value => new THREE.Vector3(value.x, value.y, value.z);
|
|
20
|
+
return new Map(rows.map(row => [row.frame, new Map(Object.entries(row.tracks).map(([track, key]) => [track, {
|
|
21
|
+
q: key.q?.map(q) ?? null, p: key.p ? p(key.p) : null,
|
|
22
|
+
...(key.baseQ ? { baseQ: key.baseQ.map(q) } : {}), ...(key.basePos ? { basePos: p(key.basePos) } : {}),
|
|
23
|
+
...(key.chainP ? { chainP: key.chainP.map(p) } : {}), ...(key.keepTranslations ? { keepTranslations: true } : {}),
|
|
24
|
+
...(key.correctionRange ? { correctionRange: { start: key.correctionRange.start, end: key.correctionRange.end } } : {}),
|
|
25
|
+
...(key.blend != null ? { blend: key.blend } : {}),
|
|
26
|
+
...(key.pin ? { pin: key.pin } : {}),
|
|
27
|
+
}]))]));
|
|
28
|
+
}
|
|
29
|
+
function cloneRangePin(pin) {
|
|
30
|
+
return {
|
|
31
|
+
...pin,
|
|
32
|
+
target: pin.target.space === "world"
|
|
33
|
+
? { space: "world", position: [...pin.target.position] }
|
|
34
|
+
: { space: "object", objectId: pin.target.objectId, local: [...pin.target.local] },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function encodeRangePins(pins) {
|
|
38
|
+
return [...(pins instanceof Map ? pins.values() : pins ?? [])].map(cloneRangePin);
|
|
39
|
+
}
|
|
40
|
+
function decodeRangePins(rows) {
|
|
41
|
+
return new Map((rows ?? []).map((pin) => [pin.id, cloneRangePin(pin)]));
|
|
42
|
+
}
|
|
43
|
+
function encodeRangePinResiduals(values) {
|
|
44
|
+
return [...(values instanceof Map ? values.entries() : values ?? [])].map(([id, entries]) => [id, (entries ?? []).map((entry) => ({ ...entry }))]);
|
|
45
|
+
}
|
|
46
|
+
function decodeRangePinResiduals(rows) {
|
|
47
|
+
return new Map((rows ?? []).map(([id, entries]) => [id, (entries ?? []).map((entry) => ({ ...entry }))]));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { sameMotionIntent, emptyMotionLayer, encodeMotionKeys, decodeMotionKeys, cloneRangePin, encodeRangePins, decodeRangePins, encodeRangePinResiduals, decodeRangePinResiduals };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { rangePinTracks } from "../../ardy/range-pin.js";
|
|
2
|
+
import { ko } from "../../locale.js";
|
|
3
|
+
|
|
4
|
+
export function ensureRangePinState(state) {
|
|
5
|
+
if (!state) return state;
|
|
6
|
+
if (!(state.pins instanceof Map)) state.pins = new Map();
|
|
7
|
+
if (!(state.pinResiduals instanceof Map)) state.pinResiduals = new Map();
|
|
8
|
+
return state;
|
|
9
|
+
}
|
|
10
|
+
export function rangePinConflictFrames(draft, state) {
|
|
11
|
+
if (!Number.isInteger(draft?.startFrame) || !Number.isInteger(draft?.endFrame) || draft.endFrame < draft.startFrame) return [];
|
|
12
|
+
const tracks = rangePinTracks(draft), conflicts = [];
|
|
13
|
+
for (let frame = draft.startFrame; frame <= draft.endFrame; frame += 1) {
|
|
14
|
+
if (tracks.some((track) => { const key = state.keys.get(frame)?.get(track); return key && key.pin !== draft.id; })) conflicts.push(frame);
|
|
15
|
+
}
|
|
16
|
+
return conflicts;
|
|
17
|
+
}
|
|
18
|
+
export function overlappingRangePin(draft, state) {
|
|
19
|
+
return [...ensureRangePinState(state).pins.values()].find((pin) => pin.id !== draft.id && rangePinTracks(pin).some((track) => rangePinTracks(draft).includes(track)) && Math.max(pin.startFrame, draft.startFrame) <= Math.min(pin.endFrame, draft.endFrame));
|
|
20
|
+
}
|
|
21
|
+
export function rangePinOverlapMessage(pin) {
|
|
22
|
+
return pin ? ko(`A required body or limb track already has an overlapping pin (${pin.startFrame}–${pin.endFrame}).`, `필요한 몸통 또는 팔다리 파츠에 겹치는 고정이 있어요 (${pin.startFrame}–${pin.endFrame}).`) : "";
|
|
23
|
+
}
|