cozyclay 3.1.6 → 3.2.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 +145 -0
- package/INSTALL_FOR_AGENTS.md +4 -4
- package/README.md +4 -2
- package/bin/agent/agent-failure-codes.mjs +34 -8
- package/bin/agent/agent-routes.mjs +6 -6
- package/bin/agent/agent-runner.mjs +25 -17
- package/bin/agent/agent-stream.mjs +26 -3
- package/bin/agent/main-prompt.mjs +1 -1
- package/bin/agent/openai-compatible-provider.mjs +24 -1
- package/bin/agent/studio-prompt.mjs +2 -2
- package/bin/agent/supervisor-followup.mjs +34 -0
- package/bin/agent/supervisor.mjs +6 -6
- package/bin/cozyclay.mjs +23 -60
- package/bin/star-prompt.mjs +15 -14
- package/bin/studio-routes.mjs +6 -3
- package/bin/telemetry-state.mjs +5 -7
- package/bin/update-check.mjs +61 -7
- package/bin/update-install.mjs +16 -3
- package/bin/update-restart.mjs +95 -0
- package/bin/update-routes.mjs +25 -3
- package/dist/app/index.html +13 -7
- package/dist/assets/TaskBoard-tgO0MNKL.js +1 -0
- package/dist/assets/analytics-CaTUuLPM.js +1 -0
- package/dist/assets/{app-CEP7jzXB.css → app-CnDkB5WO.css} +1 -1
- package/dist/assets/app-DJt06Oyb.js +5144 -0
- package/dist/assets/compile-worker-D1pl6G7x.js +6 -0
- package/dist/assets/landing-2Fy7DU41.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-BxaM0Xwl.js +1 -0
- package/dist/assets/scene-asset-sync-CLGNbEBq.js +13 -0
- package/dist/assets/task-board-UVGTZLRF.js +15 -0
- package/dist/assets/workflow-CSiBlX1s.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +4 -1
- package/dist/privacy/index.html +9 -6
- package/dist/sitemap.xml +2 -2
- package/dist/workflow/index.html +5 -4
- package/mcp/README.md +1 -1
- package/mcp/live-hub.mjs +4 -4
- package/mcp/tool-handlers.mjs +14 -16
- package/package.json +3 -1
- package/src/App.jsx +42 -75
- package/src/analytics-controls.js +25 -3
- package/src/analytics-strict.js +16 -4
- package/src/analytics.js +32 -26
- package/src/app-version.js +13 -0
- package/src/asset-drop.js +39 -0
- package/src/asset-pane.css +34 -0
- package/src/asset-pane.jsx +4 -35
- package/src/bus-tasks.js +1 -1
- package/src/character-kind.js +8 -6
- package/src/click-friction.js +5 -4
- package/src/command-bus-telemetry.js +8 -7
- package/src/command-bus.js +2 -16
- package/src/commands/cast.js +2 -1
- package/src/commands/index.js +2 -1
- package/src/commands/motion3.js +3 -1
- package/src/commands/objects.js +2 -37
- package/src/commands/physics.js +21 -0
- package/src/commands/project.js +3 -3
- package/src/commands/sculpt-compare.js +53 -0
- package/src/content-tile.jsx +36 -0
- package/src/domains/cast.js +7 -6
- package/src/domains/motion.js +13 -14
- package/src/domains/scenes.js +41 -31
- package/src/execution-telemetry.js +7 -3
- package/src/export-menu-telemetry.js +26 -0
- package/src/finger-handles.jsx +128 -0
- package/src/friction-hint.js +82 -0
- package/src/live-control.js +2 -1
- package/src/locale-zh/panels.js +20 -0
- package/src/locale-zh/shell.js +4 -0
- package/src/locale-zh/workspace.js +6 -0
- package/src/motion/center-of-mass.js +5 -23
- package/src/motion/contact-project.js +143 -0
- package/src/motion/find-bone.js +13 -0
- package/src/motion/fingers.js +170 -0
- package/src/motion/fingertip-pick.js +21 -0
- package/src/motion/fix-collisions.js +1 -1
- package/src/motion/ik-drag.js +93 -1
- package/src/motion/ik-exposure.js +48 -0
- package/src/motion/ik.js +4 -11
- package/src/motion/timeline.css +12 -0
- package/src/motion/timeline.jsx +2 -2
- package/src/motion3/compile-job.js +3 -2
- package/src/motion3/compiler/compile.js +3 -3
- package/src/motion3/compiler/head-lead.js +4 -2
- package/src/motion3/compiler/spring.js +95 -0
- package/src/motion3/compiler/validate-objects.js +18 -2
- package/src/motion3/compiler/validate-parts.js +1 -0
- package/src/motion3/compiler/vocab.js +1 -1
- package/src/motion3/fix/index.js +8 -4
- package/src/motion3/fix/person.js +110 -0
- package/src/motion3/free-motion.js +49 -11
- package/src/motion3/hybrid-hint.js +19 -0
- package/src/motion3/load.js +19 -12
- package/src/motion3/object-pose.js +44 -2
- package/src/motion3/people.js +16 -1
- package/src/motion3/physics-frames.js +72 -0
- package/src/motion3/physics-trail.js +45 -0
- package/src/motion3/physics-view.js +79 -0
- package/src/motion3/pipeline.js +5 -1
- package/src/motion3/qa/ballistic.js +67 -0
- package/src/motion3/qa/com.js +140 -0
- package/src/motion3/qa/ground.js +10 -3
- package/src/motion3/qa/hints-contact.js +21 -7
- package/src/motion3/qa/hints-motion.js +38 -1
- package/src/motion3/qa/key-checks.js +5 -4
- package/src/motion3/qa/patch.js +32 -4
- package/src/motion3/qa/people-checks.js +1 -1
- package/src/motion3/qa/spring-pivot.js +23 -0
- package/src/motion3/qa/style.js +13 -30
- package/src/motion3/qa/verdict.js +7 -2
- package/src/motion3/repair.js +28 -8
- package/src/motion3/schema/fields.js +4 -2
- package/src/motion3/schema/topics.js +15 -11
- package/src/motion3/schema/types.js +13 -9
- package/src/motion3/skills/catch.js +2 -1
- package/src/panels/FingerPanel.jsx +38 -0
- package/src/panels/RigControlPanel.jsx +9 -1
- package/src/panels/SculptPanel.jsx +12 -12
- package/src/panels/SculptPartTree.jsx +46 -0
- package/src/panels/SubjectsPanel.jsx +1 -1
- package/src/panels/sculpt.css +11 -0
- package/src/physics-overlay-settings.js +41 -0
- package/src/physics-overlay.js +152 -0
- package/src/physics-overlay.jsx +54 -0
- package/src/physics-toggle.jsx +20 -0
- package/src/pose-fade.js +7 -3
- package/src/posestudio.jsx +15 -28
- package/src/project-browser.css +6 -0
- package/src/project-browser.jsx +9 -18
- package/src/project-start-choices.jsx +45 -0
- package/src/project.js +16 -12
- package/src/proxy-figure-shape.js +11 -0
- package/src/proxy-figure.jsx +25 -18
- package/src/scene-assets.js +17 -13
- package/src/scenes.js +22 -11
- package/src/sculpt-blueprint.js +56 -0
- package/src/sculpt-mode.js +5 -0
- package/src/sculpt-part-edit.js +115 -0
- package/src/sculpt-part-gizmo.jsx +159 -0
- package/src/sculpt-part-outline.jsx +72 -0
- package/src/sculpt-pick.js +53 -0
- package/src/sculpt-run-text.js +51 -0
- package/src/sculpt-run.js +10 -46
- package/src/sculpt-shelf-file.js +92 -0
- package/src/sculpt-shelf-tiles.jsx +69 -0
- package/src/sculpt-shelf.js +108 -0
- package/src/sculpt-stage.jsx +39 -2
- package/src/sculpt-thumb.js +72 -0
- package/src/sculpt-views.js +75 -0
- package/src/semantic-edit.js +0 -87
- package/src/settings-menu.jsx +2 -2
- package/src/shell/DetailsSlot.jsx +5 -6
- package/src/shell/LibrarySlot.jsx +8 -10
- package/src/shell/MenuBar.jsx +6 -6
- package/src/shell/PreferencesDialog.jsx +2 -3
- package/src/shell/ViewportToolbar.jsx +15 -18
- package/src/shell/add-character-entries.js +13 -0
- package/src/shell/pose-controls.js +32 -0
- package/src/shell/preference-pages.jsx +2 -11
- package/src/shell/storyboard-caption.js +21 -0
- package/src/shell/studio-shell-context.js +1 -0
- package/src/shortcut-friction.js +2 -1
- package/src/site/landing-analytics.js +28 -0
- package/src/studio-actions.js +3 -2
- package/src/studio-agent-protocol.js +4 -4
- package/src/studio-app-binding.js +4 -3
- package/src/update/UpdateChip.jsx +21 -2
- package/src/update/UpdateDialog.jsx +26 -6
- package/src/update/update-client.js +123 -17
- package/src/update/update-telemetry.js +15 -3
- package/src/use-sculpt-mode.js +62 -8
- package/src/workflow/agent-failure-codes.js +28 -9
- package/src/workflow/agent-turn-telemetry.js +1 -1
- package/src/workflow/task-board.js +2 -2
- package/tools/dev/page-shell.py +1 -1
- package/tools/dev/pages/greybox-to-video.html +21 -2
- package/tools/dev/pages/previs-software.html +3 -3
- package/tools/dev/pages/privacy.html +9 -6
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/loc-baseline.json +10 -10
- package/tools/run-tests.mjs +53 -84
- package/tools/select-tests.mjs +119 -0
- package/tools/test-durations.json +455 -0
- package/tools/test-graph.mjs +181 -0
- package/tools/test-shards.mjs +78 -0
- package/tools/tutorial/README.md +74 -0
- package/tools/tutorial/assemble.mjs +43 -0
- package/tools/tutorial/captions.mjs +43 -0
- package/tools/tutorial/cdp.mjs +55 -0
- package/tools/tutorial/cursor.mjs +159 -0
- package/tools/tutorial/episode.mjs +173 -0
- package/tools/tutorial/episodes/beginner-guide/00-install.mjs +42 -0
- package/tools/tutorial/episodes/beginner-guide/01-start.mjs +50 -0
- package/tools/tutorial/episodes/beginner-guide/02-layout.mjs +67 -0
- package/tools/tutorial/episodes/beginner-guide/03-stage.mjs +58 -0
- package/tools/tutorial/episodes/beginner-guide/04-motion.mjs +98 -0
- package/tools/tutorial/episodes/beginner-guide/05-camera.mjs +47 -0
- package/tools/tutorial/episodes/beginner-guide/06-export.mjs +59 -0
- package/tools/tutorial/episodes/beginner-guide/steps.mjs +32 -0
- package/tools/tutorial/episodes/beginner-guide.mjs +17 -0
- package/tools/tutorial/episodes/cast.mjs +12 -0
- package/tools/tutorial/episodes/first-video.mjs +121 -0
- package/tools/tutorial/episodes/stub-tour.mjs +97 -0
- package/tools/tutorial/launch.mjs +63 -0
- package/tools/tutorial/mux.mjs +61 -0
- package/tools/tutorial/overlay.mjs +142 -0
- package/tools/tutorial/record.mjs +156 -0
- package/tools/tutorial/scenes/terminal.html +63 -0
- package/tools/tutorial/steps.mjs +111 -0
- package/tools/tutorial/terminal.mjs +43 -0
- package/tools/tutorial/voice.mjs +49 -0
- package/tools/update-test-durations.mjs +42 -0
- package/tools/verify-package-tarball.mjs +18 -12
- package/dist/assets/TaskBoard-DwXhsJXz.js +0 -1
- package/dist/assets/app-BLS1Zaru.js +0 -5144
- package/dist/assets/compile-worker-or7djI2v.js +0 -6
- package/dist/assets/motion3-compile-Du7UXrWn.js +0 -1
- package/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/dist/assets/scene-asset-sync-t45pWCyq.js +0 -13
- package/dist/assets/task-board-CkLZGPkv.js +0 -15
- package/dist/assets/workflow-DJwOUzqJ.js +0 -179
package/src/commands/objects.js
CHANGED
|
@@ -5,8 +5,7 @@ import { elementSetSchema, registerElementSet } from './elements.js';
|
|
|
5
5
|
import './elements/object.js';
|
|
6
6
|
import { createSceneObject, updateSceneObject, removeSceneObject, setSceneObjectParent, descendantsOf, normalizeSceneObject, groupUnderNewEmpty, objectPatchFields, createSculptObject, SCULPT_KIND } from '../scene-objects.js';
|
|
7
7
|
import { expandSculptParts, normalizeSculptRecipe, patchSculptRecipe } from '../sculpt-recipe.js';
|
|
8
|
-
import {
|
|
9
|
-
import { browserSculptImages, shrinkRgba } from './sculpt-images.js';
|
|
8
|
+
import { runSculptCompare, sculptCompare } from './sculpt-compare.js';
|
|
10
9
|
import { clampWarning } from '../receipt-findings.js';
|
|
11
10
|
import { effectPresetSet } from '../effects/effect-record.js';
|
|
12
11
|
import { withLightWarnings } from '../light-blockers.js';
|
|
@@ -52,16 +51,6 @@ const assetImport = { ...studioActionDeclaration('asset.import'), kind: 'job', d
|
|
|
52
51
|
input({ assetId: id, placement, placeAs: assetProperties.placeAs }, ['assetId', 'placeAs']),
|
|
53
52
|
input({ fileToken: id, placeAs: { type: 'string', enum: ['mesh', 'cutout'] } }, ['fileToken', 'placeAs']),
|
|
54
53
|
] } };
|
|
55
|
-
// Read-only: a job that never commits, so its receipt is `completed` with the score as output,
|
|
56
|
-
// authored false and no undo. The scoring is MCP compare_sculpt's (sculpt-compare.js).
|
|
57
|
-
const openObject = { type: 'object', properties: {}, additionalProperties: true };
|
|
58
|
-
const sculptCompare = { id: 'sculpt.compare', label: 'Compare a sculpt with its reference', kind: 'job', timeoutMs: 60000,
|
|
59
|
-
description: 'Score a sculpt\'s silhouette against a reference image without changing anything: IoU, width-to-height delta, missing area by height band (top, middle, bottom), parts that stick out, and a reference | overlay sheet (grey both, red missing, blue extra) as sheetDataUrl. reference is { assetId } of a project image or { dataUrl }. yaw is the reference camera azimuth in degrees (0 in front, positive toward the subject\'s own left); omitted tries -45..45 and keeps the best. candidates (up to 12 { label, edits }) are scored from the same view and ranked against the current sculpt; none is applied. Apply the winner with object.sculpt { id, edits }.',
|
|
60
|
-
input: input({ id, reference: { oneOf: [input({ assetId: id }), input({ dataUrl: { type: 'string', pattern: '^data:image/(png|jpeg|webp|gif);base64,', maxLength: 6000000 } })] },
|
|
61
|
-
yaw: { type: 'number', minimum: -180, maximum: 180 }, elevation: { type: 'number', minimum: -60, maximum: 60 },
|
|
62
|
-
candidates: { type: 'array', minItems: 1, maxItems: 12, items: input({ label: { type: 'string', maxLength: 60 }, edits: { type: 'array', minItems: 1, maxItems: 48, items: openObject } }) } }, ['id', 'reference']) };
|
|
63
|
-
// A receipt is at most 8 KiB, so the sheet is shrunk until the whole output fits this.
|
|
64
|
-
const SCULPT_COMPARE_OUTPUT_BYTES = 6500;
|
|
65
54
|
const matte = { id: 'object.matte', label: 'Apply object matte', description: 'Prepare derived assets, then publish one fenced object edit.', kind: 'job', domain: 'objects', exposure: 'ui-only', input: input({ objectId: id }) };
|
|
66
55
|
export const declarations = Object.freeze([...semantic, replace, batch, ...['object.attach', 'object.detach', 'object.duplicate'].map(studioActionDeclaration), matte, sculptCompare, assetImport]);
|
|
67
56
|
|
|
@@ -229,31 +218,7 @@ export function register(registry, ports) {
|
|
|
229
218
|
await owned().applyMatte(objectId, context);
|
|
230
219
|
return { affectedIds: [objectId], summary: 'Applied the object matte.' };
|
|
231
220
|
} });
|
|
232
|
-
registry.register({ ...sculptCompare, available, run:
|
|
233
|
-
const target = objectOf(objectId);
|
|
234
|
-
if (target.renderer !== SCULPT_KIND || !target.recipe) fail('INVALID_ARGUMENT', `${objectId} is a ${target.renderer}, not a sculpt.`);
|
|
235
|
-
const images = ports.sculptImages ?? browserSculptImages;
|
|
236
|
-
let image, scored;
|
|
237
|
-
try { image = await images.decode(reference); }
|
|
238
|
-
catch (error) { fail('INVALID_ARGUMENT', `Could not read the reference image: ${error?.message || error}`); }
|
|
239
|
-
context?.signal?.throwIfAborted();
|
|
240
|
-
try { scored = scoreSculptAgainstReference(target.recipe, image, { yaw, elevation, candidates }); }
|
|
241
|
-
catch (error) { fail('INVALID_ARGUMENT', error?.message || String(error)); }
|
|
242
|
-
const { result, ranked, refused } = scored;
|
|
243
|
-
const output = { id: objectId, ...result.report };
|
|
244
|
-
if (ranked) Object.assign(output, { ranking: [...ranked.map(row => rankingRow(row, result)), ...refused.map(({ label, error }) => ({ label, refused: error }))],
|
|
245
|
-
sheetLabel: ranked[0].label, note: `Nothing was applied. Apply the winner with object.sculpt { id: "${objectId}", edits }.` });
|
|
246
|
-
const sheet = (ranked ? ranked[0].result : result).sheet;
|
|
247
|
-
const room = SCULPT_COMPARE_OUTPUT_BYTES - JSON.stringify(output).length;
|
|
248
|
-
// The full-size sheet is always tried, however narrow; shrinking stops at 64 px.
|
|
249
|
-
for (let factor = 1; factor === 1 || sheet.width / factor >= 64; factor *= 2) {
|
|
250
|
-
const dataUrl = await images.encodePng(factor === 1 ? sheet : shrinkRgba(sheet, factor));
|
|
251
|
-
if (dataUrl.length + 40 <= room) { Object.assign(output, { sheetDataUrl: dataUrl, ...(factor > 1 ? { sheetScale: 1 / factor } : {}) }); break; }
|
|
252
|
-
}
|
|
253
|
-
if (!output.sheetDataUrl) output.sheetOmitted = 'The overlay sheet does not fit in a receipt at a readable size; read missingByBand and extraByPart.';
|
|
254
|
-
const lead = ranked ? `best ${ranked[0].label} at IoU ${ranked[0].result.report.iou} (current ${result.report.iou}), ${candidates.length} candidates scored, none applied` : `IoU ${result.report.iou}`;
|
|
255
|
-
return { affectedIds: [], output, summary: `Compared ${target.name || objectId} with the reference at yaw ${result.report.view.yaw}: ${lead}.` };
|
|
256
|
-
} });
|
|
221
|
+
registry.register({ ...sculptCompare, available, run: runSculptCompare(ports, objectOf) });
|
|
257
222
|
registry.register({ ...assetImport, available: () => true,
|
|
258
223
|
run: async (args, context) => {
|
|
259
224
|
const { source, ...options } = args;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// motion.physicsOverlay: the physics overlay's viewer setting (physics-overlay-settings.js), the agent's door to the
|
|
2
|
+
// motion controls' Physics box. Transient: never saved, never undone.
|
|
3
|
+
import { describePhysicsOverlay, setPhysicsOverlay } from "../physics-overlay-settings.js";
|
|
4
|
+
|
|
5
|
+
const flag = description => ({ type: "boolean", description });
|
|
6
|
+
|
|
7
|
+
export const declarations = Object.freeze([
|
|
8
|
+
{ id: "motion.physicsOverlay", label: "Physics overlay", kind: "transient",
|
|
9
|
+
input: { type: "object", required: [], additionalProperties: false, properties: {
|
|
10
|
+
com: flag("the centre of mass: a 4 cm ball with a line down to the ground, green inside the feet, red past the balance limit (what QA calls BALANCE), grey off the ground"),
|
|
11
|
+
support: flag("the support polygon: the outline of the soles on the ground"),
|
|
12
|
+
arc: flag("per air run: the hips' path (white, dashed) and the gravity parabola through the same takeoff and landing (orange); far apart is a FLOAT"),
|
|
13
|
+
trail: flag("the last 24 frames of each spring object's tip"),
|
|
14
|
+
all: flag("every part at once, before the named ones; { all: false } turns the overlay off"),
|
|
15
|
+
} },
|
|
16
|
+
description: "Draw the physics QA measures over the active character's installed take, in the editor views and in captures, contact sheets and exports (capture_frame, verify_result frames, export.shotVideo) until it is turned off: com, support, arc and trail (see each field). Updated every frame, the same in scrub, playback and export. No field turns every part on; { all: false } turns it off. Turn it on before a screenshot to see why QA reports BALANCE or FLOAT. A viewer setting: off by default, never saved or undone." },
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export function register(registry) {
|
|
20
|
+
registry.register({ ...declarations[0], available: () => true, run: args => ({ affectedIds: [], summary: describePhysicsOverlay(setPhysicsOverlay(args)) }) });
|
|
21
|
+
}
|
package/src/commands/project.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { studioActionDeclaration } from "../studio-actions.js";
|
|
2
2
|
import { fail } from "./shared.js";
|
|
3
|
-
import { PREVIS_MODES } from "../project.js";
|
|
3
|
+
import { CAST_STYLES, PREVIS_MODES } from "../project.js";
|
|
4
4
|
|
|
5
5
|
const input = (properties = {}, required = []) => ({ type: "object", properties, required, additionalProperties: false });
|
|
6
6
|
const name = { type: "string", minLength: 1, maxLength: 240 };
|
|
@@ -12,8 +12,8 @@ const saveAs = { ...save, id: "project.saveAs", label: "Save project as" };
|
|
|
12
12
|
const exportAsAnimation = studioActionDeclaration("project.exportAsAnimation");
|
|
13
13
|
// The deadline covers the user in the file picker, so it is the hub's ceiling, not the 30 s default.
|
|
14
14
|
const opening = { ...document("project.open", "Open a project file", { serialized: { type: "string", maxLength: 400_000_000 }, handleToken: token, projectToken: token }), timeoutMs: 300_000 };
|
|
15
|
-
const fresh = document("project.new", "Start a new project", { name, previsMode: { type: "string", enum: [...PREVIS_MODES] } });
|
|
16
|
-
const starter = document("project.openStarter", "Open a starter project", { id: token, source: { type: "string", enum: ["starter", "launch"] }, name });
|
|
15
|
+
const fresh = document("project.new", "Start a new project", { name, previsMode: { type: "string", enum: [...PREVIS_MODES] }, castStyle: { type: "string", enum: [...CAST_STYLES] } });
|
|
16
|
+
const starter = document("project.openStarter", "Open a starter project", { id: token, source: { type: "string", enum: ["starter", "launch"] }, name, castStyle: { type: "string", enum: [...CAST_STYLES] } });
|
|
17
17
|
const restore = { ...document("project.restore", "Restore the remembered project", { handleToken: token }), exposure: "ui-only" };
|
|
18
18
|
const browse = { id: "project.browse", label: "Browse projects", description: "Show the project browser.", kind: "transient", exposure: "ui-only", input: input() };
|
|
19
19
|
export const declarations = Object.freeze([rename, save, saveAs, exportAsAnimation, opening, fresh, starter, restore, browse]);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// sculpt.compare: the editor's read-only way to score a sculpt against its
|
|
2
|
+
// reference, one view ({ reference, yaw }) or several weighted ones
|
|
3
|
+
// ({ views: [{ yaw, reference, weight }] }, #970). A job that never commits, so
|
|
4
|
+
// its receipt is `completed` with the score as output, authored false and no
|
|
5
|
+
// undo. The scoring is MCP compare_sculpt's (sculpt-compare.js, sculpt-views.js).
|
|
6
|
+
import { SCULPT_KIND } from '../scene-objects.js';
|
|
7
|
+
import { rankingRow, scoreSculptAgainstReference } from '../sculpt-compare.js';
|
|
8
|
+
import { scoreSculptAgainstViews } from '../sculpt-views.js';
|
|
9
|
+
import { browserSculptImages, shrinkRgba } from './sculpt-images.js';
|
|
10
|
+
import { fail } from './shared.js';
|
|
11
|
+
import { StudioSchemas } from '../studio-agent-protocol.js';
|
|
12
|
+
|
|
13
|
+
const id = StudioSchemas.TargetGuard.properties.targetId;
|
|
14
|
+
const input = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: false });
|
|
15
|
+
const openObject = { type: 'object', properties: {}, additionalProperties: true };
|
|
16
|
+
const yaw = { type: 'number', minimum: -180, maximum: 180 };
|
|
17
|
+
const reference = { oneOf: [input({ assetId: id }), input({ dataUrl: { type: 'string', pattern: '^data:image/(png|jpeg|webp|gif);base64,', maxLength: 6000000 } })] };
|
|
18
|
+
export const sculptCompare = { id: 'sculpt.compare', label: 'Compare a sculpt with its reference', kind: 'job', timeoutMs: 60000,
|
|
19
|
+
description: 'Score a sculpt\'s silhouette against a reference image without changing anything: IoU, width-to-height delta, missing area by height band (top, middle, bottom), parts that stick out, and a reference | overlay sheet (grey both, red missing, blue extra) as sheetDataUrl. reference is { assetId } of a project image or { dataUrl }. yaw is the reference camera azimuth in degrees (0 in front, positive toward the subject\'s own left); omitted tries -45..45 and keeps the best. Instead of reference, views [{ yaw, reference, weight? }] scores several views of the subject (a blueprint\'s front 0, side 90, back 180) and blends their IoUs by weight (default front 0.6, side 0.25, back 0.15; a view whose height:width differs from the front\'s by more than 35% gets 0); the output adds one row per view. candidates (up to 12 { label, edits }) are scored from the same view(s) and ranked against the current sculpt; none is applied. Apply the winner with object.sculpt { id, edits }.',
|
|
20
|
+
input: input({ id, reference, yaw, elevation: { type: 'number', minimum: -60, maximum: 60 },
|
|
21
|
+
views: { type: 'array', minItems: 1, maxItems: 4, items: input({ yaw, reference, weight: { type: 'number', minimum: 0, maximum: 1 } }, ['yaw', 'reference']) },
|
|
22
|
+
candidates: { type: 'array', minItems: 1, maxItems: 12, items: input({ label: { type: 'string', maxLength: 60 }, edits: { type: 'array', minItems: 1, maxItems: 48, items: openObject } }) } }, ['id']) };
|
|
23
|
+
// A receipt is at most 8 KiB, so the sheet is shrunk until the whole output fits this.
|
|
24
|
+
const SCULPT_COMPARE_OUTPUT_BYTES = 6500;
|
|
25
|
+
|
|
26
|
+
/** The run function for sculpt.compare; `objectOf` resolves an id or fails STALE_TARGET. */
|
|
27
|
+
export const runSculptCompare = (ports, objectOf) => async ({ id: objectId, reference, views, yaw, elevation = 0, candidates }, context) => {
|
|
28
|
+
const target = objectOf(objectId);
|
|
29
|
+
if (target.renderer !== SCULPT_KIND || !target.recipe) fail('INVALID_ARGUMENT', `${objectId} is a ${target.renderer}, not a sculpt.`);
|
|
30
|
+
if ((reference === undefined) === (views === undefined)) fail('INVALID_ARGUMENT', 'Give exactly one of reference (one view) or views (several weighted views).');
|
|
31
|
+
const images = ports.sculptImages ?? browserSculptImages;
|
|
32
|
+
let decoded, scored;
|
|
33
|
+
try { decoded = views ? await Promise.all(views.map(async view => ({ ...view, image: await images.decode(view.reference) }))) : await images.decode(reference); }
|
|
34
|
+
catch (error) { fail('INVALID_ARGUMENT', `Could not read the reference image: ${error?.message || error}`); }
|
|
35
|
+
context?.signal?.throwIfAborted();
|
|
36
|
+
try { scored = views ? scoreSculptAgainstViews(target.recipe, decoded, { elevation, candidates }) : scoreSculptAgainstReference(target.recipe, decoded, { yaw, elevation, candidates }); }
|
|
37
|
+
catch (error) { fail('INVALID_ARGUMENT', error?.message || String(error)); }
|
|
38
|
+
const { result, ranked, refused } = scored;
|
|
39
|
+
const output = { id: objectId, ...result.report };
|
|
40
|
+
if (ranked) Object.assign(output, { ranking: [...ranked.map(row => rankingRow(row, result)), ...refused.map(({ label, error }) => ({ label, refused: error }))],
|
|
41
|
+
sheetLabel: ranked[0].label, note: `Nothing was applied. Apply the winner with object.sculpt { id: "${objectId}", edits }.` });
|
|
42
|
+
const sheet = (ranked ? ranked[0].result : result).sheet;
|
|
43
|
+
const room = SCULPT_COMPARE_OUTPUT_BYTES - JSON.stringify(output).length;
|
|
44
|
+
// The full-size sheet is always tried, however narrow; shrinking stops at 64 px.
|
|
45
|
+
for (let factor = 1; factor === 1 || sheet.width / factor >= 64; factor *= 2) {
|
|
46
|
+
const dataUrl = await images.encodePng(factor === 1 ? sheet : shrinkRgba(sheet, factor));
|
|
47
|
+
if (dataUrl.length + 40 <= room) { Object.assign(output, { sheetDataUrl: dataUrl, ...(factor > 1 ? { sheetScale: 1 / factor } : {}) }); break; }
|
|
48
|
+
}
|
|
49
|
+
if (!output.sheetDataUrl) output.sheetOmitted = 'The overlay sheet does not fit in a receipt at a readable size; read missingByBand and extraByPart.';
|
|
50
|
+
const lead = ranked ? `best ${ranked[0].label} at IoU ${ranked[0].result.report.iou} (current ${result.report.iou}), ${candidates.length} candidates scored, none applied` : `IoU ${result.report.iou}`;
|
|
51
|
+
const where = views ? `${views.length} weighted views (${result.report.views.map(row => `${row.view} ${row.weight}`).join(', ')})` : `the reference at yaw ${result.report.view.yaw}`;
|
|
52
|
+
return { affectedIds: [], output, summary: `Compared ${target.name || objectId} with ${where}: ${lead}.` };
|
|
53
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The Content grid's tile and its grab wire, shared by the asset pane's
|
|
2
|
+
// folders (asset-pane.jsx) and the sculpt shelf (sculpt-shelf-tiles.jsx).
|
|
3
|
+
|
|
4
|
+
/** The grab wire every card shares: left button only, App owns the rest. */
|
|
5
|
+
export function grabProps(onAssetGrab, payload) {
|
|
6
|
+
return {
|
|
7
|
+
onPointerDown: (event) => {
|
|
8
|
+
if (event.button !== 0) return;
|
|
9
|
+
event.preventDefault();
|
|
10
|
+
onAssetGrab?.(payload, event);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The Content grid's one tile grammar: a square thumbnail, an 11px name and
|
|
16
|
+
* a 10px type line. Click selects (amber ring), a left-button press starts the
|
|
17
|
+
* App-owned drag, double-click places the asset at the origin. */
|
|
18
|
+
export function ContentTile({ assetKey, label, type, title, preview, selected, onSelect, grab, onPlace, failed = false }) {
|
|
19
|
+
return (
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
className={"content-tile" + (failed ? " is-failed" : "")}
|
|
23
|
+
data-testid="content-asset"
|
|
24
|
+
data-asset-key={assetKey}
|
|
25
|
+
aria-pressed={selected}
|
|
26
|
+
title={title}
|
|
27
|
+
{...(grab ?? {})}
|
|
28
|
+
onClick={() => onSelect(assetKey)}
|
|
29
|
+
onDoubleClick={onPlace ? () => onPlace() : undefined}
|
|
30
|
+
>
|
|
31
|
+
<span className="content-tile-thumb">{preview}</span>
|
|
32
|
+
<span className="content-tile-name">{label}</span>
|
|
33
|
+
<span className="content-tile-type">{type}</span>
|
|
34
|
+
</button>
|
|
35
|
+
);
|
|
36
|
+
}
|
package/src/domains/cast.js
CHANGED
|
@@ -221,8 +221,6 @@ export function useCast(appContext) {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
function removeCharacter(charId) {
|
|
224
|
-
const list = appContext.live.characters;
|
|
225
|
-
if (list.length <= 1) return;
|
|
226
224
|
domain.run('character.remove', { characterId: charId });
|
|
227
225
|
// The full-take cache remains native until the motion owner migrates.
|
|
228
226
|
if (!appContext.storeDomain('motion')) appContext.shared.motionFullRef.current.delete(charId);
|
|
@@ -373,6 +371,7 @@ export function useCast(appContext) {
|
|
|
373
371
|
|
|
374
372
|
function setLayerField(key, value) {
|
|
375
373
|
const entry = domain.read().find(entry => entry.id === domain.activeId) ?? domain.read()[0];
|
|
374
|
+
if (!entry) return;
|
|
376
375
|
const next = typeof value === 'function' ? value(entry.layer[key]) : value;
|
|
377
376
|
if (sameCastValue(entry.layer[key], next)) return;
|
|
378
377
|
return domain.run('cast.setLayer', { characterId: entry.id, layer: { [key]: next } });
|
|
@@ -743,7 +742,8 @@ export function useCast(appContext) {
|
|
|
743
742
|
return appContext.storeDomain('motion').switchLayer(entry.id);
|
|
744
743
|
}
|
|
745
744
|
function switchActiveCharacterLayer() {
|
|
746
|
-
|
|
745
|
+
// An empty stage loads no layer (id null).
|
|
746
|
+
const entry = domain.projection().find(entry => entry.id === domain.activeId) ?? domain.projection()[0] ?? { id: null };
|
|
747
747
|
const previous = appContext.shared.loadedLayerCharRef.current;
|
|
748
748
|
if (previous === entry.id) return;
|
|
749
749
|
switchNativeMotionLayer(previous, entry);
|
|
@@ -784,7 +784,6 @@ export function useCast(appContext) {
|
|
|
784
784
|
const live = appContext.live.state;
|
|
785
785
|
const character = characterForRef(live.characters, args.ref);
|
|
786
786
|
if (!character) throw new Error("Character not found");
|
|
787
|
-
if (live.characters.length <= 1) throw new Error("Cannot remove the final character");
|
|
788
787
|
domain.run('character.remove', { characterId: character.id });
|
|
789
788
|
return { id: character.id };
|
|
790
789
|
}
|
|
@@ -793,8 +792,10 @@ export function useCast(appContext) {
|
|
|
793
792
|
function toggleCharacterHidden(charId) { return domain.run('character.update', { characterId: charId, patch: { hidden: castMemberOf(charId).hidden !== true } }); }
|
|
794
793
|
domain.snapshotMotion = snapshotNativeMotion;
|
|
795
794
|
domain.syncLayer = () => {
|
|
795
|
+
// An empty stage has no layer to edit: the buffer holds no route.
|
|
796
796
|
const entry = domain.read().find(entry => entry.id === appContext.shared.loadedLayerCharRef.current) ?? domain.read()[0];
|
|
797
|
-
appContext.shared.bufferRef.current = { ...appContext.shared.bufferRef.current, ...entry
|
|
797
|
+
appContext.shared.bufferRef.current = { ...appContext.shared.bufferRef.current, ...(entry?.layer ?? { waypoints: [] }) };
|
|
798
|
+
if (!entry) return setActiveWaypointId(null);
|
|
798
799
|
|
|
799
800
|
setActiveWaypointId(id => entry.layer.waypoints.some(waypoint => waypoint.id === id) ? id : null);
|
|
800
801
|
};
|
|
@@ -809,7 +810,7 @@ export function useCast(appContext) {
|
|
|
809
810
|
const shots = appContext.storeDomain('shot');
|
|
810
811
|
if (!shots) return;
|
|
811
812
|
const current = domain.projection(), active = current.find(entry => entry.id === domain.activeId) ?? current[0];
|
|
812
|
-
const extent = timelineContentExtent(current, active
|
|
813
|
+
const extent = timelineContentExtent(current, active?.id ?? null, appContext.shared.bufferRef.current.motion, null);
|
|
813
814
|
domain.setTimeline(Math.max(24, timelineSpan(extent, shots.read(), shots.state().frameCount)));
|
|
814
815
|
};
|
|
815
816
|
domain.extendTimeline = domain.syncTimeline;
|
package/src/domains/motion.js
CHANGED
|
@@ -12,10 +12,7 @@ import {
|
|
|
12
12
|
ikKeyframes,
|
|
13
13
|
ikRemoveKeyframe,
|
|
14
14
|
ikSeedTargets,
|
|
15
|
-
clampIkTargetToFloor,
|
|
16
|
-
solveIk,
|
|
17
15
|
MID_TRACKS,
|
|
18
|
-
solveMidJoint,
|
|
19
16
|
solveEffectorSwing,
|
|
20
17
|
ikPlantFeet,
|
|
21
18
|
solveHipsTranslateToFloor,
|
|
@@ -35,7 +32,8 @@ import { ko } from "../locale.js";
|
|
|
35
32
|
import { isProxyFigure } from "../scenes.js";
|
|
36
33
|
import { refuseRigOnly } from "../character-kind.js";
|
|
37
34
|
import { DEFAULT_POSE, restoreBindPositions, applyPose, applyHipsOffset } from "../poses.js";
|
|
38
|
-
import { bakeIkDragKey, bakeIkDragRange, chainsChangedBy, ikDragRecord, ikDragTouch } from "../motion/ik-drag.js";
|
|
35
|
+
import { bakeIkDragKey, bakeIkDragRange, chainsChangedBy, clampDragTarget, ikDragRecord, ikDragTouch, solveDragTarget } from "../motion/ik-drag.js";
|
|
36
|
+
import { fingersOf, solveFingerGesture, restFingers } from "../motion/fingers.js";
|
|
39
37
|
import { ikKeyJson, ikTrackKeyFromJson } from "../motion/ik-key-json.js";
|
|
40
38
|
import { chooseIkEntryPose } from "../ik-camera.js";
|
|
41
39
|
import { applyRangePin, captureRangePinTarget, normalizeRangePin, rangePinTargetWorld, rangePinTracks, removeRangePinKeys } from "../motion/range-pin.js";
|
|
@@ -74,8 +72,7 @@ export function createMotionDomain(appContext, characters) {
|
|
|
74
72
|
let release = native.subscribe(notify), viewRevision = 0;
|
|
75
73
|
const documentStore = { ...Object.fromEntries(Object.keys(native).map(key => [key, (...args) => native[key](...args)])),
|
|
76
74
|
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); } };
|
|
77
|
-
const read = () => documentStore.read('motion');
|
|
78
|
-
const layer = id => read().find(row => row.id === id) ?? emptyMotionLayer(id);
|
|
75
|
+
const read = () => documentStore.read('motion'), layer = id => read().find(row => row.id === id) ?? emptyMotionLayer(id);
|
|
79
76
|
function materialize(take) {
|
|
80
77
|
if (take?.source && !takes.has(take.resourceId)) {
|
|
81
78
|
try { const compiled = compileTakeSource(take.source); if (compiled) takes.set(take.resourceId, compiled); }
|
|
@@ -113,7 +110,7 @@ export function createMotionDomain(appContext, characters) {
|
|
|
113
110
|
const resolved = resolveIkRig(rig); if (resolved) Object.assign(state, resolved, { rig });
|
|
114
111
|
if (savedRigs?.has(id)) appContext.shared.restoreExportRig(savedRigs.get(id));
|
|
115
112
|
else if (resolved) {
|
|
116
|
-
if (take) applyTakeFrame(rig, take, Math.min(frame(), take.frames - 1));
|
|
113
|
+
restFingers(resolved.fkJoints); if (take) applyTakeFrame(rig, take, Math.min(frame(), take.frames - 1));
|
|
117
114
|
else {
|
|
118
115
|
const character = cast.read().find(entry => entry.id === id);
|
|
119
116
|
restoreBindPositions(rig); applyPose(rig, { ...REST_BONES, ...(character?.pose ?? DEFAULT_POSE).bones });
|
|
@@ -256,7 +253,7 @@ export function createMotionDomain(appContext, characters) {
|
|
|
256
253
|
rigOnly(id, 'pose');
|
|
257
254
|
const rig = rigFor(id), resolved = rig && resolveIkRig(rig);
|
|
258
255
|
if (!resolved) throw new StudioProtocolError('TARGET_NOT_READY', 'The character rig is not loaded.');
|
|
259
|
-
restoreBindPositions(rig); applyPose(rig, { ...REST_BONES, ...pose.bones }); applyHipsOffset(rig, pose.rootY ?? 0);
|
|
256
|
+
restoreBindPositions(rig); restFingers(resolved.fkJoints); applyPose(rig, { ...REST_BONES, ...pose.bones }); applyHipsOffset(rig, pose.rootY ?? 0);
|
|
260
257
|
editKeys(id, state => {
|
|
261
258
|
state.tracked = new Set([...resolved.chains.keys(), ...resolved.fkJoints.keys()]);
|
|
262
259
|
ikBakeKeyframe(resolved.chains, state, at, resolved.fkJoints);
|
|
@@ -741,7 +738,7 @@ export function useMotion(appContext) {
|
|
|
741
738
|
// chain toward the target; mid joints reposition the elbow/knee with both
|
|
742
739
|
// ends pinned (the handle snaps to the clamped position); FK joints swing
|
|
743
740
|
// toward the pointer. The solved pose stays a preview until Apply.
|
|
744
|
-
function ikSolve(kind, trackId, targetWorld) {
|
|
741
|
+
function ikSolve(kind, trackId, targetWorld, input = null) {
|
|
745
742
|
if (ikDragRef.current && ikDragRef.current.characterId !== appContext.shared.activeChar.id) return;
|
|
746
743
|
const state = appContext.shared.ikStateRef.current;
|
|
747
744
|
const ikChains = state.chains;
|
|
@@ -762,14 +759,16 @@ export function useMotion(appContext) {
|
|
|
762
759
|
ikDragRef.current = { frame: appContext.shared.tlFrame, ids: new Set(pending?.ids ?? []) };
|
|
763
760
|
}
|
|
764
761
|
ikDragRef.current = Object.assign(ikDragRecord(ikDragRef.current, appContext.shared.tlFrame), { characterId: appContext.shared.activeChar.id });
|
|
765
|
-
const touch = (id) => ikDragTouch(appContext.shared.ikStateRef.current, ikDragRef.current, id);
|
|
762
|
+
const touch = (id) => ikDragTouch(appContext.shared.ikStateRef.current, ikDragRef.current, id), activeId = appContext.shared.activeChar.id;
|
|
763
|
+
const clamp = () => clampDragTarget({ kind, trackId, targetWorld, bodyContact, chains: ikChains, record: ikDragRef.current, bypass: input?.bypass, open: () => ({ rigs: appContext.shared.rigs, activeId, cast: appContext.shared.characters, allow: domain.layer(activeId).take?.source?.spec?.checks?.allow, notify: appContext.notify }) });
|
|
764
|
+
if (kind === "finger") return solveFingerGesture(fingersOf(appContext.shared.ikStateRef.current.fkJoints)?.get(trackId), targetWorld, touch);
|
|
766
765
|
if (kind === "chain") {
|
|
767
766
|
const chain = appContext.shared.ikStateRef.current.chains?.get(trackId);
|
|
768
767
|
if (!chain) return;
|
|
769
768
|
touch(trackId);
|
|
770
|
-
const clampedTarget =
|
|
769
|
+
const clampedTarget = clamp();
|
|
771
770
|
appContext.shared.ikStateRef.current.targets.set(trackId, clampedTarget.clone());
|
|
772
|
-
|
|
771
|
+
solveDragTarget(kind, chain, clampedTarget);
|
|
773
772
|
return;
|
|
774
773
|
}
|
|
775
774
|
if (kind === "mid") {
|
|
@@ -778,7 +777,7 @@ export function useMotion(appContext) {
|
|
|
778
777
|
const chain = midDef ? appContext.shared.ikStateRef.current.chains?.get(midDef.chain) : null;
|
|
779
778
|
if (!chain) return;
|
|
780
779
|
touch(chain.track.id);
|
|
781
|
-
|
|
780
|
+
solveDragTarget(kind, chain, clamp());
|
|
782
781
|
return;
|
|
783
782
|
}
|
|
784
783
|
// Effector swing: the rotation ring on a focused hand/foot. Rotates
|
|
@@ -917,7 +916,7 @@ export function useMotion(appContext) {
|
|
|
917
916
|
restoreBindPositions(appContext.shared.activeRig);
|
|
918
917
|
// Reset-then-pose, the same shape the Character effect applies: unlisted
|
|
919
918
|
// joints return to rest instead of keeping stale limbs from the clip.
|
|
920
|
-
applyPose(appContext.shared.activeRig, { ...REST_BONES, ...pose.bones });
|
|
919
|
+
restFingers(ikFkJoints); applyPose(appContext.shared.activeRig, { ...REST_BONES, ...pose.bones });
|
|
921
920
|
// The hips' measured height rides into the bake: the hips FK joint keys
|
|
922
921
|
// its local position (p), so a crouched pose keys a crouched body.
|
|
923
922
|
applyHipsOffset(appContext.shared.activeRig, pose.rootY ?? 0);
|
package/src/domains/scenes.js
CHANGED
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
clearStoredProjectHandle,
|
|
38
38
|
DEFAULT_PREVIS_MODE,
|
|
39
39
|
normalizePrevisMode,
|
|
40
|
+
DEFAULT_CAST_STYLE,
|
|
41
|
+
normalizeCastStyle,
|
|
40
42
|
buildAnimationProjectFromStoryboard,
|
|
41
43
|
} from "../project.js";
|
|
42
44
|
import { playgroundSceneUrl, fetchSceneProject } from "../playground.js";
|
|
@@ -56,11 +58,13 @@ import { createIkState } from "../motion/ik.js";
|
|
|
56
58
|
|
|
57
59
|
// The scene list and project identity share one history. Dirty is derived from
|
|
58
60
|
// the saved checkpoint, not an authored edit, and has its own non-history slice.
|
|
59
|
-
// previsMode
|
|
60
|
-
// (blank creation, starter/file open, session restore),
|
|
61
|
-
|
|
61
|
+
// previsMode and castStyle are project identity too: they are set only by a
|
|
62
|
+
// document boundary (blank creation, starter/file open, session restore),
|
|
63
|
+
// never by an edit.
|
|
64
|
+
function createScenesDomain(appContext, initial, name, previsMode = DEFAULT_PREVIS_MODE, castStyle = DEFAULT_CAST_STYLE) {
|
|
62
65
|
const ordered = rows => rows.map((scene, order) => ({ ...scene, order }));
|
|
63
|
-
|
|
66
|
+
const project = (fields, mode, style) => ({ ...fields, previsMode: normalizePrevisMode(mode), castStyle: normalizeCastStyle(style) });
|
|
67
|
+
let native = createDocumentStore({ owned: { scenes: ordered(initial.scenes), project: project({ name, activeSceneId: initial.activeSceneId }, previsMode, castStyle) } });
|
|
64
68
|
const listeners = new Set();
|
|
65
69
|
const notify = () => { for (const listener of listeners) listener(); };
|
|
66
70
|
let release = native.subscribe(notify);
|
|
@@ -96,7 +100,7 @@ function createScenesDomain(appContext, initial, name, previsMode = DEFAULT_PREV
|
|
|
96
100
|
appContext.publishScenes(read());
|
|
97
101
|
appContext.shared.activeSceneIdRef.current = metadata().activeSceneId;
|
|
98
102
|
if (appContext.live.state) appContext.patchLive({ scenes: read(), activeSceneId: metadata().activeSceneId });
|
|
99
|
-
storeProjectSession(metadata().name, metadata()
|
|
103
|
+
storeProjectSession(metadata().name, metadata());
|
|
100
104
|
domain.persist?.();
|
|
101
105
|
domain.refreshDirty?.();
|
|
102
106
|
};
|
|
@@ -120,9 +124,9 @@ function createScenesDomain(appContext, initial, name, previsMode = DEFAULT_PREV
|
|
|
120
124
|
},
|
|
121
125
|
// Session cache/file loads are explicit non-authored boundaries. They
|
|
122
126
|
// retire scene history, just as opening a scene retired native history.
|
|
123
|
-
replaceDocument(scenes, activeSceneId, name = metadata().name, previsMode = metadata().previsMode) {
|
|
127
|
+
replaceDocument(scenes, activeSceneId, name = metadata().name, previsMode = metadata().previsMode, castStyle = metadata().castStyle) {
|
|
124
128
|
release(); native.dispose();
|
|
125
|
-
native = createDocumentStore({ owned: { scenes: ordered(scenes), project: { name, activeSceneId, previsMode
|
|
129
|
+
native = createDocumentStore({ owned: { scenes: ordered(scenes), project: project({ name, activeSceneId }, previsMode, castStyle) } });
|
|
126
130
|
release = native.subscribe(notify); notify();
|
|
127
131
|
},
|
|
128
132
|
setDirty(value) {
|
|
@@ -141,10 +145,10 @@ export function useScenes(appContext) {
|
|
|
141
145
|
const [domain] = useState(() => {
|
|
142
146
|
if (appContext.storeDomain("scenes")) return appContext.storeDomain("scenes");
|
|
143
147
|
const session = loadProjectSession();
|
|
144
|
-
return createScenesDomain(appContext, appContext.shared.startup.document, session?.name ?? null, session?.previsMode);
|
|
148
|
+
return createScenesDomain(appContext, appContext.shared.startup.document, session?.name ?? null, session?.previsMode, session?.castStyle);
|
|
145
149
|
});
|
|
146
150
|
const scenes = useDocumentDomain(domain.documentStore, "scenes");
|
|
147
|
-
const { activeSceneId, name: projectName, previsMode } = useDocumentDomain(domain.documentStore, "project");
|
|
151
|
+
const { activeSceneId, name: projectName, previsMode, castStyle } = useDocumentDomain(domain.documentStore, "project");
|
|
148
152
|
const projectDirty = useDocumentDomain(domain.dirtyStore, "projectDirty");
|
|
149
153
|
const runtimeValues = useRef(new Map());
|
|
150
154
|
async function runProject(id, args = {}) {
|
|
@@ -228,6 +232,7 @@ export function useScenes(appContext) {
|
|
|
228
232
|
workflow: loadWorkflowGraph(),
|
|
229
233
|
name,
|
|
230
234
|
previsMode: domain.metadata().previsMode,
|
|
235
|
+
castStyle: domain.metadata().castStyle,
|
|
231
236
|
};
|
|
232
237
|
}
|
|
233
238
|
|
|
@@ -263,7 +268,7 @@ export function useScenes(appContext) {
|
|
|
263
268
|
appContext.shared.projectSnapshotRef.current = snapshot;
|
|
264
269
|
if (domain.metadata().name === previousName) domain.nameSaved(name);
|
|
265
270
|
refreshProjectDirty();
|
|
266
|
-
storeProjectSession(domain.metadata().name, domain.metadata()
|
|
271
|
+
storeProjectSession(domain.metadata().name, domain.metadata());
|
|
267
272
|
}
|
|
268
273
|
|
|
269
274
|
function projectProblemsNotice(problems) {
|
|
@@ -405,8 +410,8 @@ export function useScenes(appContext) {
|
|
|
405
410
|
// too — otherwise an older .cozyclay would open a sixth too fast.
|
|
406
411
|
const doc = migrateToCurrentScenes(source);
|
|
407
412
|
const mergedCustomPoses = mergeProjectCustomPoses(appContext.shared.customPoses, project.customPoses);
|
|
408
|
-
const projectPrevisMode = normalizePrevisMode(project.previsMode);
|
|
409
|
-
domain.replaceDocument(doc.scenes, doc.activeSceneId, project.name, projectPrevisMode);
|
|
413
|
+
const projectPrevisMode = normalizePrevisMode(project.previsMode), projectCastStyle = normalizeCastStyle(project.castStyle);
|
|
414
|
+
domain.replaceDocument(doc.scenes, doc.activeSceneId, project.name, projectPrevisMode, projectCastStyle);
|
|
410
415
|
if (project.workspaceLayout) appContext.shared.setWorkspaceLayout({ ...DEFAULT_WORKSPACE_LAYOUT, ...project.workspaceLayout });
|
|
411
416
|
appContext.bus.run('cast.setCustomPoses', { poses: mergedCustomPoses });
|
|
412
417
|
const resolvedWorkflow = resolveWorkflowOutputs(normalizeWorkflowGraph(project.workflow), new Map((project.assets ?? []).map((asset) => [asset.id, asset])));
|
|
@@ -417,7 +422,7 @@ export function useScenes(appContext) {
|
|
|
417
422
|
appContext.shared.projectSnapshotRef.current = collectProjectSnapshot(project.name);
|
|
418
423
|
domain.pendingCheckpoint = { clock: appContext.undoClock, name: project.name };
|
|
419
424
|
domain.setDirty(false);
|
|
420
|
-
storeProjectSession(project.name, projectPrevisMode);
|
|
425
|
+
storeProjectSession(project.name, { previsMode: projectPrevisMode, castStyle: projectCastStyle });
|
|
421
426
|
setProjectStartupOpen(false);
|
|
422
427
|
setProjectManifest(resourceManifest({ scenesDocument: doc, workflow: resolvedWorkflow, poseLibrary: mergedCustomPoses, assets: project.assets ?? [], workflowOutputRefs }));
|
|
423
428
|
track("project:opened", { age_bucket: bucketProjectAge(Date.now() - (project.savedAt ?? Date.now())) });
|
|
@@ -425,7 +430,8 @@ export function useScenes(appContext) {
|
|
|
425
430
|
|
|
426
431
|
/** Open a bundled starter scene as a fresh, saveable project. Used by the
|
|
427
432
|
* first-run dialog and by `npx cozyclay --scene <id>` (`?scene=`). */
|
|
428
|
-
|
|
433
|
+
// A cast style chosen on the start screen overrides the starter's own.
|
|
434
|
+
async function openStarterScene(id, source = "starter", context = null, name = null, castStyle = undefined) {
|
|
429
435
|
if (source === "starter" && domain.dirtyStore.read("projectDirty") && !window.confirm(ko("Discard unsaved changes and start a new project?", "저장되지 않은 변경사항을 버리고 새 프로젝트를 시작할까요?"))) return false;
|
|
430
436
|
const url = playgroundSceneUrl(`?scene=${encodeURIComponent(id)}`);
|
|
431
437
|
const project = url ? await fetchSceneProject(url) : null;
|
|
@@ -434,7 +440,7 @@ export function useScenes(appContext) {
|
|
|
434
440
|
return false;
|
|
435
441
|
}
|
|
436
442
|
context?.check();
|
|
437
|
-
applyProject({ ...project, name: name?.trim() || project.name, savedAt: null }, true);
|
|
443
|
+
applyProject({ ...project, name: name?.trim() || project.name, savedAt: null, ...(castStyle ? { castStyle } : {}) }, true);
|
|
438
444
|
appContext.shared.projectHandleRef.current = null;
|
|
439
445
|
track("scene:loaded", { scene_source: source });
|
|
440
446
|
return true;
|
|
@@ -504,35 +510,39 @@ export function useScenes(appContext) {
|
|
|
504
510
|
}
|
|
505
511
|
}
|
|
506
512
|
|
|
507
|
-
|
|
508
|
-
|
|
513
|
+
// The start screen's choices (previs mode, cast style); a field left out is
|
|
514
|
+
// not sent, so the command's own default applies.
|
|
515
|
+
const chosen = ({ previsMode, castStyle } = {}) => ({ ...(previsMode === undefined ? {} : { previsMode }), ...(castStyle === undefined ? {} : { castStyle }) });
|
|
516
|
+
|
|
517
|
+
function requestNewProject(authorized = false, choices = {}) {
|
|
518
|
+
if (!authorized) return runProject("project.new", Object.keys(chosen(choices)).length ? chosen(choices) : undefined);
|
|
509
519
|
if (domain.dirtyStore.read("projectDirty") && !window.confirm(ko("Discard unsaved changes and start a new project?", "저장되지 않은 변경사항을 버리고 새 프로젝트를 시작할까요?"))) return;
|
|
510
|
-
// The
|
|
520
|
+
// The choices made on the start screen ride on the name dialog until its
|
|
511
521
|
// submit commits the project.
|
|
512
|
-
setProjectNameDialog({ kind: "new", initialName: projectName ?? "My Project", ...(
|
|
522
|
+
setProjectNameDialog({ kind: "new", initialName: projectName ?? "My Project", ...chosen(choices) });
|
|
513
523
|
}
|
|
514
524
|
|
|
515
|
-
function newProject(name, authorized = false,
|
|
525
|
+
function newProject(name, authorized = false, choices = {}) {
|
|
516
526
|
if (!authorized) {
|
|
517
|
-
|
|
518
|
-
return runProject("project.new", { ...(typeof name === "string" ? { name } : {}), ...(
|
|
527
|
+
const pending = projectNameDialog?.kind === "new" ? chosen(projectNameDialog) : {};
|
|
528
|
+
return runProject("project.new", { ...(typeof name === "string" ? { name } : {}), ...pending, ...chosen(choices) });
|
|
519
529
|
}
|
|
520
|
-
if (typeof name !== "string") return requestNewProject(true,
|
|
530
|
+
if (typeof name !== "string") return requestNewProject(true, choices);
|
|
521
531
|
name = name.trim() || "My Project";
|
|
522
|
-
previsMode = normalizePrevisMode(previsMode);
|
|
532
|
+
const previsMode = normalizePrevisMode(choices.previsMode), castStyle = normalizeCastStyle(choices.castStyle);
|
|
523
533
|
setProjectNameDialog(null);
|
|
524
534
|
const fresh = createSceneDocument(ko("SCENE 01", "씬 01"));
|
|
525
535
|
storeWorkflowGraph(createWorkflowGraph());
|
|
526
536
|
appContext.shared.motionFullRef.current.clear();
|
|
527
|
-
domain.replaceDocument(fresh.scenes, fresh.activeSceneId, name, previsMode);
|
|
537
|
+
domain.replaceDocument(fresh.scenes, fresh.activeSceneId, name, previsMode, castStyle);
|
|
528
538
|
persistScenes(fresh.scenes, fresh.activeSceneId);
|
|
529
539
|
openScene(fresh.scenes[0], fresh.scenes);
|
|
530
540
|
appContext.shared.projectHandleRef.current = null;
|
|
531
541
|
clearStoredProjectHandle();
|
|
532
542
|
appContext.shared.projectSnapshotRef.current = collectProjectSnapshot(name);
|
|
533
543
|
domain.setDirty(false);
|
|
534
|
-
storeProjectSession(name, previsMode);
|
|
535
|
-
track("scene:created", { scene_source: "new_project", previs_mode: previsMode });
|
|
544
|
+
storeProjectSession(name, { previsMode, castStyle });
|
|
545
|
+
track("scene:created", { scene_source: "new_project", previs_mode: previsMode, cast_style: castStyle });
|
|
536
546
|
setProjectStartupOpen(false);
|
|
537
547
|
appContext.shared.setFirstSuccessGuideOpen(true);
|
|
538
548
|
domain.pendingCheckpoint = { clock: appContext.undoClock, name };
|
|
@@ -768,8 +778,8 @@ export function useScenes(appContext) {
|
|
|
768
778
|
return value;
|
|
769
779
|
};
|
|
770
780
|
if (id === "project.browse") { setProjectStartupOpen(false); setProjectBrowserOpen(true); return; }
|
|
771
|
-
if (id === "project.new") return newProject(args.name, true, { previsMode: args.previsMode });
|
|
772
|
-
if (id === "project.openStarter") return openStarterScene(args.id, args.source, context, args.name);
|
|
781
|
+
if (id === "project.new") return newProject(args.name, true, { previsMode: args.previsMode, castStyle: args.castStyle });
|
|
782
|
+
if (id === "project.openStarter") return openStarterScene(args.id, args.source, context, args.name, args.castStyle);
|
|
773
783
|
if (id === "project.restore") return restoreStoredProject({ handle: runtime("handleToken") }, context);
|
|
774
784
|
if (args.handleToken) return openProjectByHandle(runtime("handleToken"), context);
|
|
775
785
|
if (args.projectToken) { applyProject(runtime("projectToken"), true); return true; }
|
|
@@ -788,14 +798,14 @@ export function useScenes(appContext) {
|
|
|
788
798
|
domain.refreshDirty = () => refreshProjectDirty(false);
|
|
789
799
|
return {
|
|
790
800
|
...domain, applyExternalScene, loadLiveScenes, refreshProjectDirty,
|
|
791
|
-
scenes, activeSceneId, sceneSaveError, snapshotActiveScene, persistScenes, projectName, previsMode,
|
|
801
|
+
scenes, activeSceneId, sceneSaveError, snapshotActiveScene, persistScenes, projectName, previsMode, castStyle,
|
|
792
802
|
projectDirty, setProjectDirty: domain.setDirty, projectSaveState, setProjectSaveState, projectMenuOpen,
|
|
793
803
|
setProjectMenuOpen, projectBrowserOpen, setProjectBrowserOpen, projectNameDialog, setProjectNameDialog,
|
|
794
804
|
projectStartupOpen, setProjectStartupOpen, projectManifest, setProjectManifest, saveBlockedReasons,
|
|
795
805
|
setSaveBlockedReasons, workflowRevision, setWorkflowRevision, collectProjectSnapshot,
|
|
796
806
|
collectProjectSerialized, projectProblemsNotice, rehydrateProjectAssets, saveProject, applyProject,
|
|
797
807
|
exportAsAnimationProject,
|
|
798
|
-
openStarterScene: async (id, source = "starter", name = null) => (await runProject("project.openStarter", { id, source, ...(name ? { name } : {}) })).output?.opened === true,
|
|
808
|
+
openStarterScene: async (id, source = "starter", name = null, castStyle = undefined) => (await runProject("project.openStarter", { id, source, ...(name ? { name } : {}), ...(castStyle ? { castStyle } : {}) })).output?.opened === true,
|
|
799
809
|
openProject, openProjectByHandle, requestNewProject, newProject, restoreOffer,
|
|
800
810
|
setRestoreOffer, restoreStoredProject, flushScenes, openScene, selectSceneDocument,
|
|
801
811
|
createSceneDocumentFromUi, duplicateSceneDocumentFromUi, renameSceneDocumentFromUi,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { bucketMs, track, EXECUTION_TELEMETRY_VALUES } from "./analytics.js";
|
|
2
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
|
-
const AGENT_FAILURE_CODES = new Set(["aborted", "auth", "rate_limited", "tool_failed", "upstream", "unknown"]);
|
|
4
|
+
const AGENT_FAILURE_CODES = new Set(["aborted", "auth", "rate_limited", "tool_failed", "upstream", "network", "unknown"]);
|
|
5
5
|
function secureId() {
|
|
6
6
|
try {
|
|
7
7
|
if (!globalThis.crypto?.getRandomValues) return null;
|
|
@@ -41,13 +41,17 @@ function executionFailureCode(error, channel, fallbackCode = "unknown") {
|
|
|
41
41
|
return allowed.has(fallbackCode) ? fallbackCode : "unknown";
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** The closed `tool_name` for an MCP tool event: a live tool name, else "other". */
|
|
45
|
+
export function mcpToolName(name) {
|
|
46
|
+
return EXECUTION_TELEMETRY_VALUES.tool_name.has(name) ? name : "other";
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
export function mcpToolCategory(name) {
|
|
45
|
-
|
|
46
50
|
if (name === "capture_frame") return "frame_capture";
|
|
47
51
|
if (name === "set_camera" || name === "frame_shot" || name === "mark_camera_move" || name === "describe_camera_move") return "camera";
|
|
48
52
|
|
|
49
53
|
if (name === "save_project" || name === "open_project") return "project_io";
|
|
50
|
-
if (name === "describe_scene" || name === "describe_shot" || name === "render_prompt" || name === "live_status" || name === "compare_sculpt") return "read";
|
|
54
|
+
if (name === "describe_scene" || name === "describe_shot" || name === "render_prompt" || name === "live_status" || name === "compare_sculpt" || name === "studio_commands") return "read";
|
|
51
55
|
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";
|
|
52
56
|
return "other";
|
|
53
57
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// export:menu_opened: one event each time the Export flyout opens, by click or
|
|
2
|
+
// keyboard (the open state's false -> true edge), so reopening counts again.
|
|
3
|
+
// webcodecs is the synchronous constructor check only; whether a codec config
|
|
4
|
+
// is supported stays with the export attempt (unsupported_codec).
|
|
5
|
+
import { useEffect } from "react";
|
|
6
|
+
import { bucketCount, track } from "./analytics.js";
|
|
7
|
+
|
|
8
|
+
/** The closed properties for one open: surface, mode, webcodecs and the shot (storyboard: panel) count bucket. */
|
|
9
|
+
export function exportMenuOpenedProps({ embedMode, previsMode, shots }, scope = globalThis) {
|
|
10
|
+
const storyboard = previsMode === "storyboard";
|
|
11
|
+
const list = Array.isArray(shots) ? shots : [];
|
|
12
|
+
return {
|
|
13
|
+
surface: embedMode ? "embed" : "studio",
|
|
14
|
+
mode: storyboard ? "storyboard" : "animation",
|
|
15
|
+
webcodecs: typeof scope.VideoEncoder === "function" && typeof scope.VideoFrame === "function",
|
|
16
|
+
shots: bucketCount(storyboard ? list.filter((entry) => entry.kind === "still").length : list.length),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Keyed on the open flag only: the render that opens the flyout supplies the shell it reports.
|
|
21
|
+
export function useExportMenuTelemetry(open, shell) {
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!open) return;
|
|
24
|
+
try { track("export:menu_opened", exportMenuOpenedProps(shell)); } catch { /* telemetry is advisory */ }
|
|
25
|
+
}, [open]);
|
|
26
|
+
}
|