cozyclay 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +211 -36
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +4 -4
- package/bin/live/cli.mjs +2 -3
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +2 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-B41FmPYI.js +1 -0
- package/dist/assets/app-Do-Og5Pu.css +1 -0
- package/dist/assets/app-DwP-tl2x.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
- package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
- package/dist/assets/workflow-Br-K7Srs.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +789 -263
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +184 -6
- package/src/ardy/timeline.jsx +113 -28
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +0 -3
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +6 -7
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +15 -2
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +79 -28
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +49 -4
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +58 -9
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +58 -14
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/TopBar.jsx +17 -3
- package/src/shell/ViewportToolbar.jsx +18 -3
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +186 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +56 -23
- package/src/studio-app-binding.js +52 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +92 -9
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +106 -18
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +94 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-BxBLC6GT.js +0 -4876
- package/dist/assets/app-CJdxvnrl.css +0 -1
- package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
- package/dist/assets/workflow-B5XZwVfe.js +0 -179
|
@@ -5,10 +5,12 @@ import { createCommandBus } from "./command-bus.js";
|
|
|
5
5
|
import { readElementDocument, elementReadback, elementPatchArgs, elementTarget, elementPatchReceipt } from "./commands/elements.js";
|
|
6
6
|
import { physicsKeyStamp } from "./ardy/physics-review.js";
|
|
7
7
|
import { shotAtFrame } from "./cuts.js";
|
|
8
|
+
import { isProxyFigure } from "./scenes.js";
|
|
8
9
|
import { CUTOUT_KIND, MESH_KIND, OBJECT_LIBRARY, supportHeightForObject } from "./scene-objects.js";
|
|
9
10
|
import { buildStudioContext, physicsFingerprintInput, studioEntityCursor, validateStudioCursor } from "./studio-agent-context.js";
|
|
10
|
-
import { createStudioCommandJournal, framingChecks, placementChecks, studioObjectCatalogue } from "./studio-agent-commands.js";
|
|
11
|
+
import { createStudioCommandJournal, entityWorldBounds, framingChecks, localTransformReadback, placementChecks, studioObjectCatalogue } from "./studio-agent-commands.js";
|
|
11
12
|
import { verifyInstalledTake } from "./studio-agent-motion.js";
|
|
13
|
+
import { shotGeometryFacts } from "./studio-geometry-facts.js";
|
|
12
14
|
import { STUDIO_TOOL_FAMILIES, StudioProtocolError, validateReceipt, validateStudioCommand, validateStudioIdentity } from "./studio-agent-protocol.js";
|
|
13
15
|
import { CONTACT_SHEET_LAYOUT, buildContactSheet, sampleContactSheetFrames } from "./studio-contact-sheet.js";
|
|
14
16
|
|
|
@@ -120,12 +122,15 @@ export function createStudioAppBinding(ports) {
|
|
|
120
122
|
position: { x: c.x, y: c.y ?? 0, z: c.z }, yawDeg: c.rot ?? 0, scale: c.scale ?? 1, tint: c.tint ?? null, modelId: c.model ?? null,
|
|
121
123
|
motion: { takeId: t?.motion?.studioTakeId ?? null, frames: t?.motion?.frames ?? 0,
|
|
122
124
|
ikKeyCount: t?.ikState?.keys.size ?? 0, promptBlockCount: c.layer?.promptClips?.length ?? 0 },
|
|
123
|
-
capabilities: { rigReady: Boolean(t?.rig), ik: Boolean(t?.rig?.userData?.poseBind), measuredFeet: false }
|
|
124
|
-
|
|
125
|
+
capabilities: { rigReady: Boolean(t?.rig), ik: Boolean(t?.rig?.userData?.poseBind), measuredFeet: false },
|
|
126
|
+
characterKind: isProxyFigure(c) ? "proxy" : "rig", ...(isProxyFigure(c) ? { posture: c.posture ?? "stand" } : {}) };
|
|
127
|
+
}), ...s.objects.map(o => { const local = localTransformReadback(o, s.objects); return { id: o.id, kind: "object", token: tokens.get(o.id).token, name: o.name || o.id,
|
|
125
128
|
position: { x: o.x, y: o.y ?? 0, z: o.z }, yawDeg: o.rot ?? 0,
|
|
126
129
|
rotationDeg: { x: o.rotX ?? 0, y: o.rot ?? 0, z: o.rotZ ?? 0 }, scale: { x: o.scaleX, y: o.scaleY, z: o.scaleZ },
|
|
127
130
|
renderer: o.renderer, color: o.color ?? null, ...(o.assetId ? { assetId: o.assetId } : {}),
|
|
128
|
-
parentId: o.parent ?? null, attachment: o.attach ?? null, pathPointCount: o.path?.points.length ?? 0
|
|
131
|
+
parentId: o.parent ?? null, attachment: o.attach ?? null, pathPointCount: o.path?.points.length ?? 0,
|
|
132
|
+
// the dots on the route, with the lean (degrees) a mark carries
|
|
133
|
+
...(o.path?.marks?.length ? { pathMarks: o.path.marks.map(mark => typeof mark === "number" ? mark : { t: mark.t, ...(mark.bank !== undefined ? { bank: mark.bank } : {}), ...(mark.pitch !== undefined ? { pitch: mark.pitch } : {}) }) } : {}), ...(local ? { local } : {}) }; })];
|
|
129
134
|
}
|
|
130
135
|
const frameRange = row => ({ startFrame: row.startFrame, endFrameExclusive: row.endFrame + 1 });
|
|
131
136
|
function assetList(s) {
|
|
@@ -147,7 +152,7 @@ export function createStudioAppBinding(ports) {
|
|
|
147
152
|
return buildStudioContext({ schema: "studio-context-v1", host: { surface: "studio", ...s.host, workspaceHandle: s.workspaceHandle },
|
|
148
153
|
revision: { scene: s.revision, physics: s.physicsRevision, view: s.viewRevision },
|
|
149
154
|
units: { distance: "m", angle: "deg", up: "+Y", yawZero: "+Z", yawPositiveToward: "+X", pivot: "base", fps: 24, rangeEnd: "exclusive" },
|
|
150
|
-
scene: { name: s.sceneName, aspect: s.aspect, floorY: 0, frameCount: s.frameCount, objectCount: s.objects.length, characterCount: s.characters.length },
|
|
155
|
+
scene: { name: s.sceneName, aspect: s.aspect, floorY: 0, frameCount: s.frameCount, objectCount: s.objects.length, characterCount: s.characters.length, ...(s.previsMode ? { previsMode: s.previsMode } : {}) },
|
|
151
156
|
selection: s.selection, activeCharacterId: s.activeCharacterId, view: s.view,
|
|
152
157
|
shot: shot ? { id: shot.id, name: shot.name, range: range(shot), mode: shot.camera?.mode ?? "keys" } : null, camera: s.camera,
|
|
153
158
|
// buildStudioContext selects the detailed rows and writes the real page.
|
|
@@ -155,7 +160,8 @@ export function createStudioAppBinding(ports) {
|
|
|
155
160
|
shots: s.shots.map(row => ({ id: row.id, name: row.name, range: range(row), keyCount: row.cameraKeys.length })), shotsTruncated: false,
|
|
156
161
|
assets: assetList(s), recentReceipts: [...receipts.values()].filter(r => r.ok).reverse().slice(0, 3).map(r => ({ id: r.receiptId, summary: r.status, canUndoDirect: ports.canUndo(r) })),
|
|
157
162
|
jobs: [], capabilities: { profile: "studio-slice-1", tools: STUDIO_TOOL_FAMILIES,
|
|
158
|
-
rigReady: Boolean(s.targets.get(s.activeCharacterId)?.rig), cameraReady: Boolean(s.camera), bridgeReady: s.bridgeReady
|
|
163
|
+
rigReady: Boolean(s.targets.get(s.activeCharacterId)?.rig), cameraReady: Boolean(s.camera), bridgeReady: s.bridgeReady,
|
|
164
|
+
...(s.previsMode ? { storyboard: s.previsMode === "storyboard" } : {}) },
|
|
159
165
|
// Every registered command; buildStudioContext keeps only its id/label index.
|
|
160
166
|
...(registry ? { actions: registry.list() } : {}) });
|
|
161
167
|
}
|
|
@@ -182,19 +188,32 @@ export function createStudioAppBinding(ports) {
|
|
|
182
188
|
return s;
|
|
183
189
|
}
|
|
184
190
|
/** Actual state of one action target after it ran. */
|
|
185
|
-
function actionReadback(id, s) {
|
|
191
|
+
function actionReadback(id, s, compact = false) {
|
|
186
192
|
const patched = Object.entries(s.document).flatMap(([kind, value]) => {
|
|
187
193
|
const target = elementTarget(kind, value, id, s.host.sceneId);
|
|
188
194
|
return target ? elementReadback(kind, target) : [];
|
|
189
195
|
});
|
|
190
196
|
if (id === s.host.sceneId) return { selection: s.selection, activeCharacterId: s.activeCharacterId, shotId: s.selectedShotId, view: s.view,
|
|
191
197
|
...(patched.length ? { patched } : {}) };
|
|
192
|
-
|
|
198
|
+
const object = s.objects.find(row => row.id === id), entity = object ?? s.characters.find(row => row.id === id);
|
|
199
|
+
// Beyond the transform an object's patched paths already carry: the world box it occupies and, for a
|
|
200
|
+
// group, who hangs off it. `compact` (a receipt near its size limit) drops back to the paths alone.
|
|
201
|
+
// A character's box means cloning and posing its rig, too dear for every receipt: inspect or verify has it.
|
|
202
|
+
const extras = () => {
|
|
203
|
+
if (!object || compact) return {};
|
|
204
|
+
const childIds = s.objects.filter(row => row.parent === id).map(row => row.id);
|
|
205
|
+
return { bounds: entityWorldBounds(object, { ...s, frame: s.view.frame }, { bounds: ports.bounds }),
|
|
206
|
+
...(childIds.length ? { childIds: childIds.slice(0, 12), ...(childIds.length > 12 ? { childCount: childIds.length } : {}) } : {}) };
|
|
207
|
+
};
|
|
208
|
+
if (patched.length) return { patched, ...extras() };
|
|
193
209
|
const shot = s.shots.find(row => row.id === id);
|
|
194
210
|
if (shot) return { name: shot.name || shot.id, range: { startFrame: shot.startFrame, endFrameExclusive: shot.endFrame + 1 } };
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
211
|
+
if (!entity) return { removed: true };
|
|
212
|
+
const brief = { name: entity.name || entity.subject || entity.id, position: { x: entity.x, y: entity.y ?? 0, z: entity.z } };
|
|
213
|
+
if (compact) return brief;
|
|
214
|
+
if (!object) return { ...brief, yawDeg: entity.rot ?? 0, scale: entity.scale ?? 1, ...extras() };
|
|
215
|
+
return { ...brief, rotationDeg: { x: object.rotX ?? 0, y: object.rot ?? 0, z: object.rotZ ?? 0 }, scale: { x: object.scaleX, y: object.scaleY, z: object.scaleZ },
|
|
216
|
+
parentId: object.parent ?? null, ...extras() };
|
|
198
217
|
}
|
|
199
218
|
/** One registered Studio action, run for the agent through the same
|
|
200
219
|
* registry the UI controls call. A mutation is bound to the native history
|
|
@@ -220,7 +239,7 @@ export function createStudioAppBinding(ports) {
|
|
|
220
239
|
}
|
|
221
240
|
function runAction(request, args) {
|
|
222
241
|
const { name: _name, args: _args, ...options } = request;
|
|
223
|
-
const result = commandBus().run(args.action, args.args, { ...options, origin: "agent", confirmationToken: args.confirmationToken ?? request.confirmationToken });
|
|
242
|
+
const result = commandBus().run(args.action, args.args, { ...options, origin: "agent", turnId: request.turnId, confirmationToken: args.confirmationToken ?? request.confirmationToken });
|
|
224
243
|
const answer = receipt => receipt.nextHost ? { ...receipt, host: receipt.nextHost } : receipt;
|
|
225
244
|
return result?.then ? result.then(answer) : answer(result);
|
|
226
245
|
}
|
|
@@ -260,9 +279,14 @@ export function createStudioAppBinding(ports) {
|
|
|
260
279
|
// Only a check nothing could compute is unsupported, and says why; a motion
|
|
261
280
|
// check computed for some characters still names the ones it skipped.
|
|
262
281
|
const scene = readCommand(), entityIds = ids => ids.filter(id => scene.objects.some(o => o.id === id) || scene.characters.some(c => c.id === id));
|
|
263
|
-
|
|
282
|
+
// A receipt's checks are evidence only for what they measured: every receipt carries a coverage tag, but
|
|
283
|
+
// only an arrange/frame receipt carries overlap or framing numbers. Anything else is measured here.
|
|
284
|
+
const measuredBy = { placement: "overlapIds", framing: "screenFraction" }, unmeasured = receipt && args.checks.some(check => measuredBy[check] && receipt.checks?.[measuredBy[check]] === undefined);
|
|
285
|
+
const measured = args.targets ? entityIds(args.targets) : unmeasured ? entityIds(receipt.affectedIds) : null;
|
|
286
|
+
// A framing check also carries the cut's geometry, read from the scene: side of the action line
|
|
287
|
+
// and sight lines per shot, the receipt's or targets' shots first (8 at most).
|
|
264
288
|
const result = { receiptId: receipt?.receiptId ?? null, revision: s.revision, evidenceRevision, stale: evidenceRevision !== s.revision, checks: measured ? { coverage: "current-scene-targets" } : receipt.checks,
|
|
265
|
-
verification: receipt?.verification ?? null, semanticStatus: "unavailable", visualRefs: [], unsupportedChecks: [], unsupportedReasons: {} };
|
|
289
|
+
verification: receipt?.verification ?? null, semanticStatus: "unavailable", visualRefs: [], unsupportedChecks: [], unsupportedReasons: {}, ...(args.checks.includes("framing") ? { geometry: shotGeometryFacts(scene, { first: receipt ? receipt.affectedIds : args.targets, ports: { bounds: ports.bounds } }) } : {}) };
|
|
266
290
|
const reasons = result.unsupportedReasons, verified = [], skipped = [], pending = [];
|
|
267
291
|
for (const check of args.checks.filter(check => check !== "motion" && measured)) {
|
|
268
292
|
if (!measured.length) { reasons[check] = "No target is an object or character in the current scene."; continue; }
|
|
@@ -281,6 +305,8 @@ export function createStudioAppBinding(ports) {
|
|
|
281
305
|
.then(verification => { verified[index] = verification; }, error => { if (!(error instanceof StudioProtocolError)) throw error; skipped.push(`${name}: ${error.message}`); }));
|
|
282
306
|
});
|
|
283
307
|
}
|
|
308
|
+
// A frame is the shot camera's view; a plan is the Top View, the set from above
|
|
309
|
+
// with names and the shot camera's wedge.
|
|
284
310
|
if (args.visual !== "none") {
|
|
285
311
|
if (args.visual === "contact_sheet") {
|
|
286
312
|
// One image of frames across the range, each rendered through the export
|
|
@@ -288,7 +314,7 @@ export function createStudioAppBinding(ports) {
|
|
|
288
314
|
const frames = sampleContactSheetFrames(args.range, s.frameCount), sheet = buildContactSheet(frames, ports.renderFrameBuffer), imageId = crypto.randomUUID();
|
|
289
315
|
images.set(imageId, { dataUrl: ports.encodePng(sheet.data, sheet), width: sheet.width, height: sheet.height, frames, layout: CONTACT_SHEET_LAYOUT, revision: s.revision, receiptId: result.receiptId });
|
|
290
316
|
result.visualRefs.push({ imageId, frames, layout: CONTACT_SHEET_LAYOUT });
|
|
291
|
-
} else { const capture = ports.capture(); const imageId = crypto.randomUUID(); images.set(imageId, { ...capture, revision: s.revision, receiptId: result.receiptId }); result.visualRefs.push({ imageId }); }
|
|
317
|
+
} else { const capture = args.visual === "plan" ? (ports.capturePlan ?? (() => fail("CAPABILITY_MISSING", "This editor cannot render the top view.")))() : ports.capture(); const imageId = crypto.randomUUID(); images.set(imageId, { ...capture, revision: s.revision, receiptId: result.receiptId }); result.visualRefs.push({ imageId, ...(args.visual === "plan" ? { kind: "plan" } : {}) }); }
|
|
292
318
|
}
|
|
293
319
|
const finish = () => {
|
|
294
320
|
const computed = verified.filter(Boolean);
|
|
@@ -312,17 +338,20 @@ export function createStudioAppBinding(ports) {
|
|
|
312
338
|
read_studio_context(request) { const c = context(); if (!same(validateStudioIdentity(request.host), owner)) fail("STALE_SCENE", "This is not the requested document."); return c; },
|
|
313
339
|
inspect_studio(args) {
|
|
314
340
|
const command = validateStudioCommand({ name: "inspect_studio", args }); const c = context();
|
|
315
|
-
// Every scope carries the
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
341
|
+
// Every scope carries the revision and document identity, not the whole
|
|
342
|
+
// context (the turn already holds that, and resending it per call is what
|
|
343
|
+
// the model pays for): the revision is what the agent's next command is
|
|
344
|
+
// admitted at, so a scope without it leaves that admission stale.
|
|
345
|
+
const head = { revision: c.revision, host: (({ workspaceId, documentEpoch, sceneId, sceneEpoch }) => ({ workspaceId, documentEpoch, sceneId, sceneEpoch }))(c.host) };
|
|
346
|
+
if (command.args.scope === "catalogue") return { ...head, ...studioObjectCatalogue() };
|
|
347
|
+
if (command.args.scope === "document") return { ...head, scope: "document",
|
|
319
348
|
...readElementDocument(refresh().document, command.args, c.host.sceneId) };
|
|
320
349
|
// Discovery for run_action: every registered action with its label, kind,
|
|
321
350
|
// exposure and availability (the reason when unavailable). Schemas are on
|
|
322
351
|
// request: ids answer those actions' full declarations, input included.
|
|
323
352
|
if (command.args.scope === "actions") {
|
|
324
|
-
const actions = ports.actions?.()?.list() ?? [];
|
|
325
|
-
return {
|
|
353
|
+
const actions = (ports.actions?.()?.list() ?? []).filter(row => !row.agentHidden);
|
|
354
|
+
return { ...head, actions: command.args.ids ? actions.filter(row => command.args.ids.includes(row.id)) : actions.map(({ input, description, ...row }) => row) };
|
|
326
355
|
}
|
|
327
356
|
const s = refresh();
|
|
328
357
|
const wanted = row => (!args.ids || args.ids.includes(row.id)) && (!args.query || Boolean(row.name?.includes(args.query)));
|
|
@@ -330,14 +359,14 @@ export function createStudioAppBinding(ports) {
|
|
|
330
359
|
const select = { shot: ["shot"], motion: ["motion", "character"] }[command.args.scope];
|
|
331
360
|
const ids = args.ids ?? (command.args.scope === "selection" ? [s.selection?.id ?? s.host.sceneId]
|
|
332
361
|
: args.query && select ? (command.args.scope === "shot" ? s.shots : entityProjection(s).filter(row => row.kind === "character")).filter(wanted).map(row => row.id) : undefined);
|
|
333
|
-
return {
|
|
362
|
+
return { ...head, scope: "document", ...readElementDocument(s.document, { ...command.args, ids, select }, c.host.sceneId) };
|
|
334
363
|
}
|
|
335
364
|
// Build each page from the same complete authoritative projection; never
|
|
336
365
|
// page by slicing an already-truncated Send context.
|
|
337
366
|
// Stable id order, so an offset cursor survives unrelated edits.
|
|
338
367
|
const filtered = entityProjection(s).filter(wanted).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
339
368
|
const offset = args.cursor ? validateStudioCursor(args.cursor, c) : 0, limit = command.args.limit;
|
|
340
|
-
return {
|
|
369
|
+
return { ...head, entities: filtered.slice(offset, offset + limit), total: filtered.length,
|
|
341
370
|
nextCursor: offset + limit < filtered.length ? studioEntityCursor(c, offset + limit) : null };
|
|
342
371
|
},
|
|
343
372
|
operate_studio: request => execute({ ...request, name: "operate_studio" }),
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// shot render shrinks, and where it lands on the one image the agent sees.
|
|
3
3
|
// Pure over RGBA byte buffers; App.jsx supplies the render and the encoder.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const CONTACT_SHEET_COLUMNS = 3;
|
|
6
|
+
const CONTACT_SHEET_ROWS = 2;
|
|
7
7
|
export const CONTACT_SHEET_LAYOUT = "3x2 row-major";
|
|
8
|
-
|
|
8
|
+
const CONTACT_SHEET_MAX_SIDE = 1600;
|
|
9
9
|
|
|
10
10
|
/** Up to 6 frames spread evenly over a half-open range, first and last
|
|
11
11
|
* included; "whole_clip" (or no range) is timeline frames 0..frameCount-1. */
|
package/src/studio-elements.js
CHANGED
|
@@ -22,7 +22,11 @@ const entries = [
|
|
|
22
22
|
{ path: "character.scale", type: "number", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry", min: 0.2, max: 3 },
|
|
23
23
|
{ path: "character.subject", type: "string", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry" },
|
|
24
24
|
{ path: "character.hidden", type: "boolean", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry" },
|
|
25
|
-
{ path: "character.
|
|
25
|
+
{ path: "character.parent", type: "id", nullable: true, persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry", note: "scene object id the character is grouped under and rides; null stands in the world" },
|
|
26
|
+
// The model and posture enums mirror scenes.js CHARACTER_KIND_IDS and
|
|
27
|
+
// POSTURES; they are literal because scenes.js imports this module.
|
|
28
|
+
{ path: "character.model", type: "enum", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry", enum: ["y-bot-tpose", "x-bot-tpose", "proxy-figure"] },
|
|
29
|
+
{ path: "character.posture", type: "enum", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry", enum: ["stand", "sit", "lie"] },
|
|
26
30
|
{ path: "character.tint", type: "color", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry" },
|
|
27
31
|
{ path: "character.identityImage", type: "image", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry", note: "data:image only" },
|
|
28
32
|
{ path: "character.pose", type: "id", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry" },
|
|
@@ -32,12 +36,13 @@ const entries = [
|
|
|
32
36
|
{ path: "character.motionRef.motionId", type: "id", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry" },
|
|
33
37
|
{ path: "character.sessionMotion", type: "array", persisted: false, undoDomain: "cast", normalizer: "createCharacterEntry", note: "dropped by createCharacterEntry" },
|
|
34
38
|
{ path: "character.ikKeys", type: "array", persisted: false, undoDomain: "cast", normalizer: "createCharacterEntry", actions: ["character.setIkKey", "character.removeIkKey", "character.clearIkKeys"], note: "dropped by createCharacterEntry; keys travel as JSON quaternions/positions" },
|
|
35
|
-
{ path: "object.renderer", type: "enum", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", enum: ["cube", "sphere", "capsule", "cylinder", "cone", "plane", "chair", "car", "small-plane"] },
|
|
39
|
+
{ path: "object.renderer", type: "enum", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", enum: ["cube", "sphere", "capsule", "cylinder", "cone", "plane", "chair", "car", "small-plane", "empty"] },
|
|
36
40
|
{ path: "object.position", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: -240, y: 0, z: -240 }, max: { x: 240, y: 240, z: 240 }, note: "y is the object's base: y=0 rests on the floor; height/supportY rise from it" },
|
|
37
41
|
{ path: "object.rotation", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: -180, y: -180, z: -180 }, max: { x: 180, y: 180, z: 180 }, note: "rot/rotX/rotZ degrees, wrapped at the upper bound" },
|
|
38
42
|
{ path: "object.scale", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: 0.1, y: 0.1, z: 0.1 }, max: { x: 100, y: 100, z: 100 } },
|
|
39
43
|
{ path: "object.name", type: "string", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject" },
|
|
40
44
|
{ path: "object.color", type: "color", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject" },
|
|
45
|
+
{ path: "object.opacity", type: "number", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: 0.05, max: 1, note: "1 is solid; lower is see-through, e.g. 0.3 for clear glass" },
|
|
41
46
|
{ path: "object.parent", type: "id", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject" },
|
|
42
47
|
{ path: "object.attach", type: "id", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", actions: ["object.attach", "object.detach"], note: "carried by a character's root or bone; channels convert so the prop stays put" },
|
|
43
48
|
{ path: "object.path", type: "array", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject" },
|
|
@@ -59,6 +64,10 @@ const entries = [
|
|
|
59
64
|
{ path: "shot.cameraKeys", type: "array", persisted: true, undoDomain: "shot", normalizer: null, frameMin: 0 },
|
|
60
65
|
{ path: "shot.cameraRail", type: "array", persisted: true, undoDomain: "shot", normalizer: "repairCamera", actions: ["shot.setCameraRail", "shot.clearCameraRail"], note: "rail points; crane/dolly timing follow the shot's camera block" },
|
|
61
66
|
{ path: "shot.targetModel", type: "id", persisted: true, undoDomain: "shot", normalizer: "repairCamera" },
|
|
67
|
+
{ path: "shot.kind", type: "enum", persisted: true, undoDomain: "shot", normalizer: null, readOnly: true, enum: ["clip", "still"], note: "clip or still; a still holds one picture for endFrame - startFrame + 1 frames" },
|
|
68
|
+
{ path: "shot.caption", type: "string", persisted: true, undoDomain: "shot", normalizer: null, readOnly: true, note: "trimmed, at most 500 characters" },
|
|
69
|
+
{ path: "shot.cast", type: "array", persisted: true, undoDomain: "shot", normalizer: null, readOnly: true, note: "per-shot placement overrides keyed by character id: { x, z, rot, posture?, pose? }" },
|
|
70
|
+
{ path: "shot.stylizedAssetId", type: "id", nullable: true, persisted: true, undoDomain: "shot", normalizer: null, readOnly: true, note: "image asset id only" },
|
|
62
71
|
{ path: "shot.freeCamera", type: "vec3", persisted: false, undoDomain: "shot", normalizer: null, note: "transient until keyed" },
|
|
63
72
|
{ path: "scenes", type: "array", persisted: true, undoDomain: null, normalizer: null, actions: ["scene.create", "scene.duplicate", "scene.rename", "scene.delete", "scene.switch"], note: "the project's scene list and the open scene; outside the undo history" },
|
|
64
73
|
{ path: "project", type: "string", persisted: true, undoDomain: null, normalizer: null, actions: ["project.save"], note: "save to the current project file; opening one needs the user's file picker" },
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Shot geometry the Studio agent reads as facts instead of judging from a picture:
|
|
2
|
+
// where the cast stands, which side of the line between the two leading
|
|
3
|
+
// characters each shot camera sits on (the 180-degree rule), and which props
|
|
4
|
+
// stand between a camera and its subject. Pure: no renderer, no React.
|
|
5
|
+
import { entityWorldBounds } from './studio-agent-commands.js';
|
|
6
|
+
import { isEffectivelyHidden } from './scene-objects.js';
|
|
7
|
+
import { shotAtFrame } from './cuts.js';
|
|
8
|
+
|
|
9
|
+
export const GEOMETRY_FACTS_LIMITS = Object.freeze({ shots: 8, subjects: 24, pairs: 24, occluders: 8 });
|
|
10
|
+
/** Closer to the action line than this (m) is "on-axis", neither side. */
|
|
11
|
+
const ON_AXIS_M = 0.05;
|
|
12
|
+
const EPS = 1e-9;
|
|
13
|
+
const round = v => Math.round(v * 1000) / 1000 + 0;
|
|
14
|
+
const point = p => ({ x: round(p.x), y: round(p.y ?? 0), z: round(p.z) });
|
|
15
|
+
const visibleCharacters = state => state.characters.filter(c => !isEffectivelyHidden(c, state.objects, state.characters));
|
|
16
|
+
|
|
17
|
+
/** Does the segment p0->p1 pass through the box {min, max}? Slab test over t in [0, 1]. */
|
|
18
|
+
export function segmentIntersectsAabb(p0, p1, box) {
|
|
19
|
+
let t0 = 0, t1 = 1;
|
|
20
|
+
for (const a of ['x', 'y', 'z']) {
|
|
21
|
+
const d = p1[a] - p0[a];
|
|
22
|
+
if (Math.abs(d) < EPS) {
|
|
23
|
+
if (p0[a] < box.min[a] || p0[a] > box.max[a]) return false;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
let near = (box.min[a] - p0[a]) / d, far = (box.max[a] - p0[a]) / d;
|
|
27
|
+
if (near > far) [near, far] = [far, near];
|
|
28
|
+
t0 = Math.max(t0, near); t1 = Math.min(t1, far);
|
|
29
|
+
if (t0 > t1) return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
const contains = (box, p) => ['x', 'y', 'z'].every(a => p[a] >= box.min[a] && p[a] <= box.max[a]);
|
|
34
|
+
|
|
35
|
+
/** Which side of the horizontal line a->b the point p stands on, seen from above
|
|
36
|
+
* (+Y up) looking along a->b: the sign of (axis x (p - a)).y with the axis
|
|
37
|
+
* normalised, so the threshold is metres. Negative is right, positive left;
|
|
38
|
+
* within ON_AXIS_M it is "on-axis". Null when a and b share a spot. */
|
|
39
|
+
export function sideOfAxis(a, b, p) {
|
|
40
|
+
const ax = b.x - a.x, az = b.z - a.z, length = Math.hypot(ax, az);
|
|
41
|
+
if (length < EPS) return null;
|
|
42
|
+
const offset = (az * (p.x - a.x) - ax * (p.z - a.z)) / length;
|
|
43
|
+
return Math.abs(offset) < ON_AXIS_M ? 'on-axis' : offset > 0 ? 'left' : 'right';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The two characters a camera is read against: the given ids that are visible
|
|
47
|
+
* characters, then the visible characters nearest the camera. Ordered by id, so
|
|
48
|
+
* every shot of the same pair reads the line in the same direction. */
|
|
49
|
+
export function primarySubjects(ids, cameraPosition, state) {
|
|
50
|
+
const visible = visibleCharacters(state);
|
|
51
|
+
const chosen = ids.map(id => visible.find(c => c.id === id)).filter(Boolean);
|
|
52
|
+
const near = c => Math.hypot(c.x - cameraPosition.x, c.z - cameraPosition.z);
|
|
53
|
+
const rest = visible.filter(c => !chosen.includes(c)).sort((p, q) => near(p) - near(q));
|
|
54
|
+
return [...chosen, ...rest].slice(0, 2).sort((p, q) => (p.id < q.id ? -1 : p.id > q.id ? 1 : 0));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Props whose world box crosses the sight line from the camera to the centre of
|
|
58
|
+
* the subject's box. The subject itself, props attached to it, hidden props and
|
|
59
|
+
* an enclosure the camera stands inside (a room set) never count. */
|
|
60
|
+
export function occludersOf(subject, cameraPosition, state, ports, subjectBounds = entityWorldBounds(subject, state, ports)) {
|
|
61
|
+
if (!subjectBounds) return [];
|
|
62
|
+
const centre = Object.fromEntries(['x', 'y', 'z'].map(a => [a, (subjectBounds.min[a] + subjectBounds.max[a]) / 2]));
|
|
63
|
+
return state.objects.filter(o => o.id !== subject.id && o.attach?.characterId !== subject.id && !isEffectivelyHidden(o, state.objects, state.characters))
|
|
64
|
+
.flatMap(o => {
|
|
65
|
+
const box = entityWorldBounds(o, state, ports);
|
|
66
|
+
return box && !contains(box, cameraPosition) && segmentIntersectsAabb(cameraPosition, centre, box) ? [{ subjectId: subject.id, byObjectId: o.id }] : [];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A shot's camera as position + aim: the live camera for the shot under the
|
|
71
|
+
* playhead, else the shot's first camera key; null when it has neither. */
|
|
72
|
+
export function shotCamera(shot, state) {
|
|
73
|
+
if (state.camera && shotAtFrame(state.shotDocument.shots, state.frame)?.id === shot.id) return { position: state.camera.position, lookAt: state.camera.lookAt };
|
|
74
|
+
const framing = shot.cameraKeys?.[0]?.framing;
|
|
75
|
+
if (!framing) return null;
|
|
76
|
+
const { pos, yaw, pitch } = framing;
|
|
77
|
+
return { position: pos, lookAt: { x: pos.x - Math.sin(yaw) * Math.cos(pitch), y: pos.y + Math.sin(pitch), z: pos.z - Math.cos(yaw) * Math.cos(pitch) } };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The 180-degree rule over shots read against the same pair: false when any
|
|
81
|
+
* pair has cameras on both sides, true when some pair has two sided shots and
|
|
82
|
+
* none crosses, null when no pair has two sided shots to compare. */
|
|
83
|
+
function axisConsistency(shots) {
|
|
84
|
+
const byPair = new Map();
|
|
85
|
+
for (const shot of shots) {
|
|
86
|
+
if (shot.cameraSide !== 'left' && shot.cameraSide !== 'right') continue;
|
|
87
|
+
const key = shot.subjectIds.join('|');
|
|
88
|
+
byPair.set(key, [...(byPair.get(key) ?? []), shot.cameraSide]);
|
|
89
|
+
}
|
|
90
|
+
const compared = [...byPair.values()].filter(sides => sides.length >= 2);
|
|
91
|
+
return compared.length ? compared.every(sides => sides.every(side => side === sides[0])) : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Facts for the given shots, bounded by GEOMETRY_FACTS_LIMITS. Without shotIds:
|
|
95
|
+
* the shots named in `first` (other ids are skipped), then the rest of the cut, so
|
|
96
|
+
* axisConsistent compares the named shots with the others. */
|
|
97
|
+
export function shotGeometryFacts(state, { shotIds, first = [], ports } = {}) {
|
|
98
|
+
const cast = visibleCharacters(state).slice(0, GEOMETRY_FACTS_LIMITS.subjects);
|
|
99
|
+
const subjects = cast.map(c => ({ id: c.id, position: point(c), yawDeg: round(c.rot ?? 0) }));
|
|
100
|
+
const pairs = cast.flatMap((a, i) => cast.slice(i + 1).map(b => ({ a: a.id, b: b.id, distanceM: round(Math.hypot(b.x - a.x, b.z - a.z)) })))
|
|
101
|
+
.sort((p, q) => p.distanceM - q.distanceM).slice(0, GEOMETRY_FACTS_LIMITS.pairs);
|
|
102
|
+
const all = state.shotDocument.shots;
|
|
103
|
+
const named = all.filter(s => first.includes(s.id)).sort((p, q) => first.indexOf(p.id) - first.indexOf(q.id));
|
|
104
|
+
const wanted = (shotIds ? shotIds.map(id => all.find(s => s.id === id)) : [...named, ...all.filter(s => !named.includes(s))]).filter(Boolean).slice(0, GEOMETRY_FACTS_LIMITS.shots);
|
|
105
|
+
const shots = wanted.map(shot => {
|
|
106
|
+
const camera = shotCamera(shot, state);
|
|
107
|
+
if (!camera) return { shotId: shot.id, subjectIds: [], camera: null, cameraSide: null, occluders: [] };
|
|
108
|
+
const pair = primarySubjects(shot.subjectIds ?? [], camera.position, state);
|
|
109
|
+
return { shotId: shot.id, subjectIds: pair.map(c => c.id), camera: { position: point(camera.position), lookAt: point(camera.lookAt) },
|
|
110
|
+
cameraSide: pair.length === 2 ? sideOfAxis(pair[0], pair[1], camera.position) : null,
|
|
111
|
+
occluders: pair.flatMap(subject => occludersOf(subject, camera.position, state, ports)).slice(0, GEOMETRY_FACTS_LIMITS.occluders) };
|
|
112
|
+
});
|
|
113
|
+
return { subjects, pairs, shots, axisConsistent: axisConsistency(shots) };
|
|
114
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -847,6 +847,9 @@ select {
|
|
|
847
847
|
display: none;
|
|
848
848
|
}
|
|
849
849
|
.app[data-embed-mode="playground"] .tl-view-tools { display: none !important; }
|
|
850
|
+
/* The v2 Sequencer styles sit outside the legacy layer, so the lane rule above
|
|
851
|
+
needs !important to keep only the Shots lane. */
|
|
852
|
+
.app[data-embed-mode="playground"] .tl-track:not(.shots) { display: none !important; }
|
|
850
853
|
/* Title bar keeps nothing but the pane's own name: the drawn dolly plays
|
|
851
854
|
full-screen through the shot camera's look-through button, and Esc is the
|
|
852
855
|
way back to flying. */
|
|
@@ -855,6 +858,8 @@ select {
|
|
|
855
858
|
/* The plan inset only matters while a rail is being drawn into it. */
|
|
856
859
|
.app[data-embed-mode="playground"]:not([data-rail-draw="1"]) .vp-inset.plan { display: none !important; }
|
|
857
860
|
.app[data-embed-mode="playground"][data-rail-draw="1"] .vp-inset.plan { width: 44% !important; height: 62% !important; }
|
|
861
|
+
/* The shot monitor would sit over the map's corner while the rail is drawn. */
|
|
862
|
+
.app[data-embed-mode="playground"][data-rail-draw="1"] .vp-shot-preview { display: none !important; }
|
|
858
863
|
.app[data-embed-mode="playground"] .bottom-window,
|
|
859
864
|
.app[data-embed-mode="playground"] .bottom-timeline {
|
|
860
865
|
flex: 0 0 auto;
|
|
@@ -865,7 +870,7 @@ select {
|
|
|
865
870
|
.app[data-embed-mode="playground"] .timeline.empty-shots { height: auto !important; min-height: 0 !important; max-height: none; }
|
|
866
871
|
.app[data-embed-mode="playground"] .tl-body { flex: 0 0 auto; overflow: hidden; }
|
|
867
872
|
.app[data-embed-mode="playground"] .tl-body { min-height: 0 !important; height: auto !important; }
|
|
868
|
-
.app[data-embed-mode="playground"] .tl-surface { height: auto !important; min-height: 0; grid-template-rows: auto auto !important; }
|
|
873
|
+
.app[data-embed-mode="playground"] .tl-surface { height: auto !important; min-height: 0 !important; grid-template-rows: auto auto !important; }
|
|
869
874
|
/* The Shots lane keeps its full-studio height so the block's hover actions
|
|
870
875
|
(Duplicate / Delete) sit in a strip above the clickable body instead of
|
|
871
876
|
covering it. */
|
|
@@ -875,7 +880,8 @@ select {
|
|
|
875
880
|
/* The shot monitor is the pane the tutorial points at: keep it small. */
|
|
876
881
|
.app[data-embed-mode="playground"] .vp-shot-preview { width: 22% !important; height: auto !important; aspect-ratio: 16 / 9; right: 12px; bottom: 12px; }
|
|
877
882
|
.app[data-embed-mode="playground"] .vp-shot-preview-tag { font-size: 12px; }
|
|
878
|
-
|
|
883
|
+
/* The look-through button carries its label now; keep it on one line. */
|
|
884
|
+
.app[data-embed-mode="playground"] .vp-look-through { height: 24px; padding: 0 8px; white-space: nowrap; }
|
|
879
885
|
.app[data-embed-mode="playground"] .vp-look-through svg { width: 15px; height: 15px; }
|
|
880
886
|
/* Tutorial hints: the landing page names the control the current step needs. */
|
|
881
887
|
.app[data-playground-hint="look"] .vp-look-through,
|
|
@@ -3032,6 +3038,28 @@ input[type=range]::-moz-range-thumb {
|
|
|
3032
3038
|
color: #eafff3;
|
|
3033
3039
|
}
|
|
3034
3040
|
|
|
3041
|
+
/* The selected dot's lean: two small scrub fields, the word as their badge. */
|
|
3042
|
+
.objmo-lean {
|
|
3043
|
+
display: inline-flex;
|
|
3044
|
+
align-items: stretch;
|
|
3045
|
+
gap: 6px;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
.objmo-lean .number-field {
|
|
3049
|
+
flex: none;
|
|
3050
|
+
width: 104px;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
.objmo-lean .number-field .axis {
|
|
3054
|
+
width: auto;
|
|
3055
|
+
padding: 0 6px;
|
|
3056
|
+
font-size: 10px;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
.number-field.disabled { opacity: .45; }
|
|
3060
|
+
.number-field.disabled .axis,
|
|
3061
|
+
.number-field.disabled input { cursor: not-allowed; }
|
|
3062
|
+
|
|
3035
3063
|
.tl-path-hint {
|
|
3036
3064
|
margin-left: auto;
|
|
3037
3065
|
color: #7fae95;
|
|
@@ -3136,9 +3164,22 @@ input[type=range]::-moz-range-thumb {
|
|
|
3136
3164
|
vector-effect: non-scaling-stroke;
|
|
3137
3165
|
}
|
|
3138
3166
|
|
|
3139
|
-
|
|
3167
|
+
/* The pin head lives outside the SVG: the graph's 1×1 viewBox is stretched to
|
|
3168
|
+
the lane, so anything drawn inside it is as wide as the lane is long. */
|
|
3169
|
+
.sg-cut-pin {
|
|
3170
|
+
position: absolute;
|
|
3171
|
+
top: 3px;
|
|
3172
|
+
width: 9px;
|
|
3173
|
+
height: 9px;
|
|
3174
|
+
padding: 0;
|
|
3175
|
+
border: 0;
|
|
3176
|
+
border-radius: 2px;
|
|
3177
|
+
background: #ffb454;
|
|
3178
|
+
transform: translateX(-50%) rotate(45deg);
|
|
3179
|
+
cursor: pointer;
|
|
3180
|
+
}
|
|
3140
3181
|
|
|
3141
|
-
.sg-pin.selected
|
|
3182
|
+
.sg-cut-pin.selected { background: #ffd08a; box-shadow: 0 0 0 2px rgba(255, 208, 138, .35); }
|
|
3142
3183
|
.sg-pin.selected .sg-cut { stroke-width: 2; stroke-dasharray: none; }
|
|
3143
3184
|
|
|
3144
3185
|
.sg-cut-pick {
|
|
@@ -3334,7 +3375,9 @@ input[type=range]::-moz-range-thumb {
|
|
|
3334
3375
|
.sg-shot .sg-line { stroke: #b9a1e8; }
|
|
3335
3376
|
.sg-shot .sg-axis, .sg-shot .sg-average { stroke: rgba(185, 161, 232, .4); }
|
|
3336
3377
|
.sg-shot .sg-readout { background: #4a3d6e; }
|
|
3337
|
-
|
|
3378
|
+
/* The box's own width is its clock and it has no room for an axis: a dozen
|
|
3379
|
+
pixels tall, three marks would sit on top of one another. */
|
|
3380
|
+
.sg-bare .sg-scale-top, .sg-bare .sg-scale-avg, .sg-bare .sg-scale-zero { display: none; }
|
|
3338
3381
|
|
|
3339
3382
|
|
|
3340
3383
|
|
|
@@ -3390,10 +3433,13 @@ input[type=range]::-moz-range-thumb {
|
|
|
3390
3433
|
pointer-events: none;
|
|
3391
3434
|
}
|
|
3392
3435
|
|
|
3436
|
+
/* One axis, one edge: all three scale marks sit on the left. */
|
|
3393
3437
|
.sg-scale-top,
|
|
3394
|
-
.sg-scale-avg
|
|
3438
|
+
.sg-scale-avg,
|
|
3439
|
+
.sg-scale-zero {
|
|
3395
3440
|
position: absolute;
|
|
3396
3441
|
left: 4px;
|
|
3442
|
+
right: auto;
|
|
3397
3443
|
color: #7fae95;
|
|
3398
3444
|
font-size: 8px;
|
|
3399
3445
|
pointer-events: none;
|
|
@@ -5992,6 +6038,12 @@ input[type=range]::-moz-range-thumb {
|
|
|
5992
6038
|
border-radius: 3px;
|
|
5993
6039
|
}
|
|
5994
6040
|
|
|
6041
|
+
/* an Empty has no colour: a dashed, hollow swatch reads as "nothing to draw" */
|
|
6042
|
+
.add-object-swatch.empty {
|
|
6043
|
+
background: transparent !important;
|
|
6044
|
+
border-style: dashed;
|
|
6045
|
+
}
|
|
6046
|
+
|
|
5995
6047
|
/* inspector cards clip their overflow, so the menu opens inline there */
|
|
5996
6048
|
.card .add-object {
|
|
5997
6049
|
display: block;
|
|
@@ -6803,10 +6855,9 @@ input[type=range]::-moz-range-thumb {
|
|
|
6803
6855
|
|
|
6804
6856
|
/* Keep the timeline focused on the selected workflow. Pose-correction and
|
|
6805
6857
|
take-generation controls are useful while authoring motion, but they are
|
|
6806
|
-
noise during blocking and camera work. The
|
|
6807
|
-
|
|
6858
|
+
noise during blocking and camera work. The motion clear action stays
|
|
6859
|
+
visible in Stage so a loaded take is always reversible. */
|
|
6808
6860
|
.app[data-workflow-mode="scene"] .tl-pose-tools,
|
|
6809
|
-
.app[data-workflow-mode="scene"] .tl-motion-tools,
|
|
6810
6861
|
.app[data-workflow-mode="scene"] .take-bar,
|
|
6811
6862
|
.app[data-workflow-mode="camera"] .tl-pose-tools,
|
|
6812
6863
|
.app[data-workflow-mode="camera"] .tl-motion-tools,
|
|
@@ -10217,4 +10268,36 @@ input[type=range] {
|
|
|
10217
10268
|
margin: 8px 0 0;
|
|
10218
10269
|
color: var(--muted);
|
|
10219
10270
|
}
|
|
10271
|
+
|
|
10272
|
+
/* Sculpt mode (#739): the turntable owns the main pane and the shot preview
|
|
10273
|
+
shows the scene, so the Top-View inset has nothing to add. */
|
|
10274
|
+
.app[data-workflow-mode="sculpt"] .vp-inset { display: none !important; }
|
|
10275
|
+
|
|
10276
|
+
/* The empty turntable says what the mode is waiting for, under the mode pill
|
|
10277
|
+
(the dock floats over the pane's foot). Overlay chrome, like the viewport
|
|
10278
|
+
pills: it never takes the pointer from the orbit. */
|
|
10279
|
+
.vp-sculpt-hint {
|
|
10280
|
+
position: absolute;
|
|
10281
|
+
left: 50%;
|
|
10282
|
+
top: calc(2 * var(--sp-2, 12px) + var(--h-control, 28px));
|
|
10283
|
+
z-index: 7;
|
|
10284
|
+
display: grid;
|
|
10285
|
+
gap: var(--sp-1, 4px);
|
|
10286
|
+
max-width: min(420px, calc(100% - 2 * var(--sp-3, 16px)));
|
|
10287
|
+
padding: var(--sp-2, 8px) var(--sp-3, 16px);
|
|
10288
|
+
border-radius: var(--r-2, 6px);
|
|
10289
|
+
background: var(--overlay);
|
|
10290
|
+
color: var(--text-2);
|
|
10291
|
+
font-size: 11px;
|
|
10292
|
+
line-height: 1.45;
|
|
10293
|
+
text-align: center;
|
|
10294
|
+
transform: translateX(-50%);
|
|
10295
|
+
pointer-events: none;
|
|
10296
|
+
}
|
|
10297
|
+
|
|
10298
|
+
.vp-sculpt-hint strong {
|
|
10299
|
+
color: var(--text-1);
|
|
10300
|
+
font-size: 12px;
|
|
10301
|
+
font-weight: 600;
|
|
10302
|
+
}
|
|
10220
10303
|
}
|
package/src/ui-scrub.js
CHANGED
|
@@ -28,7 +28,7 @@ export const SCRUB_THRESHOLD_PX = 4;
|
|
|
28
28
|
|
|
29
29
|
/** Shift buys a quarter-speed pass for fine detail; Alt keeps its old 0.1x. */
|
|
30
30
|
export const FINE_SCRUB_FACTOR = 0.25;
|
|
31
|
-
|
|
31
|
+
const EXTRA_FINE_SCRUB_FACTOR = 0.1;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* The meaningful span a full drag sweeps when the field does not name one.
|
|
@@ -39,8 +39,8 @@ export const SCRUB_RANGE_STEPS = 100;
|
|
|
39
39
|
|
|
40
40
|
/** The span one full `SCRUB_TRAVEL_PX` drag covers for a field. */
|
|
41
41
|
export function scrubRangeFor({ scrubRange, step } = {}) {
|
|
42
|
-
if (
|
|
43
|
-
const size =
|
|
42
|
+
if (Number.isFinite(scrubRange) && scrubRange > 0) return scrubRange;
|
|
43
|
+
const size = Number.isFinite(step) && step > 0 ? step : 1;
|
|
44
44
|
return size * SCRUB_RANGE_STEPS;
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -95,7 +95,7 @@ export function createScrubGesture({ x, value, step, precision, scrubRange }) {
|
|
|
95
95
|
* 1.7000000000000002 in the input.
|
|
96
96
|
*/
|
|
97
97
|
export function snapScrubValue(value, { step, precision = 2 } = {}) {
|
|
98
|
-
const size =
|
|
98
|
+
const size = Number.isFinite(step) && step > 0 ? step : 0;
|
|
99
99
|
const snapped = size > 0 ? Math.round(value / size) * size : value;
|
|
100
100
|
const digits = Number.isFinite(precision) ? Math.max(0, Math.min(15, Math.round(precision))) : 2;
|
|
101
101
|
return Number(snapped.toFixed(digits));
|
package/src/ui.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { createScrubGesture } from "./ui-scrub.js";
|
|
4
|
+
import { isImeComposing } from "./ime.js";
|
|
4
5
|
|
|
5
6
|
// Custom dropdown, structurally matched to the reference (`.dropdown`,
|
|
6
7
|
// `dd-caret`, `dropdown-menu`, `dropdown-item`, `dd-check`) with the keyboard
|
|
@@ -279,7 +280,7 @@ export function Field({ label, children }) {
|
|
|
279
280
|
</div>
|
|
280
281
|
);
|
|
281
282
|
}
|
|
282
|
-
export function NumberField({ label, value, step, precision = 2, scrubRange, onChange, onScrubStart, onScrubEnd, title }) {
|
|
283
|
+
export function NumberField({ label, value, step, precision = 2, scrubRange, onChange, onScrubStart, onScrubEnd, title, disabled = false }) {
|
|
283
284
|
const [focused, setFocused] = useState(false);
|
|
284
285
|
const [draft, setDraft] = useState(null);
|
|
285
286
|
// The draft must be readable synchronously: blur() fires the commit
|
|
@@ -335,7 +336,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
|
|
|
335
336
|
};
|
|
336
337
|
|
|
337
338
|
const onKeyDown = (e) => {
|
|
338
|
-
if (e.key === "Enter") {
|
|
339
|
+
if (e.key === "Enter" && !isImeComposing(e)) {
|
|
339
340
|
e.preventDefault();
|
|
340
341
|
commitDraft();
|
|
341
342
|
// Keep focus and select the committed value so the next keystroke
|
|
@@ -357,7 +358,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
|
|
|
357
358
|
// alone was a 15 px hotspot nobody found). Sensitivity is a fixed-travel
|
|
358
359
|
// rate (src/ui-scrub.js), not pixels * step, so every field feels the same.
|
|
359
360
|
const onFieldPointerDown = (e) => {
|
|
360
|
-
if (e.button !== 0) return;
|
|
361
|
+
if (e.button !== 0 || disabled) return;
|
|
361
362
|
// A press inside an already-focused input is text editing (caret
|
|
362
363
|
// placement, drag-select), not a scrub — leave it alone.
|
|
363
364
|
if (focused && e.target === inputRef.current) return;
|
|
@@ -460,7 +461,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
|
|
|
460
461
|
|
|
461
462
|
return (
|
|
462
463
|
<span
|
|
463
|
-
className={`number-field v2-details-number-field${focused ? " editing" : ""}`}
|
|
464
|
+
className={`number-field v2-details-number-field${focused ? " editing" : ""}${disabled ? " disabled" : ""}`}
|
|
464
465
|
title={title}
|
|
465
466
|
onPointerDown={onFieldPointerDown}
|
|
466
467
|
onPointerUp={endFieldDrag}
|
|
@@ -472,6 +473,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
|
|
|
472
473
|
type="text"
|
|
473
474
|
inputMode="decimal"
|
|
474
475
|
value={display}
|
|
476
|
+
disabled={disabled}
|
|
475
477
|
onFocus={onFocus}
|
|
476
478
|
onBlur={onBlur}
|
|
477
479
|
onChange={(e) => setDraftValue(e.target.value)}
|