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/shot-authoring.js
CHANGED
|
@@ -9,26 +9,62 @@ import { createShot } from "./cuts.js";
|
|
|
9
9
|
import { normalizeStableItems } from "./stable-items.js";
|
|
10
10
|
import { VIDEO_MODEL_PRESETS } from "./model-presets.js";
|
|
11
11
|
import { elementByPath } from "./studio-elements.js";
|
|
12
|
+
import { isImageAssetId } from "./scene-assets.js";
|
|
13
|
+
import { createCharacterEntry } from "./scenes.js";
|
|
12
14
|
|
|
13
15
|
const VIDEO_MODEL_IDS = new Set(VIDEO_MODEL_PRESETS.map((preset) => preset.id));
|
|
14
16
|
const CAMERA_KEY_LIMITS = elementByPath("shot.cameraKeys");
|
|
15
17
|
|
|
16
|
-
export const SHOT_AUTHORING_VERSION =
|
|
17
|
-
export const SHOT_AUTHORING_KEY = "cozyclay.shot-authoring.
|
|
18
|
+
export const SHOT_AUTHORING_VERSION = 5;
|
|
19
|
+
export const SHOT_AUTHORING_KEY = "cozyclay.shot-authoring.v5";
|
|
18
20
|
// The single-key alias points at the newest legacy body for older callers.
|
|
19
21
|
// New readers should walk the list so a user can still arrive directly from v1.
|
|
20
|
-
export const SHOT_AUTHORING_LEGACY_KEY = "cozyclay.shot-authoring.
|
|
22
|
+
export const SHOT_AUTHORING_LEGACY_KEY = "cozyclay.shot-authoring.v4";
|
|
21
23
|
export const SHOT_AUTHORING_LEGACY_KEYS = Object.freeze([
|
|
22
24
|
SHOT_AUTHORING_LEGACY_KEY,
|
|
25
|
+
"cozyclay.shot-authoring.v3",
|
|
23
26
|
"cozyclay.shot-authoring.v2",
|
|
24
27
|
"cozyclay.shot-authoring.v1",
|
|
25
28
|
]);
|
|
26
|
-
export const SHOT_AUTHORING_QUARANTINE_KEY = "cozyclay.shot-authoring.
|
|
29
|
+
export const SHOT_AUTHORING_QUARANTINE_KEY = "cozyclay.shot-authoring.v5.quarantine";
|
|
30
|
+
|
|
31
|
+
/** A still holds one picture for `endFrame - startFrame + 1` frames (24 fps). */
|
|
32
|
+
export const STILL_HOLD_DEFAULT = 48;
|
|
33
|
+
export const STILL_HOLD_MAX = 240;
|
|
34
|
+
export const shotHold = (shot) => shot.endFrame - shot.startFrame + 1;
|
|
35
|
+
|
|
36
|
+
/** Lay the stills end to end from frame 0 in their array order, each keeping
|
|
37
|
+
* its hold; camera keys travel with their still. Clip shots are untouched.
|
|
38
|
+
* The result is sorted by startFrame, the order the document stores. */
|
|
39
|
+
export function reflowStillShots(shots) {
|
|
40
|
+
let cursor = 0;
|
|
41
|
+
return shots.map((shot) => {
|
|
42
|
+
if (shot.kind !== "still") return shot;
|
|
43
|
+
const hold = shotHold(shot), delta = cursor - shot.startFrame;
|
|
44
|
+
cursor += hold;
|
|
45
|
+
if (!delta) return shot;
|
|
46
|
+
return { ...shot, startFrame: shot.startFrame + delta, endFrame: shot.endFrame + delta,
|
|
47
|
+
cameraKeys: shot.cameraKeys.map((key) => ({ ...key, frame: key.frame + delta })) };
|
|
48
|
+
}).sort((a, b) => a.startFrame - b.startFrame);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The timeline length a reflowed document needs, unclamped so a caller can
|
|
52
|
+
* refuse a storyboard past STILL_FRAME_COUNT_MAX instead of truncating it. */
|
|
53
|
+
export function stillFrameCount(shots) {
|
|
54
|
+
return Math.max(DEFAULT_FRAME_COUNT, ...shots.map((shot) => shot.endFrame + 1));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const SHOT_KINDS = new Set(["clip", "still"]);
|
|
58
|
+
const CAPTION_MAX = 500;
|
|
59
|
+
// Mirrors the cast posture vocabulary (POSTURES in src/scenes.js once the
|
|
60
|
+
// proxy-figure work lands); kept local so this module does not wait on it.
|
|
61
|
+
const POSTURES = new Set(["stand", "sit", "lie"]);
|
|
27
62
|
|
|
28
63
|
/** clip length sanity bounds, frames @ 24 fps: 1 s .. 20 min */
|
|
29
64
|
const FRAME_COUNT_MIN = 24;
|
|
30
65
|
const FRAME_COUNT_MAX = 28800;
|
|
31
66
|
const DEFAULT_FRAME_COUNT = 144;
|
|
67
|
+
export const STILL_FRAME_COUNT_MAX = FRAME_COUNT_MAX;
|
|
32
68
|
const RAIL_MAX_POINTS = 512;
|
|
33
69
|
const finite = Number.isFinite;
|
|
34
70
|
|
|
@@ -159,10 +195,36 @@ function repairShots(entries, frameCount, inheritedCamera = null, ids = new Set(
|
|
|
159
195
|
cameraKeys: repairKeys(entry.cameraKeys, startFrame, endFrame, ids),
|
|
160
196
|
camera: repairCamera(inheritedCamera ?? entry.camera),
|
|
161
197
|
...targetModel,
|
|
198
|
+
kind: SHOT_KINDS.has(entry.kind) ? entry.kind : "clip",
|
|
199
|
+
caption: repairCaption(entry.caption),
|
|
200
|
+
cast: repairCast(entry.cast),
|
|
201
|
+
stylizedAssetId: isImageAssetId(entry.stylizedAssetId) ? entry.stylizedAssetId : null,
|
|
162
202
|
};
|
|
163
203
|
});
|
|
164
204
|
}
|
|
165
205
|
|
|
206
|
+
function repairCaption(value) {
|
|
207
|
+
return typeof value === "string" ? Array.from(value.trim()).slice(0, CAPTION_MAX).join("") : "";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Per-shot placement overrides keyed by character id. An entry without a
|
|
211
|
+
* usable id or a finite x/z/rot is dropped; its valid siblings survive. */
|
|
212
|
+
function repairCast(value) {
|
|
213
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
214
|
+
const rows = [];
|
|
215
|
+
for (const [characterId, entry] of Object.entries(value)) {
|
|
216
|
+
if (!characterId.trim() || !entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
217
|
+
if (!finite(entry.x) || !finite(entry.z) || !finite(entry.rot)) continue;
|
|
218
|
+
const row = { x: entry.x, z: entry.z, rot: entry.rot };
|
|
219
|
+
if (POSTURES.has(entry.posture)) row.posture = entry.posture;
|
|
220
|
+
const pose = createCharacterEntry({ pose: entry.pose }).pose;
|
|
221
|
+
if (pose) row.pose = pose;
|
|
222
|
+
rows.push([characterId, row]);
|
|
223
|
+
}
|
|
224
|
+
// fromEntries defines own properties, so a "__proto__" id stays data.
|
|
225
|
+
return Object.fromEntries(rows);
|
|
226
|
+
}
|
|
227
|
+
|
|
166
228
|
function repairWaypoints(entries, ids = new Set()) {
|
|
167
229
|
const byFrame = new Map();
|
|
168
230
|
for (const waypoint of normalizeStableItems(entries, "waypoint", ids)) {
|
|
@@ -313,8 +375,9 @@ export function readShotAuthoringDocument(raw) {
|
|
|
313
375
|
}
|
|
314
376
|
const ids = new Set();
|
|
315
377
|
return {
|
|
316
|
-
// A v3 body
|
|
317
|
-
//
|
|
378
|
+
// A v3 body was authored on the old clock and a v4 body lacks the v5
|
|
379
|
+
// kind/caption/cast/stylizedAssetId defaults; both are rewritten, so they
|
|
380
|
+
// report as migrated, not valid.
|
|
318
381
|
status: version < SHOT_AUTHORING_VERSION ? "migrated" : "valid",
|
|
319
382
|
state: { ...repairShared(parsed, frameCount, ids), shots: repairShots(parsed.shots, effectiveFrameCount, null, ids) },
|
|
320
383
|
};
|
package/src/stable-items.js
CHANGED
|
@@ -18,7 +18,7 @@ function hash(value) {
|
|
|
18
18
|
return (result >>> 0).toString(36);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
class UnknownStableItemIdError extends Error {
|
|
22
22
|
constructor(collection, id) {
|
|
23
23
|
super(`Unknown ${collection} ID: ${id}`);
|
|
24
24
|
this.name = "UnknownStableItemIdError";
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { SCENE_ATTACH_BONES } from "./scene-objects.js";
|
|
3
|
+
import { TRAIL_EFFECTOR_JOINTS } from "./motion-trail.js";
|
|
4
|
+
import { normalizeBoneName } from "./poses.js";
|
|
5
|
+
|
|
6
|
+
/** The rig rows that ARE an attach frame: `rig.<track id>`, the same camel
|
|
7
|
+
* spellings SCENE_ATTACH_BONES uses. The group rows above them (rig.torso,
|
|
8
|
+
* rig.leftArm …) name a limb, not a frame, so they are deliberately absent. */
|
|
9
|
+
export const ATTACH_BONE_ROWS = new Map(SCENE_ATTACH_BONES.map((bone) => [`rig.${bone}`, bone]));
|
|
10
|
+
|
|
11
|
+
/** Attach track id -> the Mixamo bone carrying that frame on the shipped rigs.
|
|
12
|
+
* TRAIL_EFFECTOR_JOINTS names the cskel27 joint, and cskel27 and Mixamo agree
|
|
13
|
+
* everywhere except the spine, which sits one bone up the Mixamo chain (the
|
|
14
|
+
* same shift ardy/playback.js SKINNING_MAP makes). */
|
|
15
|
+
export const ATTACH_BONE_NAMES = Object.fromEntries(SCENE_ATTACH_BONES.map((bone) => {
|
|
16
|
+
const joint = TRAIL_EFFECTOR_JOINTS[bone];
|
|
17
|
+
return [bone, joint === "Spine1" ? "Spine" : joint === "Spine2" ? "Spine1" : joint];
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// One scratch set for the whole module: resolving a frame runs per attached
|
|
21
|
+
// prop per frame and must never allocate.
|
|
22
|
+
export const attachPos = new THREE.Vector3();
|
|
23
|
+
export const attachQuat = new THREE.Quaternion();
|
|
24
|
+
export const attachScale = new THREE.Vector3();
|
|
25
|
+
export const attachRootPos = new THREE.Vector3();
|
|
26
|
+
export const attachRootQuat = new THREE.Quaternion();
|
|
27
|
+
export const ATTACH_UNIT_SCALE = new THREE.Vector3(1, 1, 1);
|
|
28
|
+
export const attachWorldMatrix = new THREE.Matrix4();
|
|
29
|
+
export const attachLocalMatrix = new THREE.Matrix4();
|
|
30
|
+
export const attachToMatrix = new THREE.Matrix4();
|
|
31
|
+
export const attachInverseMatrix = new THREE.Matrix4();
|
|
32
|
+
export const placePos = new THREE.Vector3();
|
|
33
|
+
export const placeQuat = new THREE.Quaternion();
|
|
34
|
+
export const placeScale = new THREE.Vector3();
|
|
35
|
+
export const placeEuler = new THREE.Euler();
|
|
36
|
+
|
|
37
|
+
/** The bone a rig offers under `mixamoName`, by the project's matching rule
|
|
38
|
+
* (normalised names equal, or one a suffix of the other; first depth-first
|
|
39
|
+
* match wins — poses.js and ardy/playback.js agree). Cached per rig: this is
|
|
40
|
+
* asked once per attached prop per rendered frame. */
|
|
41
|
+
export const attachBoneCache = new WeakMap();
|
|
42
|
+
export function attachBoneOf(rig, mixamoName) {
|
|
43
|
+
let byName = attachBoneCache.get(rig);
|
|
44
|
+
if (!byName) attachBoneCache.set(rig, (byName = new Map()));
|
|
45
|
+
if (byName.has(mixamoName)) return byName.get(mixamoName);
|
|
46
|
+
const target = normalizeBoneName(mixamoName);
|
|
47
|
+
let found = null;
|
|
48
|
+
rig.traverse((node) => {
|
|
49
|
+
if (found || !node.isBone) return;
|
|
50
|
+
const norm = normalizeBoneName(node.name);
|
|
51
|
+
if (norm === target || norm.endsWith(target)) found = node;
|
|
52
|
+
});
|
|
53
|
+
byName.set(mixamoName, found);
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The LIVE world frame an attachment rides, or null when the rig (or that bone)
|
|
59
|
+
* is not there — a dangling attachment then renders where its numbers say,
|
|
60
|
+
* which is exactly what a detached prop does.
|
|
61
|
+
*
|
|
62
|
+
* The frame is deliberately UNSCALED. The shipped rigs are Mixamo centimetres
|
|
63
|
+
* scaled by 0.01 on the model, so a raw bone matrix would push a hundredfold
|
|
64
|
+
* into every carried prop's local numbers and leave the Inspector unreadable.
|
|
65
|
+
* A carried prop takes the bone's place and facing; its size stays its own.
|
|
66
|
+
*
|
|
67
|
+
* `bone === null` is the character's animated ROOT: the hips' travel (during
|
|
68
|
+
* playback the whole root trajectory lives in the bones — see
|
|
69
|
+
* ardy/playback.js) carried at the character's own yaw, so a prop dropped on
|
|
70
|
+
* the character walks with the body instead of rolling with the hips.
|
|
71
|
+
*/
|
|
72
|
+
export function attachFrameMatrix(rig, bone, out = new THREE.Matrix4()) {
|
|
73
|
+
if (!rig) return null;
|
|
74
|
+
const name = ATTACH_BONE_NAMES[bone ?? "hips"];
|
|
75
|
+
if (!name) return null;
|
|
76
|
+
const node = attachBoneOf(rig, name);
|
|
77
|
+
if (!node) return null;
|
|
78
|
+
// Ancestors included: the bones are written imperatively by playback and by
|
|
79
|
+
// the export, either of which can land before the renderer's own pass.
|
|
80
|
+
node.updateWorldMatrix(true, false);
|
|
81
|
+
node.matrixWorld.decompose(attachPos, attachQuat, attachScale);
|
|
82
|
+
if (bone != null) return out.compose(attachPos, attachQuat, ATTACH_UNIT_SCALE);
|
|
83
|
+
// The Character group carries the scene placement and the clip-to-scene yaw;
|
|
84
|
+
// the rig model hangs off it, so its parent IS that group.
|
|
85
|
+
const group = rig.parent ?? rig;
|
|
86
|
+
group.updateWorldMatrix(true, false);
|
|
87
|
+
group.matrixWorld.decompose(attachRootPos, attachRootQuat, attachScale);
|
|
88
|
+
return out.compose(attachPos, attachRootQuat, ATTACH_UNIT_SCALE);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A prop's placement as a matrix — the same compose props.jsx renders from
|
|
92
|
+
* (position, XYZ euler in degrees, per-axis scale). */
|
|
93
|
+
export function sceneObjectMatrix(object, out = new THREE.Matrix4()) {
|
|
94
|
+
return out.compose(
|
|
95
|
+
placePos.set(object.x ?? 0, object.y ?? 0, object.z ?? 0),
|
|
96
|
+
placeQuat.setFromEuler(placeEuler.set(
|
|
97
|
+
(object.rotX ?? 0) * THREE.MathUtils.DEG2RAD,
|
|
98
|
+
(object.rot ?? 0) * THREE.MathUtils.DEG2RAD,
|
|
99
|
+
(object.rotZ ?? 0) * THREE.MathUtils.DEG2RAD,
|
|
100
|
+
)),
|
|
101
|
+
placeScale.set(object.scaleX ?? 1, object.scaleY ?? 1, object.scaleZ ?? 1),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** …and back out into the nine authored channels, or null if the matrix could
|
|
106
|
+
* not be read as a placement at all (a singular frame inverts to zeros, and a
|
|
107
|
+
* zero matrix decomposes to NaN). Rounded, because a matrix round trip
|
|
108
|
+
* otherwise turns a typed 1.5 into 1.4999999999999998 in the Inspector the
|
|
109
|
+
* moment a prop is picked up and put down again. */
|
|
110
|
+
export function sceneObjectPlacement(matrix) {
|
|
111
|
+
matrix.decompose(placePos, placeQuat, placeScale);
|
|
112
|
+
placeEuler.setFromQuaternion(placeQuat);
|
|
113
|
+
const round = (value) => Math.round(value * 1e6) / 1e6;
|
|
114
|
+
const placement = {
|
|
115
|
+
x: round(placePos.x), y: round(placePos.y), z: round(placePos.z),
|
|
116
|
+
rotX: round(placeEuler.x * THREE.MathUtils.RAD2DEG),
|
|
117
|
+
rot: round(placeEuler.y * THREE.MathUtils.RAD2DEG),
|
|
118
|
+
rotZ: round(placeEuler.z * THREE.MathUtils.RAD2DEG),
|
|
119
|
+
scaleX: round(placeScale.x), scaleY: round(placeScale.y), scaleZ: round(placeScale.z),
|
|
120
|
+
};
|
|
121
|
+
return Object.values(placement).every((value) => Number.isFinite(value)) ? placement : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The nine channels rewritten so a prop does not MOVE when it changes frames.
|
|
126
|
+
*
|
|
127
|
+
* `world` is where the prop actually IS — read off the live group in the scene
|
|
128
|
+
* graph, NOT recomputed from its numbers. That distinction is the whole point:
|
|
129
|
+
* an attach->attach drop would otherwise have to reconstruct the world
|
|
130
|
+
* transform through the frame the prop is leaving, and any disagreement
|
|
131
|
+
* between that reconstruction and what the renderer last drew (a rig posed a
|
|
132
|
+
* beat later than the last drawn frame, a frame that no longer resolves) lands
|
|
133
|
+
* on screen as a jump. Reading the group makes every drop the same one-frame
|
|
134
|
+
* conversion the world->attach and attach->world cases already were.
|
|
135
|
+
*
|
|
136
|
+
* Returns null when the frame it needs is missing — converting against a rig
|
|
137
|
+
* that is not on stage would be precisely the jump this exists to prevent —
|
|
138
|
+
* and the caller then refuses the whole drop rather than re-labelling numbers
|
|
139
|
+
* it could not convert.
|
|
140
|
+
*/
|
|
141
|
+
export function attachPlacementPatch(world, attach, frameOf) {
|
|
142
|
+
if (!world) return null;
|
|
143
|
+
if (!attach) return sceneObjectPlacement(attachLocalMatrix.copy(world));
|
|
144
|
+
const frame = frameOf(attach.characterId, attach.bone ?? null, attachToMatrix);
|
|
145
|
+
if (!frame) return null;
|
|
146
|
+
return sceneObjectPlacement(attachLocalMatrix.copy(world).premultiply(attachInverseMatrix.copy(frame).invert()));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Write a computed placement straight onto the record, around
|
|
151
|
+
* updateSceneObject. Deliberate, and the only call site allowed to: those
|
|
152
|
+
* limits are WORLD limits (y floors at the deck, x/z stay inside the room),
|
|
153
|
+
* and while a prop is attached its numbers are a LOCAL transform in a bone
|
|
154
|
+
* frame — a hand rides 1.3 m above the deck, so a carried prop's local y is
|
|
155
|
+
* routinely negative and clamping it to the floor IS the visual jump the
|
|
156
|
+
* conversion exists to prevent. Every value here comes out of a decomposed
|
|
157
|
+
* matrix, so the record shape is sound by construction.
|
|
158
|
+
*/
|
|
159
|
+
export function placeSceneObject(objects, id, placement) {
|
|
160
|
+
if (!placement) return objects;
|
|
161
|
+
let changed = false;
|
|
162
|
+
const next = objects.map((object) => {
|
|
163
|
+
if (object.id !== id) return object;
|
|
164
|
+
if (Object.entries(placement).every(([key, value]) => object[key] === value)) return object;
|
|
165
|
+
changed = true;
|
|
166
|
+
return { ...object, ...placement };
|
|
167
|
+
});
|
|
168
|
+
return changed ? next : objects;
|
|
169
|
+
}
|