cozyclay 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +219 -38
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +6 -5
- package/bin/cozyclay.mjs +12 -3
- package/bin/live/cli.mjs +2 -3
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +12 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-B41FmPYI.js +1 -0
- package/dist/assets/app-Do-Og5Pu.css +1 -0
- package/dist/assets/app-DwP-tl2x.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
- package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
- package/dist/assets/workflow-Br-K7Srs.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/sitemap.xml +5 -5
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +801 -259
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +196 -3
- package/src/ardy/timeline.jsx +117 -29
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +1 -4
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +43 -24
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +16 -3
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +89 -32
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion/generation.js +1 -1
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +52 -3
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +62 -17
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +58 -14
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/TopBar.jsx +17 -3
- package/src/shell/ViewportToolbar.jsx +18 -3
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +186 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +56 -23
- package/src/studio-app-binding.js +52 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +111 -10
- package/src/theme.js +1 -1
- package/src/timeline-fps.js +13 -0
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +110 -20
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +96 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-C1sdVbg4.css +0 -1
- package/dist/assets/app-CA55tcI3.js +0 -4876
- package/dist/assets/shot-prompt-Yrt99wZx.js +0 -17
- package/dist/assets/workflow-dNPGY9xz.js +0 -179
package/src/project.js
CHANGED
|
@@ -16,8 +16,17 @@
|
|
|
16
16
|
|
|
17
17
|
import { SCENES_VERSION } from "./scenes.js";
|
|
18
18
|
import { ASSET_MAX_SOURCE_BYTES, assetIdForBytes, isAssetId, isImageAssetId, isMeshAssetId, meshIdForBytes, normalizeAsset, referencedAssetIds } from "./scene-assets.js";
|
|
19
|
+
import { ko } from "./locale.js";
|
|
19
20
|
|
|
20
|
-
export const PROJECT_VERSION =
|
|
21
|
+
export const PROJECT_VERSION = 5;
|
|
22
|
+
// A project is authored either as a storyboard (stills) or as animation
|
|
23
|
+
// (clips). v5 records the choice on the envelope; v2-v4 files are animation.
|
|
24
|
+
export const PREVIS_MODES = Object.freeze(["storyboard", "animation"]);
|
|
25
|
+
export const DEFAULT_PREVIS_MODE = "animation";
|
|
26
|
+
|
|
27
|
+
export function normalizePrevisMode(value) {
|
|
28
|
+
return PREVIS_MODES.includes(value) ? value : DEFAULT_PREVIS_MODE;
|
|
29
|
+
}
|
|
21
30
|
export const PROJECT_EXTENSION = ".cclayproject";
|
|
22
31
|
export const WORKFLOW_VERSION = 1;
|
|
23
32
|
export const WORKFLOW_STORAGE_KEY = "cozyclay.workflow.v1";
|
|
@@ -32,8 +41,6 @@ export const PROJECT_MAX_RESOURCE_BYTES = 256 * 1024 * 1024;
|
|
|
32
41
|
// A motion id is the SHA-256 of the raw NPZ bytes (see motion-resources.js).
|
|
33
42
|
const MOTION_ID_PATTERN = /^[0-9a-f]{64}$/i;
|
|
34
43
|
|
|
35
|
-
/* ---------------------------- workflow graph --------------------------- */
|
|
36
|
-
|
|
37
44
|
function plainRecord(value) {
|
|
38
45
|
if (!value || typeof value !== "object") return false;
|
|
39
46
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -132,11 +139,14 @@ export function normalizeWorkflowGraph(value) {
|
|
|
132
139
|
return { version: WORKFLOW_VERSION, nodes, edges };
|
|
133
140
|
}
|
|
134
141
|
|
|
135
|
-
/** Read the browser-local workflow draft used by the standalone canvas.
|
|
136
|
-
|
|
142
|
+
/** Read the browser-local workflow draft used by the standalone canvas.
|
|
143
|
+
* A corrupt draft reads as an empty graph unless `strict`, which rethrows the
|
|
144
|
+
* parse error so a caller about to write can refuse to overwrite it. */
|
|
145
|
+
export function loadWorkflowGraph(storage = globalThis.localStorage, { strict = false } = {}) {
|
|
137
146
|
try {
|
|
138
147
|
return normalizeWorkflowGraph(JSON.parse(storage?.getItem(WORKFLOW_STORAGE_KEY) || "null"));
|
|
139
|
-
} catch {
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (strict) throw error;
|
|
140
150
|
return createWorkflowGraph();
|
|
141
151
|
}
|
|
142
152
|
}
|
|
@@ -155,8 +165,6 @@ export function storeWorkflowGraph(graph, storage = globalThis.localStorage) {
|
|
|
155
165
|
return normalized;
|
|
156
166
|
}
|
|
157
167
|
|
|
158
|
-
/* ------------------------------- envelope ------------------------------- */
|
|
159
|
-
|
|
160
168
|
function asArrayBuffer(bytes) {
|
|
161
169
|
if (bytes instanceof ArrayBuffer) return bytes;
|
|
162
170
|
if (ArrayBuffer.isView(bytes)) return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
@@ -362,13 +370,13 @@ function readEmbeddedMotions(value, report) {
|
|
|
362
370
|
}
|
|
363
371
|
|
|
364
372
|
/**
|
|
365
|
-
* Build the
|
|
373
|
+
* Build the v5 envelope. Images are embedded when a scene or workflow
|
|
366
374
|
* assetRef references them; motions are embedded as given (deduped by
|
|
367
375
|
* motionId, first record wins) because the caller already knows which ones
|
|
368
376
|
* the project uses. Throws `resources-too-large` when one motion or the whole
|
|
369
377
|
* manifest exceeds its budget.
|
|
370
378
|
*/
|
|
371
|
-
export function createProjectDocument({ scenesDocument, workspaceLayout, customPoses, name, assets, motions, savedAt, workflow }) {
|
|
379
|
+
export function createProjectDocument({ scenesDocument, workspaceLayout, customPoses, name, previsMode, assets, motions, savedAt, workflow }) {
|
|
372
380
|
const assetRecords = new Map();
|
|
373
381
|
for (const record of Array.isArray(assets) ? assets : []) {
|
|
374
382
|
const asset = embeddedAsset(record);
|
|
@@ -404,6 +412,7 @@ export function createProjectDocument({ scenesDocument, workspaceLayout, customP
|
|
|
404
412
|
kind: "project",
|
|
405
413
|
version: PROJECT_VERSION,
|
|
406
414
|
name: typeof name === "string" && name.trim() ? name.trim() : "Untitled",
|
|
415
|
+
previsMode: normalizePrevisMode(previsMode),
|
|
407
416
|
...(Number.isFinite(savedAt) && savedAt > 0 ? { savedAt } : {}),
|
|
408
417
|
scenes: scenesDocument,
|
|
409
418
|
workspace: workspaceLayout ?? null,
|
|
@@ -413,12 +422,43 @@ export function createProjectDocument({ scenesDocument, workspaceLayout, customP
|
|
|
413
422
|
};
|
|
414
423
|
}
|
|
415
424
|
|
|
425
|
+
/** Convert a normalized storyboard project into a fresh animation envelope.
|
|
426
|
+
* The source remains untouched: still shot records are copied with their
|
|
427
|
+
* ranges, camera keys, captions, cast overrides and stylized asset references.
|
|
428
|
+
*/
|
|
429
|
+
export function buildAnimationProjectFromStoryboard(project) {
|
|
430
|
+
const scenesDocument = structuredClone(project?.scenesDocument ?? { version: SCENES_VERSION, activeSceneId: null, scenes: [] });
|
|
431
|
+
scenesDocument.scenes = (scenesDocument.scenes ?? []).map((scene) => ({
|
|
432
|
+
...scene,
|
|
433
|
+
shotDocument: scene.shotDocument && typeof scene.shotDocument === "object"
|
|
434
|
+
? {
|
|
435
|
+
...scene.shotDocument,
|
|
436
|
+
shots: Array.isArray(scene.shotDocument.shots)
|
|
437
|
+
? scene.shotDocument.shots.map((shot) => ({ ...shot, ...(shot.kind === "still" ? { kind: "clip" } : {}) }))
|
|
438
|
+
: scene.shotDocument.shots,
|
|
439
|
+
}
|
|
440
|
+
: scene.shotDocument,
|
|
441
|
+
}));
|
|
442
|
+
return createProjectDocument({
|
|
443
|
+
scenesDocument,
|
|
444
|
+
workspaceLayout: structuredClone(project?.workspaceLayout ?? null),
|
|
445
|
+
customPoses: structuredClone(project?.customPoses ?? []),
|
|
446
|
+
name: `${project?.name || "Untitled"} - ${ko("Animation", "애니메이션")}`,
|
|
447
|
+
previsMode: "animation",
|
|
448
|
+
assets: project?.assets ?? [],
|
|
449
|
+
motions: project?.motions ?? [],
|
|
450
|
+
workflow: structuredClone(project?.workflow ?? createWorkflowGraph()),
|
|
451
|
+
savedAt: Date.now(),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
416
455
|
/**
|
|
417
456
|
* Parse + validate a project file. Returns { ok, reason?, warnings?,
|
|
418
457
|
* problems?, project? }. Invalid embedded records are skipped, each one
|
|
419
458
|
* reported as a warning string and a `{ kind, id, code, message }` problem.
|
|
420
459
|
* v2/v3 files carry images at the top-level `assets`; v4 moves everything
|
|
421
|
-
* under `resources
|
|
460
|
+
* under `resources`; v5 adds the top-level `previsMode` (absent or unknown
|
|
461
|
+
* reads as the default). Older files are read as-is, never rewritten.
|
|
422
462
|
*/
|
|
423
463
|
export function readProjectDocument(raw) {
|
|
424
464
|
let parsed;
|
|
@@ -452,6 +492,7 @@ export function readProjectDocument(raw) {
|
|
|
452
492
|
problems,
|
|
453
493
|
project: {
|
|
454
494
|
name: typeof parsed.name === "string" && parsed.name.trim() ? parsed.name.trim() : "Untitled",
|
|
495
|
+
previsMode: normalizePrevisMode(parsed.previsMode),
|
|
455
496
|
savedAt: Number.isFinite(parsed.savedAt) && parsed.savedAt > 0 ? parsed.savedAt : null,
|
|
456
497
|
scenesDocument: { version: scenes.version, activeSceneId: scenes.activeSceneId ?? null, scenes: scenes.scenes },
|
|
457
498
|
workspaceLayout: parsed.workspace && typeof parsed.workspace === "object" ? parsed.workspace : null,
|
|
@@ -465,8 +506,6 @@ export function readProjectDocument(raw) {
|
|
|
465
506
|
};
|
|
466
507
|
}
|
|
467
508
|
|
|
468
|
-
/* ------------------------- file system access --------------------------- */
|
|
469
|
-
|
|
470
509
|
export function hasFileSystemAccess() {
|
|
471
510
|
return typeof window !== "undefined" && typeof window.showSaveFilePicker === "function" && typeof window.showOpenFilePicker === "function";
|
|
472
511
|
}
|
|
@@ -502,8 +541,6 @@ export async function readProjectFile(handle) {
|
|
|
502
541
|
return { name: file.name.replace(/\.cclayproject$|\.json$/i, ""), text: await file.text(), savedAt: file.lastModified };
|
|
503
542
|
}
|
|
504
543
|
|
|
505
|
-
/* --------------------------- fallback (no FS API) ------------------------ */
|
|
506
|
-
|
|
507
544
|
export function downloadProjectFallback(serialized, name) {
|
|
508
545
|
const blob = new Blob([serialized], { type: "application/json" });
|
|
509
546
|
const url = URL.createObjectURL(blob);
|
|
@@ -529,8 +566,6 @@ export function openProjectFallback() {
|
|
|
529
566
|
});
|
|
530
567
|
}
|
|
531
568
|
|
|
532
|
-
/* ------------------------------- IDB handle ------------------------------ */
|
|
533
|
-
|
|
534
569
|
function openIdb() {
|
|
535
570
|
return new Promise((resolve, reject) => {
|
|
536
571
|
const request = indexedDB.open(IDB_NAME, 1);
|
|
@@ -605,27 +640,23 @@ export function loadProjectSession(storage = globalThis.localStorage) {
|
|
|
605
640
|
const value = JSON.parse(storage?.getItem(PROJECT_SESSION_KEY) || "null");
|
|
606
641
|
if (!value || typeof value !== "object") return null;
|
|
607
642
|
const name = typeof value.name === "string" ? value.name.trim() : "";
|
|
608
|
-
return name ? { name, updatedAt: Number.isFinite(value.updatedAt) ? value.updatedAt : 0 } : null;
|
|
643
|
+
return name ? { name, previsMode: normalizePrevisMode(value.previsMode), updatedAt: Number.isFinite(value.updatedAt) ? value.updatedAt : 0 } : null;
|
|
609
644
|
} catch {
|
|
610
645
|
return null;
|
|
611
646
|
}
|
|
612
647
|
}
|
|
613
648
|
|
|
614
|
-
export function storeProjectSession(name, storage = globalThis.localStorage) {
|
|
649
|
+
export function storeProjectSession(name, previsMode = DEFAULT_PREVIS_MODE, storage = globalThis.localStorage) {
|
|
615
650
|
const normalized = typeof name === "string" ? name.trim() : "";
|
|
616
651
|
if (!normalized) return false;
|
|
617
652
|
try {
|
|
618
|
-
storage?.setItem(PROJECT_SESSION_KEY, JSON.stringify({ name: normalized, updatedAt: Date.now() }));
|
|
653
|
+
storage?.setItem(PROJECT_SESSION_KEY, JSON.stringify({ name: normalized, previsMode: normalizePrevisMode(previsMode), updatedAt: Date.now() }));
|
|
619
654
|
return true;
|
|
620
655
|
} catch {
|
|
621
656
|
return false;
|
|
622
657
|
}
|
|
623
658
|
}
|
|
624
659
|
|
|
625
|
-
export function clearProjectSession(storage = globalThis.localStorage) {
|
|
626
|
-
try { storage?.removeItem(PROJECT_SESSION_KEY); } catch { /* private mode */ }
|
|
627
|
-
}
|
|
628
|
-
|
|
629
660
|
async function idbSet(key, value) {
|
|
630
661
|
try {
|
|
631
662
|
const db = await openIdb();
|
package/src/props.jsx
CHANGED
|
@@ -12,12 +12,16 @@
|
|
|
12
12
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
13
13
|
import { useFrame } from "@react-three/fiber";
|
|
14
14
|
import { objectTransformAt } from "./object-path.js";
|
|
15
|
+
import { sceneObjectCarryMatrixAt, sceneObjectTravelMatrixAt, travelPose } from "./object-travel.js";
|
|
15
16
|
import * as THREE from "three";
|
|
16
17
|
import { GIZMO_LAYER } from "./dualview.jsx";
|
|
17
|
-
import { CUTOUT_KIND, MESH_KIND } from "./scene-objects.js";
|
|
18
|
+
import { CUTOUT_KIND, MESH_KIND, EMPTY_KIND, EMPTY_MARKER_SIZE, SCULPT_KIND } from "./scene-objects.js";
|
|
19
|
+
import { buildSculptGroup, disposeSculptGroup } from "./sculpt-geometry.js";
|
|
18
20
|
import { subscribeToAssetTexture } from "./scene-asset-cache.js";
|
|
19
21
|
import { subscribeToMeshScene } from "./scene-mesh-cache.js";
|
|
20
22
|
import { cloneMeshGraph } from "./mesh-graph-clone.js";
|
|
23
|
+
import { applyPoseFade } from "./pose-fade.js";
|
|
24
|
+
import { coplanarDepthRanks } from "./coplanar-depth.js";
|
|
21
25
|
|
|
22
26
|
const CLAY_CAR = "#d98770";
|
|
23
27
|
const CLAY_CAR_TOP = "#e49a84";
|
|
@@ -56,27 +60,22 @@ export function Car({ position = [0, 0, 0], rotY = 0, color = CLAY_CAR, topColor
|
|
|
56
60
|
: { color: topColor, roughness: 0.5, metalness: 0.2 };
|
|
57
61
|
return (
|
|
58
62
|
<group position={position} rotation={[0, rotY, 0]}>
|
|
59
|
-
{/* lower body */}
|
|
60
63
|
<mesh position={[0, 0.62, 0]}>
|
|
61
64
|
<boxGeometry args={[1.78, 0.62, 4.45]} />
|
|
62
65
|
<meshStandardMaterial {...bodyMaterial} />
|
|
63
66
|
</mesh>
|
|
64
|
-
{/* cabin: centred over the wheelbase, not stacked on the tail */}
|
|
65
67
|
<mesh position={[0, 1.12, -0.15]}>
|
|
66
68
|
<boxGeometry args={[1.58, 0.5, 2.2]} />
|
|
67
69
|
<meshStandardMaterial {...topMaterial} />
|
|
68
70
|
</mesh>
|
|
69
|
-
{/* greenhouse glass band */}
|
|
70
71
|
<mesh position={[0, 1.14, -0.15]}>
|
|
71
72
|
<boxGeometry args={[1.62, 0.3, 1.9]} />
|
|
72
73
|
<meshStandardMaterial color={CLAY_GLASS} roughness={0.15} metalness={0.6} />
|
|
73
74
|
</mesh>
|
|
74
|
-
{/* windshield slope */}
|
|
75
75
|
<mesh position={[0, 1.05, 0.95]} rotation={[0.5, 0, 0]}>
|
|
76
76
|
<boxGeometry args={[1.56, 0.42, 0.08]} />
|
|
77
77
|
<meshStandardMaterial color={CLAY_GLASS} roughness={0.15} metalness={0.6} />
|
|
78
78
|
</mesh>
|
|
79
|
-
{/* rear glass slope */}
|
|
80
79
|
<mesh position={[0, 1.05, -1.25]} rotation={[-0.55, 0, 0]}>
|
|
81
80
|
<boxGeometry args={[1.56, 0.42, 0.08]} />
|
|
82
81
|
<meshStandardMaterial color={CLAY_GLASS} roughness={0.15} metalness={0.6} />
|
|
@@ -98,7 +97,6 @@ export function SmallPlane({ position = [0, 0, 0], rotY = 0, autoColor = undefin
|
|
|
98
97
|
autoColor ? autoFlat(autoColor) : { color, roughness, metalness };
|
|
99
98
|
return (
|
|
100
99
|
<group position={position} rotation={[0, rotY, 0]}>
|
|
101
|
-
{/* fuselage and tapered nose */}
|
|
102
100
|
<mesh position={[0, 0.76, 0]} rotation={[Math.PI / 2, 0, 0]}>
|
|
103
101
|
<cylinderGeometry args={[0.23, 0.31, 2.75, 16]} />
|
|
104
102
|
<meshStandardMaterial {...mat(CLAY_PLANE, 0.58, 0.18)} />
|
|
@@ -108,7 +106,6 @@ export function SmallPlane({ position = [0, 0, 0], rotY = 0, autoColor = undefin
|
|
|
108
106
|
<meshStandardMaterial {...mat(CLAY_PLANE_TRIM, 0.52, 0.2)} />
|
|
109
107
|
</mesh>
|
|
110
108
|
|
|
111
|
-
{/* main wing and tail plane */}
|
|
112
109
|
<mesh position={[0, 0.73, 0.15]}>
|
|
113
110
|
<boxGeometry args={[3.4, 0.09, 0.58]} />
|
|
114
111
|
<meshStandardMaterial {...mat(CLAY_PLANE, 0.62, 0.14)} />
|
|
@@ -122,13 +119,11 @@ export function SmallPlane({ position = [0, 0, 0], rotY = 0, autoColor = undefin
|
|
|
122
119
|
<meshStandardMaterial {...mat(CLAY_PLANE, 0.62, 0.12)} />
|
|
123
120
|
</mesh>
|
|
124
121
|
|
|
125
|
-
{/* cockpit canopy */}
|
|
126
122
|
<mesh position={[0, 1.02, 0.42]} scale={[0.62, 0.46, 0.9]}>
|
|
127
123
|
<sphereGeometry args={[0.42, 16, 10, 0, Math.PI * 2, 0, Math.PI / 2]} />
|
|
128
124
|
<meshStandardMaterial color={CLAY_GLASS} roughness={0.16} metalness={0.55} />
|
|
129
125
|
</mesh>
|
|
130
126
|
|
|
131
|
-
{/* propeller hub and blades */}
|
|
132
127
|
<group position={[0, 0.76, 1.86]}>
|
|
133
128
|
<mesh rotation={[Math.PI / 2, 0, 0]}>
|
|
134
129
|
<cylinderGeometry args={[0.11, 0.11, 0.2, 12]} />
|
|
@@ -140,7 +135,6 @@ export function SmallPlane({ position = [0, 0, 0], rotY = 0, autoColor = undefin
|
|
|
140
135
|
</mesh>
|
|
141
136
|
</group>
|
|
142
137
|
|
|
143
|
-
{/* simple landing gear */}
|
|
144
138
|
{[-0.55, 0.55].map((x) => (
|
|
145
139
|
<group key={x} position={[x, 0.2, 0.15]}>
|
|
146
140
|
<mesh position={[0, 0.24, 0]} rotation={[0, 0, x < 0 ? -0.35 : 0.35]}>
|
|
@@ -210,12 +204,20 @@ function autoFlat(hex) {
|
|
|
210
204
|
return { color: hex, roughness: 1, metalness: 0, emissive: hex, emissiveIntensity: 0.4 };
|
|
211
205
|
}
|
|
212
206
|
|
|
213
|
-
|
|
207
|
+
// Parts of an assembly can share a plane with an earlier part (hood top and
|
|
208
|
+
// grille top). The depth test can't order them and the winner flips as the
|
|
209
|
+
// camera moves, so a ranked part is pulled toward the camera by `rank` steps
|
|
210
|
+
// (negative = nearer). Rank 0 gets nothing: those materials stay untouched.
|
|
211
|
+
function depthRankProps(rank) {
|
|
212
|
+
return rank > 0 ? { polygonOffset: true, polygonOffsetFactor: -rank, polygonOffsetUnits: -rank } : null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function Primitive({ kind, color, autoColor, depthRank = 0 }) {
|
|
214
216
|
const side = kind === "plane" ? THREE.DoubleSide : THREE.FrontSide;
|
|
215
217
|
const material = autoColor ? (
|
|
216
|
-
<meshStandardMaterial {...autoFlat(autoColor)} side={side} />
|
|
218
|
+
<meshStandardMaterial {...autoFlat(autoColor)} side={side} {...depthRankProps(depthRank)} />
|
|
217
219
|
) : (
|
|
218
|
-
<meshStandardMaterial color={color} roughness={0.82} side={side} />
|
|
220
|
+
<meshStandardMaterial color={color} roughness={0.82} side={side} {...depthRankProps(depthRank)} />
|
|
219
221
|
);
|
|
220
222
|
if (kind === "sphere") {
|
|
221
223
|
return (
|
|
@@ -311,8 +313,8 @@ function disposeOwnedMaterials(root) {
|
|
|
311
313
|
*
|
|
312
314
|
* Skinned graphs go through `cloneMeshGraph` so a Mixamo-as-statue keeps
|
|
313
315
|
* its bind pose. Clay replaces materials on THIS clone only, so a second
|
|
314
|
-
* instance of the same file can keep the textures from the disk.
|
|
315
|
-
*
|
|
316
|
+
* instance of the same file can keep the textures from the disk. Every clone
|
|
317
|
+
* owns its materials, so auto-color tints and opacity fades never leak.
|
|
316
318
|
*/
|
|
317
319
|
function instantiateMesh(source, object) {
|
|
318
320
|
const root = cloneMeshGraph(source);
|
|
@@ -338,18 +340,16 @@ function instantiateMesh(source, object) {
|
|
|
338
340
|
node.receiveShadow = true;
|
|
339
341
|
if (clay) {
|
|
340
342
|
const make = () => new THREE.MeshStandardMaterial({ color: clayColor, roughness: 0.9, metalness: 0 });
|
|
343
|
+
for (const material of Array.isArray(node.material) ? node.material : [node.material]) material?.dispose?.();
|
|
341
344
|
node.material = Array.isArray(node.material) ? node.material.map(() => make()) : make();
|
|
342
345
|
node.userData.clayOwned = true;
|
|
343
346
|
return;
|
|
344
347
|
}
|
|
345
348
|
if (!object.autoColor || !node.material) return;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
};
|
|
351
|
-
node.material = Array.isArray(node.material) ? node.material.map(tint) : tint(node.material);
|
|
352
|
-
node.userData.instanceOwned = true;
|
|
349
|
+
// cloneMeshGraph already gave this instance its own materials.
|
|
350
|
+
for (const material of Array.isArray(node.material) ? node.material : [node.material]) {
|
|
351
|
+
if (material.color) material.color.set(object.autoColor);
|
|
352
|
+
}
|
|
353
353
|
});
|
|
354
354
|
return root;
|
|
355
355
|
}
|
|
@@ -420,21 +420,92 @@ function Cutout({ object }) {
|
|
|
420
420
|
);
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
/**
|
|
424
|
+
* A sculpt: the clay parts its recipe describes, rebuilt only when the recipe
|
|
425
|
+
* or the look changes. Each part keeps its own clay colour; auto-colour paints
|
|
426
|
+
* the whole sculpt flat like any other prop, because it is one object.
|
|
427
|
+
*/
|
|
428
|
+
function Sculpt({ object, depthRank = 0 }) {
|
|
429
|
+
const { recipe, autoColor } = object;
|
|
430
|
+
const group = useMemo(
|
|
431
|
+
() => buildSculptGroup(recipe, {
|
|
432
|
+
materialFor: (part) => new THREE.MeshStandardMaterial({
|
|
433
|
+
...(autoColor ? autoFlat(autoColor) : { color: part.color, roughness: 0.82, metalness: 0 }),
|
|
434
|
+
...depthRankProps(depthRank),
|
|
435
|
+
}),
|
|
436
|
+
}),
|
|
437
|
+
[recipe, autoColor, depthRank],
|
|
438
|
+
);
|
|
439
|
+
useEffect(() => () => disposeSculptGroup(group), [group]);
|
|
440
|
+
return <primitive object={group} />;
|
|
441
|
+
}
|
|
442
|
+
|
|
423
443
|
const PRIMITIVE_KINDS = new Set(["cube", "sphere", "capsule", "cylinder", "cone", "plane"]);
|
|
424
444
|
|
|
425
|
-
|
|
445
|
+
// X red, Y green, Z blue: the DCC convention, so a turned Empty reads its own
|
|
446
|
+
// orientation off the marker.
|
|
447
|
+
const EMPTY_AXES = [
|
|
448
|
+
{ to: [1, 0, 0], color: "#e5594f" },
|
|
449
|
+
{ to: [0, 1, 0], color: "#5fb95f" },
|
|
450
|
+
{ to: [0, 0, 1], color: "#4f86e5" },
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* An Empty's viewport presence: a three-axis cross and a pick volume, and
|
|
455
|
+
* nothing else. It is editor furniture, so every node sits on GIZMO_LAYER —
|
|
456
|
+
* the layer the shot camera, the preview card, PlayView, the ink prepass and
|
|
457
|
+
* the recorder all drop — and it casts no shadow because it has no mesh to
|
|
458
|
+
* cast one. The pick volume is invisible (never drawn) but still raycasts; the
|
|
459
|
+
* object picker finds it by `userData.emptyPick` on the gizmo-layer pass.
|
|
460
|
+
*/
|
|
461
|
+
function EmptyMarker() {
|
|
462
|
+
const half = EMPTY_MARKER_SIZE / 2;
|
|
463
|
+
const axes = useMemo(() => {
|
|
464
|
+
const positions = [];
|
|
465
|
+
const colors = [];
|
|
466
|
+
const color = new THREE.Color();
|
|
467
|
+
for (const axis of EMPTY_AXES) {
|
|
468
|
+
positions.push(-axis.to[0] * half, -axis.to[1] * half, -axis.to[2] * half, axis.to[0] * half, axis.to[1] * half, axis.to[2] * half);
|
|
469
|
+
color.set(axis.color);
|
|
470
|
+
colors.push(color.r, color.g, color.b, color.r, color.g, color.b);
|
|
471
|
+
}
|
|
472
|
+
const geometry = new THREE.BufferGeometry();
|
|
473
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
|
474
|
+
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
|
|
475
|
+
return geometry;
|
|
476
|
+
}, [half]);
|
|
477
|
+
useEffect(() => () => axes.dispose(), [axes]);
|
|
478
|
+
const onLayer = (node) => {
|
|
479
|
+
if (node) node.layers.set(GIZMO_LAYER);
|
|
480
|
+
};
|
|
481
|
+
return (
|
|
482
|
+
<group userData={{ emptyMarker: true }}>
|
|
483
|
+
<lineSegments ref={onLayer} geometry={axes} renderOrder={997} frustumCulled={false}>
|
|
484
|
+
<lineBasicMaterial vertexColors depthTest={false} depthWrite={false} transparent opacity={0.95} />
|
|
485
|
+
</lineSegments>
|
|
486
|
+
<mesh ref={onLayer} visible={false} userData={{ emptyPick: true }}>
|
|
487
|
+
<sphereGeometry args={[half * 1.25, 12, 8]} />
|
|
488
|
+
<meshBasicMaterial />
|
|
489
|
+
</mesh>
|
|
490
|
+
</group>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function SceneObjectContent({ object, depthRank = 0 }) {
|
|
426
495
|
// `autoColor` is the viewport-only display color the auto-color mode stamps
|
|
427
496
|
// onto the DISPLAYED object (App's displaySceneObjects); the authored
|
|
428
497
|
// `color` is untouched underneath. Cutouts stay out: tinting a photo
|
|
429
498
|
// standee destroys the one thing it is for. Imported meshes take the same
|
|
430
499
|
// override as a cube — file materials are cloned and tinted per instance.
|
|
431
500
|
const { renderer, color, autoColor } = object;
|
|
501
|
+
if (renderer === EMPTY_KIND) return <EmptyMarker />;
|
|
432
502
|
if (renderer === CUTOUT_KIND) return <Cutout object={object} />;
|
|
433
503
|
if (renderer === MESH_KIND) return <ImportedMesh object={object} />;
|
|
504
|
+
if (renderer === SCULPT_KIND) return <Sculpt object={object} depthRank={depthRank} />;
|
|
434
505
|
if (renderer === "car") return <Car color={autoColor ?? color} autoColor={autoColor} />;
|
|
435
506
|
if (renderer === "small-plane") return <SmallPlane autoColor={autoColor} />;
|
|
436
507
|
if (renderer === "chair") return <Chair autoColor={autoColor} />;
|
|
437
|
-
if (PRIMITIVE_KINDS.has(renderer)) return <Primitive kind={renderer} color={color} autoColor={autoColor} />;
|
|
508
|
+
if (PRIMITIVE_KINDS.has(renderer)) return <Primitive kind={renderer} color={color} autoColor={autoColor} depthRank={depthRank} />;
|
|
438
509
|
return null;
|
|
439
510
|
}
|
|
440
511
|
|
|
@@ -444,9 +515,12 @@ function SceneObjectContent({ object }) {
|
|
|
444
515
|
* object instead of a selection.
|
|
445
516
|
*/
|
|
446
517
|
function SelectionBox({ object }) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const
|
|
518
|
+
// An Empty has no extent, so its cage is the marker's own cube, centred on
|
|
519
|
+
// the origin (a prop's cage stands on it).
|
|
520
|
+
const empty = object.renderer === EMPTY_KIND;
|
|
521
|
+
const height = empty ? EMPTY_MARKER_SIZE : Math.max(object.height ?? 1, 0.08);
|
|
522
|
+
const width = empty ? EMPTY_MARKER_SIZE : object.footprint?.width ?? 1;
|
|
523
|
+
const depth = empty ? EMPTY_MARKER_SIZE : object.footprint?.depth ?? 1;
|
|
450
524
|
const edges = useMemo(
|
|
451
525
|
() => new THREE.EdgesGeometry(new THREE.BoxGeometry(width * 1.04, height * 1.04, depth * 1.04)),
|
|
452
526
|
[width, height, depth],
|
|
@@ -463,7 +537,7 @@ function SelectionBox({ object }) {
|
|
|
463
537
|
if (mesh) mesh.layers.set(GIZMO_LAYER);
|
|
464
538
|
}}
|
|
465
539
|
geometry={edges}
|
|
466
|
-
position={[0, height / 2, 0]}
|
|
540
|
+
position={[0, empty ? 0 : height / 2, 0]}
|
|
467
541
|
renderOrder={998}
|
|
468
542
|
>
|
|
469
543
|
<lineBasicMaterial color="#e7b557" transparent opacity={0.95} depthTest={false} depthWrite={false} />
|
|
@@ -482,7 +556,7 @@ const placeLocal = new THREE.Matrix4();
|
|
|
482
556
|
const placeWorld = new THREE.Matrix4();
|
|
483
557
|
const placeFrame = new THREE.Matrix4();
|
|
484
558
|
|
|
485
|
-
function SceneObject({ object, selected, frameRef = null, take = null, attachFrameRef = null, registryRef = null }) {
|
|
559
|
+
function SceneObject({ object, selected, frameRef = null, take = null, attachFrameRef = null, registryRef = null, travelLookupRef = null, fadeOpacity = null, depthRank = 0 }) {
|
|
486
560
|
const groupRef = useRef(null);
|
|
487
561
|
const attach = object.attach ?? null;
|
|
488
562
|
// An object on a travel path — or one CARRIED by a character — is placed
|
|
@@ -496,17 +570,27 @@ function SceneObject({ object, selected, frameRef = null, take = null, attachFra
|
|
|
496
570
|
const group = groupRef.current;
|
|
497
571
|
if (!group) return;
|
|
498
572
|
const frame = frameRef?.current ?? 0;
|
|
499
|
-
|
|
573
|
+
// A record under a routed parent travels with it (object-travel.js), and
|
|
574
|
+
// so does a routed record itself: one answer from the authored records.
|
|
575
|
+
const travelled = frameRef && !attach && travelLookupRef?.current
|
|
576
|
+
? sceneObjectTravelMatrixAt(travelLookupRef.current, object.id, frame, take ?? {}, placeWorld)
|
|
577
|
+
: null;
|
|
578
|
+
if (travelled) {
|
|
579
|
+
travelled.decompose(group.position, group.quaternion, group.scale);
|
|
580
|
+
} else if (attach || object.path) {
|
|
500
581
|
// The authored numbers first. While attached they are the prop's LOCAL
|
|
501
582
|
// transform in the attach frame; otherwise they are already world.
|
|
502
583
|
placePos.set(object.x, object.y ?? 0, object.z);
|
|
503
|
-
placeEuler.set((object.rotX ?? 0) * DEG, object.rot * DEG, (object.rotZ ?? 0) * DEG);
|
|
584
|
+
placeEuler.set((object.rotX ?? 0) * DEG, object.rot * DEG, (object.rotZ ?? 0) * DEG, "XYZ");
|
|
504
585
|
placeScale.set(object.scaleX ?? 1, object.scaleY ?? 1, object.scaleZ ?? 1);
|
|
505
586
|
if (frameRef && object.path) {
|
|
506
587
|
const at = objectTransformAt(object, frame, take ?? {});
|
|
507
588
|
if (at) {
|
|
508
589
|
placePos.set(at.x, at.y, at.z);
|
|
509
|
-
|
|
590
|
+
// The same pose object-travel gives a routed record: yaw, then
|
|
591
|
+
// the body's own pitch, then its roll (+ the route's lean).
|
|
592
|
+
const pose = travelPose(object, at);
|
|
593
|
+
placeEuler.set(pose.rotX * DEG, pose.rot * DEG, pose.rotZ * DEG, "YXZ");
|
|
510
594
|
}
|
|
511
595
|
}
|
|
512
596
|
// A missing rig (the character left the cast, or its model has not
|
|
@@ -530,8 +614,9 @@ function SceneObject({ object, selected, frameRef = null, take = null, attachFra
|
|
|
530
614
|
// the store only knows the authored one — and while attached the authored
|
|
531
615
|
// one is not even in world space. Harmless in normal use.
|
|
532
616
|
if (typeof window !== "undefined") {
|
|
533
|
-
(window.__cclayPropWorld ??= {})[object.id] = { x: group.position.x, y: group.position.y, z: group.position.z, frame };
|
|
617
|
+
(window.__cclayPropWorld ??= {})[object.id] = { x: group.position.x, y: group.position.y, z: group.position.z, quat: { x: group.quaternion.x, y: group.quaternion.y, z: group.quaternion.z, w: group.quaternion.w }, frame };
|
|
534
618
|
}
|
|
619
|
+
applyPoseFade(group, fadeOpacity, object.opacity ?? 1);
|
|
535
620
|
};
|
|
536
621
|
useFrame(place);
|
|
537
622
|
// Two things the App needs to reach imperatively, registered per prop: a
|
|
@@ -570,7 +655,7 @@ function SceneObject({ object, selected, frameRef = null, take = null, attachFra
|
|
|
570
655
|
// the viewport picker walks up from a hit mesh to find this id
|
|
571
656
|
userData={{ sceneObjectId: object.id }}
|
|
572
657
|
>
|
|
573
|
-
<SceneObjectContent object={object} />
|
|
658
|
+
<SceneObjectContent object={object} depthRank={depthRank} />
|
|
574
659
|
{selected && <SelectionBox object={object} />}
|
|
575
660
|
</group>
|
|
576
661
|
);
|
|
@@ -584,8 +669,16 @@ function SceneObject({ object, selected, frameRef = null, take = null, attachFra
|
|
|
584
669
|
* loop and would otherwise record props one frame stale; `worldRef` with a
|
|
585
670
|
* "where is this prop" lookup, so a reparent converts from the transform on
|
|
586
671
|
* screen instead of from a second computation of it. */
|
|
587
|
-
export function SetProps({ objects = [], selectedId = null, frameRef = null, take = null, attachFrameRef = null, syncRef = null, worldRef = null }) {
|
|
672
|
+
export function SetProps({ objects = [], authoredObjects = null, selectedId = null, frameRef = null, take = null, attachFrameRef = null, syncRef = null, worldRef = null, fadeOpacity = null }) {
|
|
588
673
|
const registryRef = useRef(null);
|
|
674
|
+
// The authored records by id: a child's travel is its routed parent's
|
|
675
|
+
// route against the parent's AUTHORED pose, which the display copies in
|
|
676
|
+
// `objects` no longer carry once they have been animated.
|
|
677
|
+
const travelLookupRef = useRef(null);
|
|
678
|
+
travelLookupRef.current = useMemo(() => (authoredObjects ? new Map(authoredObjects.map((object) => [object.id, object])) : null), [authoredObjects]);
|
|
679
|
+
// Coplanar-face ranks come from the AUTHORED records: a routed group moves
|
|
680
|
+
// rigidly, so what shares a plane at rest shares it on every frame.
|
|
681
|
+
const depthRanks = useMemo(() => coplanarDepthRanks(authoredObjects ?? objects), [authoredObjects, objects]);
|
|
589
682
|
if (!registryRef.current) registryRef.current = new Map();
|
|
590
683
|
if (syncRef) syncRef.current = () => { for (const entry of registryRef.current.values()) entry.place(); };
|
|
591
684
|
if (worldRef) worldRef.current = (id, out) => registryRef.current.get(id)?.world(out) ?? null;
|
|
@@ -600,8 +693,56 @@ export function SetProps({ objects = [], selectedId = null, frameRef = null, tak
|
|
|
600
693
|
take={take}
|
|
601
694
|
attachFrameRef={attachFrameRef}
|
|
602
695
|
registryRef={registryRef}
|
|
696
|
+
travelLookupRef={travelLookupRef}
|
|
697
|
+
fadeOpacity={fadeOpacity}
|
|
698
|
+
depthRank={depthRanks.get(object.id) ?? 0}
|
|
603
699
|
/>
|
|
604
700
|
))}
|
|
605
701
|
</group>
|
|
606
702
|
);
|
|
607
703
|
}
|
|
704
|
+
|
|
705
|
+
const carryMatrix = new THREE.Matrix4();
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* A group that rides a scene object's travel: whatever is inside it (a
|
|
709
|
+
* character grouped under a car) is moved and turned with the object's route,
|
|
710
|
+
* on top of its own transform and animation. Placed imperatively from the
|
|
711
|
+
* frame ref for the same reason a routed prop is — the offscreen export
|
|
712
|
+
* advances frames without a re-render — and registered in `registryRef` so the
|
|
713
|
+
* recorder can place every carrier before it renders a frame.
|
|
714
|
+
*
|
|
715
|
+
* `objectsRef.current` holds the AUTHORED records by id; with no object, or an
|
|
716
|
+
* object that does not travel, the group stays at identity.
|
|
717
|
+
*/
|
|
718
|
+
export function ObjectCarrier({ objectId = null, objectsRef, frameRef = null, take = null, registryRef = null, children }) {
|
|
719
|
+
const groupRef = useRef(null);
|
|
720
|
+
const place = () => {
|
|
721
|
+
const group = groupRef.current;
|
|
722
|
+
if (!group) return;
|
|
723
|
+
const carried = objectId && objectsRef?.current
|
|
724
|
+
? sceneObjectCarryMatrixAt(objectsRef.current, objectId, frameRef?.current ?? 0, take ?? {}, carryMatrix)
|
|
725
|
+
: null;
|
|
726
|
+
if (carried) carried.decompose(group.position, group.quaternion, group.scale);
|
|
727
|
+
else {
|
|
728
|
+
group.position.set(0, 0, 0);
|
|
729
|
+
group.quaternion.identity();
|
|
730
|
+
group.scale.set(1, 1, 1);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
// Ahead of every priority-0 placement: a prop held by a rider reads its
|
|
734
|
+
// bone's world matrix through this group, so the carry must land first or
|
|
735
|
+
// the prop trails the hand by one frame. Mount order alone does not hold
|
|
736
|
+
// (SetProps mounts before the cast; a rig remount moves to the back).
|
|
737
|
+
useFrame(place, -1);
|
|
738
|
+
const placeRef = useRef(place);
|
|
739
|
+
placeRef.current = place;
|
|
740
|
+
useEffect(() => {
|
|
741
|
+
if (!registryRef) return undefined;
|
|
742
|
+
const registry = registryRef.current;
|
|
743
|
+
const entry = () => placeRef.current();
|
|
744
|
+
registry.add(entry);
|
|
745
|
+
return () => { registry.delete(entry); };
|
|
746
|
+
}, [registryRef]);
|
|
747
|
+
return <group ref={groupRef}>{children}</group>;
|
|
748
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { memo, useEffect, useMemo } from "react";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
import { defaultCharacterTint } from "./app-stage.jsx";
|
|
4
|
+
|
|
5
|
+
const BODY_RADIUS = 0.22;
|
|
6
|
+
const BODY_HEIGHT = 1.45;
|
|
7
|
+
const HEAD_RADIUS = 0.12;
|
|
8
|
+
|
|
9
|
+
export function proxyYaw(rot, posture = "stand") {
|
|
10
|
+
return (rot * Math.PI) / 180 + (posture === "lie" ? Math.PI : 0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ProxyFigure = memo(function ProxyFigure({
|
|
14
|
+
position,
|
|
15
|
+
rot,
|
|
16
|
+
tint,
|
|
17
|
+
scale = 1,
|
|
18
|
+
posture = "stand",
|
|
19
|
+
pickId,
|
|
20
|
+
selected,
|
|
21
|
+
}) {
|
|
22
|
+
const bodyHeight = posture === "sit" ? BODY_HEIGHT * 0.6 : BODY_HEIGHT;
|
|
23
|
+
const headY = bodyHeight + 0.13;
|
|
24
|
+
const model = useMemo(() => {
|
|
25
|
+
const bodyMaterial = new THREE.MeshStandardMaterial({
|
|
26
|
+
color: tint ?? defaultCharacterTint({ model: "proxy-figure" }, 0),
|
|
27
|
+
roughness: 0.66,
|
|
28
|
+
metalness: 0,
|
|
29
|
+
envMapIntensity: 0.35,
|
|
30
|
+
});
|
|
31
|
+
const group = new THREE.Group();
|
|
32
|
+
const body = new THREE.Mesh(new THREE.CapsuleGeometry(BODY_RADIUS, bodyHeight - BODY_RADIUS * 2, 12, 20), bodyMaterial);
|
|
33
|
+
body.position.y = bodyHeight / 2;
|
|
34
|
+
body.castShadow = true;
|
|
35
|
+
body.receiveShadow = true;
|
|
36
|
+
body.frustumCulled = false;
|
|
37
|
+
group.add(body);
|
|
38
|
+
const head = new THREE.Mesh(new THREE.SphereGeometry(HEAD_RADIUS, 16, 12), bodyMaterial);
|
|
39
|
+
head.position.y = headY;
|
|
40
|
+
head.castShadow = true;
|
|
41
|
+
head.receiveShadow = true;
|
|
42
|
+
head.frustumCulled = false;
|
|
43
|
+
group.add(head);
|
|
44
|
+
return group;
|
|
45
|
+
}, [bodyHeight, headY, tint]);
|
|
46
|
+
useEffect(() => () => {
|
|
47
|
+
const materials = new Set();
|
|
48
|
+
model.traverse(node => {
|
|
49
|
+
if (!node.isMesh) return;
|
|
50
|
+
node.geometry.dispose();
|
|
51
|
+
materials.add(node.material);
|
|
52
|
+
});
|
|
53
|
+
for (const material of materials) material.dispose();
|
|
54
|
+
}, [model]);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<group
|
|
58
|
+
position={position}
|
|
59
|
+
rotation={[0, proxyYaw(rot, posture), 0]}
|
|
60
|
+
scale={scale}
|
|
61
|
+
userData={pickId ? { characterPick: pickId, selected: !!selected } : undefined}
|
|
62
|
+
>
|
|
63
|
+
<group position={posture === "lie" ? [0, BODY_RADIUS, 0] : [0, 0, 0]} rotation={posture === "lie" ? [-Math.PI / 2, 0, 0] : [0, 0, 0]}>
|
|
64
|
+
<primitive object={model} />
|
|
65
|
+
</group>
|
|
66
|
+
</group>
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
package/src/range-pin-panel.jsx
CHANGED