cozyclay 3.2.0 → 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 +87 -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 +16 -13
- package/bin/agent/agent-stream.mjs +26 -3
- package/bin/agent/main-prompt.mjs +1 -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-CnDkB5WO.css +1 -0
- package/dist/assets/app-DJt06Oyb.js +5144 -0
- package/dist/assets/{compile-worker-DRWRa9pr.js → compile-worker-D1pl6G7x.js} +5 -5
- 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 +2 -1
- package/src/App.jsx +37 -70
- 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/motion3.js +3 -1
- package/src/commands/objects.js +2 -37
- 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 +6 -5
- 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/find-bone.js +13 -0
- package/src/motion/fingers.js +170 -0
- package/src/motion/fingertip-pick.js +21 -0
- package/src/motion/ik-exposure.js +48 -0
- package/src/motion/ik.js +4 -11
- package/src/motion3/compiler/head-lead.js +4 -2
- package/src/motion3/hybrid-hint.js +19 -0
- package/src/motion3/load.js +19 -12
- package/src/motion3/qa/hints-contact.js +12 -0
- package/src/motion3/qa/patch.js +4 -1
- package/src/motion3/qa/verdict.js +7 -2
- package/src/motion3/repair.js +28 -8
- 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/pose-fade.js +7 -3
- package/src/posestudio.jsx +14 -27
- 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 +45 -69
- 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-BUjjuvm5.js +0 -1
- package/dist/assets/app-Dige7q3X.css +0 -1
- package/dist/assets/app-DxoI941o.js +0 -5144
- package/dist/assets/motion3-compile-B4SX4TGx.js +0 -1
- package/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/dist/assets/scene-asset-sync-LeI3eSnO.js +0 -13
- package/dist/assets/task-board-5PKn7ZaA.js +0 -15
- package/dist/assets/workflow-Dg9jCKOq.js +0 -179
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
|
@@ -33,6 +33,7 @@ import { isProxyFigure } from "../scenes.js";
|
|
|
33
33
|
import { refuseRigOnly } from "../character-kind.js";
|
|
34
34
|
import { DEFAULT_POSE, restoreBindPositions, applyPose, applyHipsOffset } from "../poses.js";
|
|
35
35
|
import { bakeIkDragKey, bakeIkDragRange, chainsChangedBy, clampDragTarget, ikDragRecord, ikDragTouch, solveDragTarget } from "../motion/ik-drag.js";
|
|
36
|
+
import { fingersOf, solveFingerGesture, restFingers } from "../motion/fingers.js";
|
|
36
37
|
import { ikKeyJson, ikTrackKeyFromJson } from "../motion/ik-key-json.js";
|
|
37
38
|
import { chooseIkEntryPose } from "../ik-camera.js";
|
|
38
39
|
import { applyRangePin, captureRangePinTarget, normalizeRangePin, rangePinTargetWorld, rangePinTracks, removeRangePinKeys } from "../motion/range-pin.js";
|
|
@@ -71,8 +72,7 @@ export function createMotionDomain(appContext, characters) {
|
|
|
71
72
|
let release = native.subscribe(notify), viewRevision = 0;
|
|
72
73
|
const documentStore = { ...Object.fromEntries(Object.keys(native).map(key => [key, (...args) => native[key](...args)])),
|
|
73
74
|
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); } };
|
|
74
|
-
const read = () => documentStore.read('motion');
|
|
75
|
-
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);
|
|
76
76
|
function materialize(take) {
|
|
77
77
|
if (take?.source && !takes.has(take.resourceId)) {
|
|
78
78
|
try { const compiled = compileTakeSource(take.source); if (compiled) takes.set(take.resourceId, compiled); }
|
|
@@ -110,7 +110,7 @@ export function createMotionDomain(appContext, characters) {
|
|
|
110
110
|
const resolved = resolveIkRig(rig); if (resolved) Object.assign(state, resolved, { rig });
|
|
111
111
|
if (savedRigs?.has(id)) appContext.shared.restoreExportRig(savedRigs.get(id));
|
|
112
112
|
else if (resolved) {
|
|
113
|
-
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));
|
|
114
114
|
else {
|
|
115
115
|
const character = cast.read().find(entry => entry.id === id);
|
|
116
116
|
restoreBindPositions(rig); applyPose(rig, { ...REST_BONES, ...(character?.pose ?? DEFAULT_POSE).bones });
|
|
@@ -253,7 +253,7 @@ export function createMotionDomain(appContext, characters) {
|
|
|
253
253
|
rigOnly(id, 'pose');
|
|
254
254
|
const rig = rigFor(id), resolved = rig && resolveIkRig(rig);
|
|
255
255
|
if (!resolved) throw new StudioProtocolError('TARGET_NOT_READY', 'The character rig is not loaded.');
|
|
256
|
-
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);
|
|
257
257
|
editKeys(id, state => {
|
|
258
258
|
state.tracked = new Set([...resolved.chains.keys(), ...resolved.fkJoints.keys()]);
|
|
259
259
|
ikBakeKeyframe(resolved.chains, state, at, resolved.fkJoints);
|
|
@@ -761,6 +761,7 @@ export function useMotion(appContext) {
|
|
|
761
761
|
ikDragRef.current = Object.assign(ikDragRecord(ikDragRef.current, appContext.shared.tlFrame), { characterId: appContext.shared.activeChar.id });
|
|
762
762
|
const touch = (id) => ikDragTouch(appContext.shared.ikStateRef.current, ikDragRef.current, id), activeId = appContext.shared.activeChar.id;
|
|
763
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);
|
|
764
765
|
if (kind === "chain") {
|
|
765
766
|
const chain = appContext.shared.ikStateRef.current.chains?.get(trackId);
|
|
766
767
|
if (!chain) return;
|
|
@@ -915,7 +916,7 @@ export function useMotion(appContext) {
|
|
|
915
916
|
restoreBindPositions(appContext.shared.activeRig);
|
|
916
917
|
// Reset-then-pose, the same shape the Character effect applies: unlisted
|
|
917
918
|
// joints return to rest instead of keeping stale limbs from the clip.
|
|
918
|
-
applyPose(appContext.shared.activeRig, { ...REST_BONES, ...pose.bones });
|
|
919
|
+
restFingers(ikFkJoints); applyPose(appContext.shared.activeRig, { ...REST_BONES, ...pose.bones });
|
|
919
920
|
// The hips' measured height rides into the bake: the hips FK joint keys
|
|
920
921
|
// its local position (p), so a crouched pose keys a crouched body.
|
|
921
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
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { POSER_LAYER } from "./dualview.jsx";
|
|
5
|
+
import { fingerTip, fingersOf } from "./motion/fingers.js";
|
|
6
|
+
import { claimFingertip } from "./motion/fingertip-pick.js";
|
|
7
|
+
import { handleColor } from "./pose-gizmo.js";
|
|
8
|
+
|
|
9
|
+
// A fingertip handle is a real sphere in the scene, a little wider than the finger, so it sits on the tip,
|
|
10
|
+
// shades with the lights and hides behind the body by depth like the mesh does.
|
|
11
|
+
const TIP_R = 0.009;
|
|
12
|
+
// The tip bone sits inside the fingertip; the sphere rides this far further along the last bone so most of
|
|
13
|
+
// it shows past the skin.
|
|
14
|
+
const TIP_PUSH = 0.008;
|
|
15
|
+
// Picking reaches a few pixels past the sphere's projected edge, nearest tip first.
|
|
16
|
+
const PICK_SLACK_PX = 6;
|
|
17
|
+
// A tip may sit this far under the first body surface and still be grabbed: the IK controls' exposure rule
|
|
18
|
+
// (ikControlIsExposed) with their default allowance, twice the handle radius (#913).
|
|
19
|
+
const TIP_VISIBILITY_DEPTH = TIP_R * 2;
|
|
20
|
+
|
|
21
|
+
/** The focused hand's fingers, and fingertip editing: a mode of the focused hand, on only while a hand is focused. */
|
|
22
|
+
export function useFingerEdit(ikFocus, fkJoints) {
|
|
23
|
+
const [fingerEdit, setFingerEdit] = useState(false);
|
|
24
|
+
// IkHandles fills this with its exposure probe; the fingertip pick asks it before taking a press.
|
|
25
|
+
const exposureProbeRef = useRef(null);
|
|
26
|
+
const side = ikFocus === "leftHand" ? "Left" : ikFocus === "rightHand" ? "Right" : null;
|
|
27
|
+
const handFingers = side ? [...(fingersOf(fkJoints)?.values() ?? [])].filter((finger) => finger.side === side) : [];
|
|
28
|
+
return { handFingers, fingerEditing: fingerEdit && handFingers.length > 0, setFingerEdit, exposureProbeRef };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fingertip handles for the focused hand. Dragging a tip solves that finger toward the pointer on a
|
|
33
|
+
* camera-facing plane (onSolve("finger", fingerId, point)); release bakes like any IK drag (onDragEnd).
|
|
34
|
+
* A tip the body hides cannot be grabbed (exposureProbeRef, the IK controls' rule); that press goes to IkHandles.
|
|
35
|
+
* The press is claimed in the window capture phase, ahead of IkHandles, so it never reads as an empty
|
|
36
|
+
* click that would drop the hand's focus.
|
|
37
|
+
*/
|
|
38
|
+
export function FingerHandles({ fingers: handFingers, enabled, onSolve, onDragEnd, exposureProbeRef }) {
|
|
39
|
+
const { camera, gl } = useThree();
|
|
40
|
+
const meshes = useRef(new Map());
|
|
41
|
+
const solveRef = useRef(onSolve), endRef = useRef(onDragEnd);
|
|
42
|
+
useEffect(() => { solveRef.current = onSolve; endRef.current = onDragEnd; });
|
|
43
|
+
const fingers = enabled ? handFingers : [];
|
|
44
|
+
|
|
45
|
+
useFrame(() => {
|
|
46
|
+
const along = new THREE.Vector3();
|
|
47
|
+
for (const finger of fingers) {
|
|
48
|
+
const mesh = meshes.current.get(finger.id);
|
|
49
|
+
if (!mesh) continue;
|
|
50
|
+
fingerTip(finger, mesh.position);
|
|
51
|
+
finger.bones[2].getWorldPosition(along);
|
|
52
|
+
mesh.userData.push = along.subVectors(mesh.position, along).normalize().multiplyScalar(TIP_PUSH).clone();
|
|
53
|
+
mesh.position.add(mesh.userData.push);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const active = fingers.length > 0;
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!active) return undefined;
|
|
60
|
+
const el = gl.domElement, ndc = new THREE.Vector2(), hit = new THREE.Vector3(), ray = new THREE.Raycaster();
|
|
61
|
+
let drag = null;
|
|
62
|
+
const toNdc = (ev) => {
|
|
63
|
+
const rect = el.getBoundingClientRect();
|
|
64
|
+
return ndc.set(((ev.clientX - rect.left) / rect.width) * 2 - 1, -((ev.clientY - rect.top) / rect.height) * 2 + 1);
|
|
65
|
+
};
|
|
66
|
+
const onMove = (ev) => {
|
|
67
|
+
if (!drag) return;
|
|
68
|
+
ray.setFromCamera(toNdc(ev), camera);
|
|
69
|
+
if (!ray.ray.intersectPlane(drag.plane, hit)) return;
|
|
70
|
+
drag.moved = true;
|
|
71
|
+
solveRef.current?.("finger", drag.id, hit.clone().sub(drag.push));
|
|
72
|
+
};
|
|
73
|
+
const release = () => {
|
|
74
|
+
window.removeEventListener("pointermove", onMove, true);
|
|
75
|
+
window.removeEventListener("pointerup", onUp, true);
|
|
76
|
+
window.removeEventListener("pointercancel", onUp, true);
|
|
77
|
+
};
|
|
78
|
+
const onUp = () => {
|
|
79
|
+
const moved = drag?.moved;
|
|
80
|
+
drag = null;
|
|
81
|
+
release();
|
|
82
|
+
if (moved) endRef.current?.();
|
|
83
|
+
};
|
|
84
|
+
const onDown = (ev) => {
|
|
85
|
+
if (ev.button !== 0 || ev.target !== el) return;
|
|
86
|
+
const rect = el.getBoundingClientRect(), centre = new THREE.Vector3(), edge = new THREE.Vector3();
|
|
87
|
+
const right = new THREE.Vector3().setFromMatrixColumn(camera.matrixWorld, 0);
|
|
88
|
+
const toScreen = (point) => { point.project(camera); return [rect.left + ((point.x + 1) / 2) * rect.width, rect.top + ((1 - point.y) / 2) * rect.height]; };
|
|
89
|
+
const candidates = [...meshes.current].filter(([, mesh]) => mesh).map(([id, mesh]) => {
|
|
90
|
+
const [cx, cy] = toScreen(centre.copy(mesh.position));
|
|
91
|
+
const [ex] = toScreen(edge.copy(mesh.position).addScaledVector(right, TIP_R));
|
|
92
|
+
return { id, mesh, gap: Math.hypot(cx - ev.clientX, cy - ev.clientY), reach: Math.abs(ex - cx) + PICK_SLACK_PX };
|
|
93
|
+
});
|
|
94
|
+
const exposed = exposureProbeRef?.current;
|
|
95
|
+
const picked = claimFingertip(ev, candidates, ({ mesh }) => !exposed || exposed(mesh.position, TIP_VISIBILITY_DEPTH));
|
|
96
|
+
if (!picked) return;
|
|
97
|
+
const normal = camera.getWorldDirection(new THREE.Vector3());
|
|
98
|
+
drag = { id: picked.id, moved: false, push: picked.mesh.userData.push?.clone() ?? new THREE.Vector3(), plane: new THREE.Plane().setFromNormalAndCoplanarPoint(normal, picked.mesh.position) };
|
|
99
|
+
window.addEventListener("pointermove", onMove, true);
|
|
100
|
+
window.addEventListener("pointerup", onUp, true);
|
|
101
|
+
window.addEventListener("pointercancel", onUp, true);
|
|
102
|
+
};
|
|
103
|
+
window.addEventListener("pointerdown", onDown, true);
|
|
104
|
+
return () => {
|
|
105
|
+
window.removeEventListener("pointerdown", onDown, true);
|
|
106
|
+
release();
|
|
107
|
+
};
|
|
108
|
+
}, [active, camera, gl, exposureProbeRef]);
|
|
109
|
+
|
|
110
|
+
if (!active) return null;
|
|
111
|
+
return (
|
|
112
|
+
<group>
|
|
113
|
+
{fingers.map((finger) => (
|
|
114
|
+
<mesh
|
|
115
|
+
key={finger.id}
|
|
116
|
+
raycast={() => {}}
|
|
117
|
+
ref={(mesh) => {
|
|
118
|
+
if (mesh) { mesh.layers.set(POSER_LAYER); meshes.current.set(finger.id, mesh); }
|
|
119
|
+
else meshes.current.delete(finger.id);
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<sphereGeometry args={[TIP_R, 20, 14]} />
|
|
123
|
+
<meshStandardMaterial color={handleColor(finger.trackIds[0])} roughness={0.45} metalness={0} />
|
|
124
|
+
</mesh>
|
|
125
|
+
))}
|
|
126
|
+
</group>
|
|
127
|
+
);
|
|
128
|
+
}
|