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
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Depth ranks for coplanar faces of scene-object primitives.
|
|
5
|
+
*
|
|
6
|
+
* Agent-built assemblies stack parts whose same-facing faces lie in exactly
|
|
7
|
+
* one plane (a hood and a grille sharing a top at y=1.05). The depth test
|
|
8
|
+
* cannot order them, so as the camera moves the winner flips every frame and
|
|
9
|
+
* the strip sparkles. The ranks computed here let the renderer pull the
|
|
10
|
+
* later-created part toward the camera by a polygon offset, which holds
|
|
11
|
+
* whatever the camera does. The records are never edited.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const DEG = Math.PI / 180;
|
|
15
|
+
/** Planes closer than this are one plane. */
|
|
16
|
+
const PLANE_EPS = 1e-3;
|
|
17
|
+
/** Normals this aligned (cosine) face the same way. */
|
|
18
|
+
const NORMAL_DOT = 0.9999;
|
|
19
|
+
/** Overlap shallower than this is two faces touching along an edge. */
|
|
20
|
+
const OVERLAP_EPS = 1e-4;
|
|
21
|
+
/** Bounds the offset: a deeper chain shares the top rank. */
|
|
22
|
+
export const MAX_DEPTH_RANK = 6;
|
|
23
|
+
|
|
24
|
+
// Planar faces per kind, in the primitive's local frame BEFORE scale: the axis
|
|
25
|
+
// the face is perpendicular to, which side, and where along that axis it sits
|
|
26
|
+
// (cube/cylinder/cone sit on y in [0, 1] — see Primitive in props.jsx).
|
|
27
|
+
const BOX_FACES = [
|
|
28
|
+
{ axis: 0, side: 1, at: 0.5 }, { axis: 0, side: -1, at: -0.5 },
|
|
29
|
+
{ axis: 1, side: 1, at: 1 }, { axis: 1, side: -1, at: 0 },
|
|
30
|
+
{ axis: 2, side: 1, at: 0.5 }, { axis: 2, side: -1, at: -0.5 },
|
|
31
|
+
];
|
|
32
|
+
const CAP_TOP = { axis: 1, side: 1, at: 1 };
|
|
33
|
+
const CAP_BASE = { axis: 1, side: -1, at: 0 };
|
|
34
|
+
const FACES_BY_KIND = {
|
|
35
|
+
cube: BOX_FACES,
|
|
36
|
+
cylinder: [CAP_TOP, CAP_BASE],
|
|
37
|
+
cone: [CAP_BASE],
|
|
38
|
+
// 2x2, a hair above the floor, drawn double-sided
|
|
39
|
+
plane: [{ axis: 1, side: 1, at: 0.004, flat: true }, { axis: 1, side: -1, at: 0.004, flat: true }],
|
|
40
|
+
};
|
|
41
|
+
// Local centre of the box the faces live on, per axis (x/z centred, y from 0).
|
|
42
|
+
const MID = [0, 0.5, 0];
|
|
43
|
+
const HALF = [0.5, 0.5, 0.5];
|
|
44
|
+
const PLANE_HALF = [1, 0, 1];
|
|
45
|
+
|
|
46
|
+
const rot = new THREE.Matrix4();
|
|
47
|
+
const euler = new THREE.Euler();
|
|
48
|
+
|
|
49
|
+
/** World-space rectangular faces of one object, or null when it has none. */
|
|
50
|
+
function facesOf(object) {
|
|
51
|
+
const kind = object.renderer ?? object.kind;
|
|
52
|
+
const table = FACES_BY_KIND[kind];
|
|
53
|
+
if (!table || object.attach || object.hidden === true) return null;
|
|
54
|
+
euler.set((object.rotX ?? 0) * DEG, (object.rot ?? 0) * DEG, (object.rotZ ?? 0) * DEG);
|
|
55
|
+
rot.makeRotationFromEuler(euler);
|
|
56
|
+
const e = rot.elements;
|
|
57
|
+
// column a of the rotation = where local axis a points in the world
|
|
58
|
+
const axes = [[e[0], e[1], e[2]], [e[4], e[5], e[6]], [e[8], e[9], e[10]]];
|
|
59
|
+
const scale = [object.scaleX ?? 1, object.scaleY ?? 1, object.scaleZ ?? 1];
|
|
60
|
+
const pos = [object.x ?? 0, object.y ?? 0, object.z ?? 0];
|
|
61
|
+
if (![...scale, ...pos, ...axes[0]].every(Number.isFinite)) return null;
|
|
62
|
+
const faces = [];
|
|
63
|
+
for (const face of table) {
|
|
64
|
+
const a = face.axis;
|
|
65
|
+
const u = (a + 1) % 3;
|
|
66
|
+
const v = (a + 2) % 3;
|
|
67
|
+
// a mirrored scale turns the outward normal around with it
|
|
68
|
+
const flip = face.side * (scale[a] < 0 ? -1 : 1);
|
|
69
|
+
const half = face.flat ? PLANE_HALF : HALF;
|
|
70
|
+
const centre = [0, 0, 0];
|
|
71
|
+
for (let i = 0; i < 3; i += 1) {
|
|
72
|
+
const local = i === a ? face.at : face.flat ? 0 : MID[i];
|
|
73
|
+
for (let k = 0; k < 3; k += 1) centre[k] += axes[i][k] * local * scale[i];
|
|
74
|
+
}
|
|
75
|
+
faces.push({
|
|
76
|
+
n: [axes[a][0] * flip, axes[a][1] * flip, axes[a][2] * flip],
|
|
77
|
+
c: [centre[0] + pos[0], centre[1] + pos[1], centre[2] + pos[2]],
|
|
78
|
+
u: axes[u],
|
|
79
|
+
v: axes[v],
|
|
80
|
+
hu: half[u] * Math.abs(scale[u]),
|
|
81
|
+
hv: half[v] * Math.abs(scale[v]),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// world box of the faces, for the pair prefilter
|
|
85
|
+
const min = [Infinity, Infinity, Infinity];
|
|
86
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
87
|
+
for (const f of faces) {
|
|
88
|
+
for (let k = 0; k < 3; k += 1) {
|
|
89
|
+
const reach = Math.abs(f.u[k]) * f.hu + Math.abs(f.v[k]) * f.hv;
|
|
90
|
+
min[k] = Math.min(min[k], f.c[k] - reach);
|
|
91
|
+
max[k] = Math.max(max[k], f.c[k] + reach);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { faces, min, max };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const dot = (p, q) => p[0] * q[0] + p[1] * q[1] + p[2] * q[2];
|
|
98
|
+
|
|
99
|
+
// Overlap of two rectangles lying in one plane, by separating axes in the
|
|
100
|
+
// plane: the rectangles' own edge directions are the only candidates.
|
|
101
|
+
function overlaps(a, b) {
|
|
102
|
+
const rel = [b.c[0] - a.c[0], b.c[1] - a.c[1], b.c[2] - a.c[2]];
|
|
103
|
+
for (const axis of [a.u, a.v, b.u, b.v]) {
|
|
104
|
+
const reachA = Math.abs(dot(axis, a.u)) * a.hu + Math.abs(dot(axis, a.v)) * a.hv;
|
|
105
|
+
const reachB = Math.abs(dot(axis, b.u)) * b.hu + Math.abs(dot(axis, b.v)) * b.hv;
|
|
106
|
+
if (Math.abs(dot(axis, rel)) >= reachA + reachB - OVERLAP_EPS) return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Every pair of same-facing faces of a and b that share a plane and overlap. */
|
|
112
|
+
function conflictingFaces(a, b) {
|
|
113
|
+
const found = [];
|
|
114
|
+
for (const fa of a.faces) {
|
|
115
|
+
for (const fb of b.faces) {
|
|
116
|
+
if (dot(fa.n, fb.n) <= NORMAL_DOT) continue;
|
|
117
|
+
const gap = dot(fa.n, [fb.c[0] - fa.c[0], fb.c[1] - fa.c[1], fb.c[2] - fa.c[2]]);
|
|
118
|
+
if (Math.abs(gap) > PLANE_EPS) continue;
|
|
119
|
+
if (overlaps(fa, fb)) found.push(fa);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return found;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function conflict(a, b) {
|
|
126
|
+
for (const fa of a.faces) {
|
|
127
|
+
for (const fb of b.faces) {
|
|
128
|
+
if (dot(fa.n, fb.n) <= NORMAL_DOT) continue;
|
|
129
|
+
const gap = dot(fa.n, [fb.c[0] - fa.c[0], fb.c[1] - fa.c[1], fb.c[2] - fa.c[2]]);
|
|
130
|
+
if (Math.abs(gap) > PLANE_EPS) continue;
|
|
131
|
+
if (overlaps(fa, fb)) return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Which objects need to win the depth test over an earlier one.
|
|
139
|
+
*
|
|
140
|
+
* Two planar faces conflict when they face the same way, share a plane to
|
|
141
|
+
* within a millimetre and overlap with real area; touching boxes (opposite
|
|
142
|
+
* facing) never do. The later object in `objects` is the later-made detail, so
|
|
143
|
+
* each conflict is an edge earlier -> later, and an object's rank is the length
|
|
144
|
+
* of the longest chain ending at it (capped). Only ranked objects (> 0) get an
|
|
145
|
+
* entry. Run it on the AUTHORED records: a routed group moves rigidly, so
|
|
146
|
+
* relative geometry inside it does not change with the frame.
|
|
147
|
+
*
|
|
148
|
+
* Skipped: spheres, capsules, models, cutouts, props carried by a character
|
|
149
|
+
* and hidden props. Cylinder and cone caps are approximated by their bounding
|
|
150
|
+
* rectangle, which can over-report a conflict near a rim; that costs a
|
|
151
|
+
* sub-pixel pull on a prop and nothing else.
|
|
152
|
+
*
|
|
153
|
+
* @param {Array<object>} objects scene-object records, in creation order
|
|
154
|
+
* @returns {Map<string, number>} id -> rank
|
|
155
|
+
*/
|
|
156
|
+
export function coplanarDepthRanks(objects) {
|
|
157
|
+
const ranks = new Map();
|
|
158
|
+
if (!Array.isArray(objects) || objects.length < 2) return ranks;
|
|
159
|
+
const shapes = objects.map((object) => (object && typeof object === "object" ? facesOf(object) : null));
|
|
160
|
+
const rank = new Array(objects.length).fill(0);
|
|
161
|
+
for (let j = 1; j < objects.length; j += 1) {
|
|
162
|
+
const b = shapes[j];
|
|
163
|
+
if (!b) continue;
|
|
164
|
+
for (let i = 0; i < j; i += 1) {
|
|
165
|
+
const a = shapes[i];
|
|
166
|
+
if (!a || rank[i] + 1 <= rank[j]) continue;
|
|
167
|
+
if (
|
|
168
|
+
a.min[0] > b.max[0] + PLANE_EPS || b.min[0] > a.max[0] + PLANE_EPS ||
|
|
169
|
+
a.min[1] > b.max[1] + PLANE_EPS || b.min[1] > a.max[1] + PLANE_EPS ||
|
|
170
|
+
a.min[2] > b.max[2] + PLANE_EPS || b.min[2] > a.max[2] + PLANE_EPS
|
|
171
|
+
) continue;
|
|
172
|
+
if (conflict(a, b)) rank[j] = Math.min(MAX_DEPTH_RANK, rank[i] + 1);
|
|
173
|
+
}
|
|
174
|
+
if (rank[j] > 0) ranks.set(objects[j].id, rank[j]);
|
|
175
|
+
}
|
|
176
|
+
return ranks;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const AXES = ["x", "y", "z"];
|
|
180
|
+
const FACE_NAMES = { "y+": "tops", "y-": "bottoms" };
|
|
181
|
+
const round = (value) => Number(value.toFixed(4));
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The coplanar conflicts coplanarDepthRanks resolves by offset, named: for
|
|
185
|
+
* each pair of objects with same-facing faces in one plane, the shared planes
|
|
186
|
+
* ("tops at y=1.05"). Informational for a receipt; it does not decide ranks.
|
|
187
|
+
*
|
|
188
|
+
* @param {Array<object>} objects scene-object records
|
|
189
|
+
* @returns {Array<{a: string, b: string, planes: Array<{face: string, axis: string|null, at: number}>}>}
|
|
190
|
+
*/
|
|
191
|
+
export function coplanarConflicts(objects) {
|
|
192
|
+
const pairs = [];
|
|
193
|
+
if (!Array.isArray(objects) || objects.length < 2) return pairs;
|
|
194
|
+
const shapes = objects.map((object) => (object && typeof object === "object" ? facesOf(object) : null));
|
|
195
|
+
for (let j = 1; j < objects.length; j += 1) {
|
|
196
|
+
const b = shapes[j];
|
|
197
|
+
if (!b) continue;
|
|
198
|
+
for (let i = 0; i < j; i += 1) {
|
|
199
|
+
const a = shapes[i];
|
|
200
|
+
if (!a) continue;
|
|
201
|
+
if (
|
|
202
|
+
a.min[0] > b.max[0] + PLANE_EPS || b.min[0] > a.max[0] + PLANE_EPS ||
|
|
203
|
+
a.min[1] > b.max[1] + PLANE_EPS || b.min[1] > a.max[1] + PLANE_EPS ||
|
|
204
|
+
a.min[2] > b.max[2] + PLANE_EPS || b.min[2] > a.max[2] + PLANE_EPS
|
|
205
|
+
) continue;
|
|
206
|
+
const planes = conflictingFaces(a, b).map((face) => {
|
|
207
|
+
const axis = [0, 1, 2].find((k) => Math.abs(face.n[k]) > NORMAL_DOT);
|
|
208
|
+
if (axis === undefined) return { face: "tilted faces", axis: null, at: round(dot(face.n, face.c)) };
|
|
209
|
+
const sign = face.n[axis] > 0 ? "+" : "-";
|
|
210
|
+
return { face: FACE_NAMES[`${AXES[axis]}${sign}`] ?? `${sign}${AXES[axis]} faces`, axis: AXES[axis], at: round(face.c[axis]) };
|
|
211
|
+
});
|
|
212
|
+
if (planes.length) pairs.push({ a: objects[i].id, b: objects[j].id, planes });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return pairs;
|
|
216
|
+
}
|
package/src/cuts.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
updateStableItem,
|
|
12
12
|
} from "./stable-items.js";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const DEFAULT_SHOT_FRAMES = 40;
|
|
15
15
|
|
|
16
16
|
const frameNumber = (value, fallback = 0) => Number.isFinite(value) ? Math.round(value) : fallback;
|
|
17
17
|
|
|
@@ -48,6 +48,12 @@ export function createShot(name = "Shot", startFrame = 0, endFrame = startFrame
|
|
|
48
48
|
endFrame: end,
|
|
49
49
|
cameraKeys: uniqueKeys(cameraKeys, start, end),
|
|
50
50
|
camera: createCameraBlock(camera),
|
|
51
|
+
// v5 shot-document defaults, so a fresh shot already has the shape the
|
|
52
|
+
// document normalizer gives it (and an undo restores).
|
|
53
|
+
kind: "clip",
|
|
54
|
+
caption: "",
|
|
55
|
+
cast: {},
|
|
56
|
+
stylizedAssetId: null,
|
|
51
57
|
};
|
|
52
58
|
}
|
|
53
59
|
|
|
@@ -133,11 +139,6 @@ export function resizeShot(shots, shotId, edge, rawFrame, frameCount) {
|
|
|
133
139
|
.sort((a, b) => a.startFrame - b.startFrame);
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
/** Compatibility name for callers that move the left edge. */
|
|
137
|
-
export function moveBoundary(shots, shotId, newStartFrame, frameCount) {
|
|
138
|
-
return resizeShot(shots, shotId, "start", newStartFrame, frameCount);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
142
|
export function removeShot(shots, shotId) {
|
|
142
143
|
return removeStableItem(shots, shotId, "shots");
|
|
143
144
|
}
|
package/src/domains/cast.js
CHANGED
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
captureHipsOffset,
|
|
13
13
|
saveCustomPoses,
|
|
14
14
|
} from "../poses.js";
|
|
15
|
-
import { createCharacterEntry, createCharacterLayer } from "../scenes.js";
|
|
15
|
+
import { createCharacterEntry, createCharacterLayer, isProxyFigure, PROXY_FIGURE_MODEL } from "../scenes.js";
|
|
16
|
+
import { kindRefusal } from "../character-kind.js";
|
|
16
17
|
import {
|
|
17
18
|
DEFAULT_SUBJECT,
|
|
18
19
|
DEFAULT_SUBJECT2,
|
|
@@ -28,12 +29,12 @@ import { judgeNextWaypoint } from "../ardy/waypoints.js";
|
|
|
28
29
|
import { StudioProtocolError } from "../studio-agent-protocol.js";
|
|
29
30
|
import { studioActionRefusal } from "../studio-actions.js";
|
|
30
31
|
import { createStableItemId, removeStableItem } from "../stable-items.js";
|
|
31
|
-
import { trackFeature } from "../analytics.js";
|
|
32
|
+
import { track, trackFeature } from "../analytics.js";
|
|
32
33
|
import { requestBridgeExtract } from "../multimodel-ingest.js";
|
|
33
34
|
import { loadMotionFromUrl } from "../ardy/npz.js";
|
|
34
35
|
import { snapshotPlaybackBones, applyMotionFrame, restorePlaybackBones } from "../ardy/playback.js";
|
|
35
36
|
|
|
36
|
-
import {
|
|
37
|
+
import { shotAtFrame } from "../cuts.js";
|
|
37
38
|
|
|
38
39
|
const sameCastValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
39
40
|
|
|
@@ -43,7 +44,7 @@ export function createCastDomain(appContext, initial, customPoses = []) {
|
|
|
43
44
|
let native = createDocumentStore({ owned: { cast: normalize(initial) } });
|
|
44
45
|
const listeners = new Set(), motions = new Map();
|
|
45
46
|
const notify = () => { for (const listener of listeners) listener(); };
|
|
46
|
-
let release = native.subscribe(notify)
|
|
47
|
+
let release = native.subscribe(notify);
|
|
47
48
|
const documentStore = { ...Object.fromEntries(Object.keys(native).map(key => [key, (...args) => native[key](...args)])),
|
|
48
49
|
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); } };
|
|
49
50
|
const state = () => documentStore.read('cast'), read = () => state().characters;
|
|
@@ -74,6 +75,25 @@ export function createCastDomain(appContext, initial, customPoses = []) {
|
|
|
74
75
|
function write(update) {
|
|
75
76
|
return writeState(before => ({ ...before, characters: typeof update === 'function' ? update(before.characters) : update }));
|
|
76
77
|
}
|
|
78
|
+
function update(characterId, patch) {
|
|
79
|
+
const entry = read().find(entry => entry.id === characterId);
|
|
80
|
+
if (!entry) throw new StudioProtocolError('STALE_TARGET', `Character ${characterId} is not in this scene.`);
|
|
81
|
+
const raw = appContext.ports.read(), shot = shotAtFrame(raw.shots, raw.view.frame);
|
|
82
|
+
const fields = ['x', 'z', 'rot', 'posture', 'pose'];
|
|
83
|
+
const panelPatch = Object.fromEntries(Object.entries(patch).filter(([key, value]) => fields.includes(key) && value !== undefined));
|
|
84
|
+
if (shot?.kind === 'still' && !raw.targets?.get?.(characterId)?.motion && Object.keys(panelPatch).length) {
|
|
85
|
+
const current = { ...entry, ...shot.cast[characterId] };
|
|
86
|
+
const normalized = createCharacterEntry({ ...current, ...panelPatch });
|
|
87
|
+
const override = { x: normalized.x, z: normalized.z, rot: normalized.rot };
|
|
88
|
+
for (const key of ['posture', 'pose']) if (normalized[key] != null) override[key] = normalized[key];
|
|
89
|
+
// Enlist the shot owner in the already-open command/drag session:
|
|
90
|
+
// even a mixed scale + placement edit is one shared Undo entry.
|
|
91
|
+
const shots = appContext.storeDomain('shot');
|
|
92
|
+
appContext.recordAction('shot', () => shots.setCastOverride(shot.id, characterId, override), null, true);
|
|
93
|
+
patch = Object.fromEntries(Object.entries(patch).filter(([key]) => !fields.includes(key)));
|
|
94
|
+
}
|
|
95
|
+
if (Object.keys(patch).length) write(rows => rows.map(row => row.id === characterId ? { ...row, ...patch } : row));
|
|
96
|
+
}
|
|
77
97
|
function publishMotion(characterId, motion) {
|
|
78
98
|
if (motion) motions.set(characterId, motion); else motions.delete(characterId);
|
|
79
99
|
publish(); notify();
|
|
@@ -109,7 +129,7 @@ export function createCastDomain(appContext, initial, customPoses = []) {
|
|
|
109
129
|
}
|
|
110
130
|
return run('run.update', { txId: gesture.txId, args });
|
|
111
131
|
}
|
|
112
|
-
const domain = { documentStore, state, read, write, writeState, run, edit, beginGesture, finishGesture,
|
|
132
|
+
const domain = { documentStore, state, read, write, update, writeState, run, edit, beginGesture, finishGesture,
|
|
113
133
|
activeId: read()[0]?.id ?? null, projection: () => projection, publishMotion, normalizeCharacters: rows => rows.map(createCharacterEntry),
|
|
114
134
|
bindRender(context) { appContext = context; },
|
|
115
135
|
beginAction: () => documentStore.beginAction('cast'), canUndo: id => documentStore.canUndo(id),
|
|
@@ -121,13 +141,11 @@ export function createCastDomain(appContext, initial, customPoses = []) {
|
|
|
121
141
|
write(plan.draft); return plan;
|
|
122
142
|
},
|
|
123
143
|
load(value) {
|
|
124
|
-
finishGesture(true);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
domain.activeId = read()[0]?.id ?? null; domain.loadView?.(); notify();
|
|
130
|
-
} finally { loading = false; }
|
|
144
|
+
finishGesture(true);
|
|
145
|
+
release(); native.dispose(); motions.clear();
|
|
146
|
+
const next = normalize(Array.isArray(value) ? { characters: value, customPoses: state().customPoses } : value);
|
|
147
|
+
native = createDocumentStore({ owned: { cast: next } }); release = native.subscribe(notify);
|
|
148
|
+
domain.activeId = read()[0]?.id ?? null; domain.loadView?.(); notify();
|
|
131
149
|
},
|
|
132
150
|
dispose() { finishGesture(true); unregister(); unsubscribe(); release(); native.dispose(); listeners.clear(); },
|
|
133
151
|
};
|
|
@@ -164,7 +182,6 @@ export function useCast(appContext) {
|
|
|
164
182
|
|
|
165
183
|
const [poseRevision, setPoseTick] = useState(0);
|
|
166
184
|
|
|
167
|
-
/* --------------------- derived cast view + shims ---------------------- */
|
|
168
185
|
// Fallback second slot mirrors the old charB defaults so preset math and
|
|
169
186
|
// the two-subject inspector never see a hole before B exists.
|
|
170
187
|
const charA = characters[0] ?? createCharacterEntry(null, 0);
|
|
@@ -181,29 +198,12 @@ export function useCast(appContext) {
|
|
|
181
198
|
|
|
182
199
|
const subject2 = charB.subject ?? DEFAULT_SUBJECT2;
|
|
183
200
|
|
|
184
|
-
const rigA = rigs[charA.id] ?? null;
|
|
185
|
-
|
|
186
|
-
const rigB = (characters[1] ? rigs[charB.id] : null) ?? null;
|
|
187
|
-
|
|
188
201
|
function updateCharacterAt(index, next) {
|
|
189
|
-
const
|
|
202
|
+
const base = domain.read()[index], raw = appContext.ports.read();
|
|
203
|
+
const entry = base && { ...base, ...shotAtFrame(raw.shots, raw.view.frame)?.cast[base.id] };
|
|
190
204
|
if (entry) return domain.edit('character.update', { characterId: entry.id, patch: typeof next === 'function' ? next(entry) : next });
|
|
191
205
|
}
|
|
192
206
|
|
|
193
|
-
// The shims keep the legacy call sites (inspector sliders, presets, pose
|
|
194
|
-
// studio, prompts) untouched while the list stays the source of truth.
|
|
195
|
-
const setCharA = (next) => updateCharacterAt(0, next);
|
|
196
|
-
|
|
197
|
-
const setCharB = (next) => updateCharacterAt(1, next);
|
|
198
|
-
|
|
199
|
-
const setPoseA = (pose) => updateCharacterAt(0, (entry) => ({ pose: typeof pose === "function" ? pose(entry.pose ?? DEFAULT_POSE) : pose }));
|
|
200
|
-
|
|
201
|
-
const setPoseB = (pose) => updateCharacterAt(1, (entry) => ({ pose: typeof pose === "function" ? pose(entry.pose ?? DEFAULT_POSE) : pose }));
|
|
202
|
-
|
|
203
|
-
const setSubject = (value) => updateCharacterAt(0, (entry) => ({ subject: typeof value === "function" ? value(entry.subject) : value }));
|
|
204
|
-
|
|
205
|
-
const setSubject2 = (value) => updateCharacterAt(1, (entry) => ({ subject: typeof value === "function" ? value(entry.subject) : value }));
|
|
206
|
-
|
|
207
207
|
function setShowB(next) {
|
|
208
208
|
return domain.run('cast.showExtras', { show: typeof next === 'function' ? next(domain.read().some((entry, i) => i > 0 && !entry.hidden)) : next });
|
|
209
209
|
}
|
|
@@ -221,7 +221,8 @@ export function useCast(appContext) {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
function moveCharacter(charId, next) {
|
|
224
|
-
const
|
|
224
|
+
const base = domain.read().find(entry => entry.id === charId), raw = appContext.ports.read();
|
|
225
|
+
const entry = { ...base, ...shotAtFrame(raw.shots, raw.view.frame)?.cast[charId] };
|
|
225
226
|
return domain.edit('character.update', { characterId: charId, patch: typeof next === 'function' ? next(entry) : next });
|
|
226
227
|
}
|
|
227
228
|
|
|
@@ -258,6 +259,7 @@ export function useCast(appContext) {
|
|
|
258
259
|
const spawnCharacter = (model, x, z) => {
|
|
259
260
|
const id = nextCharacterId(domain.read());
|
|
260
261
|
domain.run('character.add', { character: { id, model, x, z, pose: DEFAULT_POSE, subject: 'a person' } });
|
|
262
|
+
if (model === PROXY_FIGURE_MODEL) track("cast:proxy_added", { surface: "assets" });
|
|
261
263
|
appContext.shared.setSelectedHierarchyId(`character:${id}`);
|
|
262
264
|
appContext.notify(ko("Character added to the scene", "인물을 씬에 추가했어요"));
|
|
263
265
|
};
|
|
@@ -270,7 +272,6 @@ export function useCast(appContext) {
|
|
|
270
272
|
return `character:${key.startsWith("char:") ? key.slice(5) : key}`;
|
|
271
273
|
};
|
|
272
274
|
|
|
273
|
-
/* ------------------- active character (motion layer) ------------------- */
|
|
274
275
|
// Every character owns an animation layer (root path, prompt blocks,
|
|
275
276
|
// generated clip, IK keys). The studio's motion machinery edits ONE layer
|
|
276
277
|
// at a time — the ACTIVE character's — and selection decides who that is.
|
|
@@ -332,9 +333,6 @@ export function useCast(appContext) {
|
|
|
332
333
|
waypointFrames: (entry.layer?.waypoints ?? []).map((waypoint) => waypoint.frame),
|
|
333
334
|
}]), [characters, activeChar.id]);
|
|
334
335
|
|
|
335
|
-
// Undo/redo (plan §6.5). The store settles any open drag first, so a
|
|
336
|
-
// mid-drag press commits that drag as one entry and then steps past it.
|
|
337
|
-
// After a step the selection can point at a deleted object — drop it to
|
|
338
336
|
/* Cast authoring uses the registered store. The native compatibility
|
|
339
337
|
* snapshot remains for motion/IK until its owner migrates; it must not
|
|
340
338
|
* restore the owned cast over newer character edits. */
|
|
@@ -424,10 +422,9 @@ export function useCast(appContext) {
|
|
|
424
422
|
const posedRig = () => rigs[posing] ?? null;
|
|
425
423
|
|
|
426
424
|
const setPosed = (pose) => {
|
|
427
|
-
if (posingIndex >= 0) updateCharacterAt(posingIndex, { pose: typeof pose === "function" ? pose(
|
|
425
|
+
if (posingIndex >= 0) updateCharacterAt(posingIndex, entry => ({ pose: typeof pose === "function" ? pose(entry.pose ?? DEFAULT_POSE) : pose }));
|
|
428
426
|
};
|
|
429
427
|
|
|
430
|
-
/* ------------------------- waypoint workspace --------------------------- */
|
|
431
428
|
// A walking pace turns clicked distance into clip time, so pins land at
|
|
432
429
|
// frames the character can actually reach without ice-skating.
|
|
433
430
|
const WALK_SPEED_MPS = 1.4;
|
|
@@ -681,6 +678,23 @@ export function useCast(appContext) {
|
|
|
681
678
|
: ko(`Saved the current pose to the library as “${pose.label}”`, `지금 자세를 “${pose.label}”로 라이브러리에 저장했어요`));
|
|
682
679
|
}
|
|
683
680
|
|
|
681
|
+
/** A handle drag is an authored pose. When it ends, the rig as it now
|
|
682
|
+
* stands becomes the posed character's own pose — so the scene autosaves
|
|
683
|
+
* it and undo can step it back — instead of living only on the rig until
|
|
684
|
+
* a reload re-applies the stored pose over it. The library is untouched;
|
|
685
|
+
* Save pose still bottles a pose there. */
|
|
686
|
+
function commitPosedPose() {
|
|
687
|
+
const rig = posedRig();
|
|
688
|
+
if (!rig || !posingChar) return;
|
|
689
|
+
domain.run('character.setPose', { characterId: posingChar.id, clearMotion: false, pose: {
|
|
690
|
+
id: `edited:${posingChar.id}`,
|
|
691
|
+
label: ko("Edited pose", "편집한 포즈"),
|
|
692
|
+
prompt: "in the exact body pose shown in the blocking frame",
|
|
693
|
+
bones: capturePose(rig),
|
|
694
|
+
rootY: captureHipsOffset(rig),
|
|
695
|
+
} });
|
|
696
|
+
}
|
|
697
|
+
|
|
684
698
|
function savePose() {
|
|
685
699
|
const rig = posedRig();
|
|
686
700
|
if (!rig) return;
|
|
@@ -720,6 +734,8 @@ export function useCast(appContext) {
|
|
|
720
734
|
setPhotoPoseState("running");
|
|
721
735
|
setPhotoPoseError("");
|
|
722
736
|
try {
|
|
737
|
+
// A capsule figure has no bones for a photo to pose.
|
|
738
|
+
if (isProxyFigure(posingChar ?? activeChar)) throw new StudioProtocolError("TARGET_NOT_READY", kindRefusal("photoPose", isKo));
|
|
723
739
|
if (!rig) throw new Error("rig-not-loaded");
|
|
724
740
|
objectUrl = URL.createObjectURL(file);
|
|
725
741
|
let bones = null;
|
|
@@ -907,7 +923,9 @@ export function useCast(appContext) {
|
|
|
907
923
|
const live = appContext.live.state;
|
|
908
924
|
const patch = finitePatch(args, ["x", "z", "rot"]);
|
|
909
925
|
const id = nextCharacterId(live.characters);
|
|
910
|
-
|
|
926
|
+
const entry = createCharacterEntry({ id, model: args.model, subject: args.subject, pose: DEFAULT_POSE, ...patch }, live.characters.length);
|
|
927
|
+
replaceCharacters([...live.characters, entry]);
|
|
928
|
+
if (isProxyFigure(entry)) track("cast:proxy_added", { surface: "mcp" });
|
|
911
929
|
return { id };
|
|
912
930
|
}
|
|
913
931
|
function updateLiveCharacter(args) {
|
|
@@ -947,7 +965,6 @@ export function useCast(appContext) {
|
|
|
947
965
|
if (typeof block.text !== "string" || !block.text.trim()) throw new Error(`Block ${i + 1} needs text`);
|
|
948
966
|
return { id: `prompt-${stamp}-${i}`, startFrame, endFrame, text: block.text.trim() };
|
|
949
967
|
});
|
|
950
|
-
const live = appContext.live.state;
|
|
951
968
|
domain.run('character.setPromptBlocks', { characterId: domain.activeId, blocks: clips });
|
|
952
969
|
return { blocks: clips.length };
|
|
953
970
|
}
|
|
@@ -982,14 +999,14 @@ export function useCast(appContext) {
|
|
|
982
999
|
const motion = appContext.storeDomain('motion');
|
|
983
1000
|
if (motion) motion.clear(characterId); else appContext.shared.motionDomain.clearMotionNative();
|
|
984
1001
|
}, characterId, true);
|
|
985
|
-
domain.
|
|
1002
|
+
domain.update(characterId, { pose });
|
|
986
1003
|
};
|
|
987
1004
|
appContext.updateActionPorts({ addCharacterWaypoint, moveCharacterWaypoint, removeCharacterWaypoint, clearCharacterWaypoints, setWaypointMode });
|
|
988
1005
|
return {
|
|
989
1006
|
...domain,
|
|
990
1007
|
applyExternalCharacters, publishStudioCharacters, syncStudioLayerBuffer, undoScene, redoScene, snapshotStudioDomain, removeLegacyRootWaypoint, switchActiveCharacterLayer, createLegacyCastHandlers, toggleCharacterHidden,
|
|
991
1008
|
characters, editCharacters, customPoses, posing, setPosing, posingClosing,
|
|
992
|
-
studioPick, setStudioPick, rigs, rigMountEpoch, setRigMountEpoch, setPoseTick, charA, charB, showB,
|
|
1009
|
+
studioPick, setStudioPick, commitPosedPose, rigs, rigMountEpoch, setRigMountEpoch, setPoseTick, charA, charB, showB,
|
|
993
1010
|
poseA, poseB, subject, subject2, updateCharacterAt, setShowB, moveCharacter, removeCharacter, reportRig,
|
|
994
1011
|
spawnCharacter, charKeyToHierarchyId, charIdFromHierarchyId, activeCharacterId, setActiveCharacterId,
|
|
995
1012
|
rowIdForCharIndex, activeChar, selectActiveCharacterInHierarchy, activeCharIndex, activeRig, waitForRig,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
const sameMotionIntent = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
4
|
+
const emptyMotionLayer = id => ({ id, take: null, fullTake: null, ikKeys: [], ikPins: [], ikPinResiduals: [], committedIkEdits: [], takeRecipe: null, takeVersions: [] });
|
|
5
|
+
const keyVector = p => ({ x: p.x, y: p.y, z: p.z });
|
|
6
|
+
const keyQuaternion = q => ({ ...keyVector(q), w: q.w });
|
|
7
|
+
function encodeMotionKeys(keys) {
|
|
8
|
+
return [...keys].sort(([a], [b]) => a - b).map(([frame, tracks]) => ({ frame, tracks: Object.fromEntries([...tracks].map(([track, key]) => [track, {
|
|
9
|
+
...(key.q ? { q: key.q.map(keyQuaternion) } : {}), ...(key.p ? { p: keyVector(key.p) } : {}),
|
|
10
|
+
...(key.baseQ ? { baseQ: key.baseQ.map(keyQuaternion) } : {}), ...(key.basePos ? { basePos: keyVector(key.basePos) } : {}),
|
|
11
|
+
...(key.chainP ? { chainP: key.chainP.map(keyVector) } : {}), ...(key.keepTranslations ? { keepTranslations: true } : {}),
|
|
12
|
+
...(key.correctionRange ? { correctionRange: { start: key.correctionRange.start, end: key.correctionRange.end } } : {}),
|
|
13
|
+
...(key.blend != null ? { blend: key.blend } : {}),
|
|
14
|
+
...(key.pin ? { pin: key.pin } : {}),
|
|
15
|
+
}])) }));
|
|
16
|
+
}
|
|
17
|
+
function decodeMotionKeys(rows) {
|
|
18
|
+
const q = value => new THREE.Quaternion(value.x, value.y, value.z, value.w).normalize();
|
|
19
|
+
const p = value => new THREE.Vector3(value.x, value.y, value.z);
|
|
20
|
+
return new Map(rows.map(row => [row.frame, new Map(Object.entries(row.tracks).map(([track, key]) => [track, {
|
|
21
|
+
q: key.q?.map(q) ?? null, p: key.p ? p(key.p) : null,
|
|
22
|
+
...(key.baseQ ? { baseQ: key.baseQ.map(q) } : {}), ...(key.basePos ? { basePos: p(key.basePos) } : {}),
|
|
23
|
+
...(key.chainP ? { chainP: key.chainP.map(p) } : {}), ...(key.keepTranslations ? { keepTranslations: true } : {}),
|
|
24
|
+
...(key.correctionRange ? { correctionRange: { start: key.correctionRange.start, end: key.correctionRange.end } } : {}),
|
|
25
|
+
...(key.blend != null ? { blend: key.blend } : {}),
|
|
26
|
+
...(key.pin ? { pin: key.pin } : {}),
|
|
27
|
+
}]))]));
|
|
28
|
+
}
|
|
29
|
+
function cloneRangePin(pin) {
|
|
30
|
+
return {
|
|
31
|
+
...pin,
|
|
32
|
+
target: pin.target.space === "world"
|
|
33
|
+
? { space: "world", position: [...pin.target.position] }
|
|
34
|
+
: { space: "object", objectId: pin.target.objectId, local: [...pin.target.local] },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function encodeRangePins(pins) {
|
|
38
|
+
return [...(pins instanceof Map ? pins.values() : pins ?? [])].map(cloneRangePin);
|
|
39
|
+
}
|
|
40
|
+
function decodeRangePins(rows) {
|
|
41
|
+
return new Map((rows ?? []).map((pin) => [pin.id, cloneRangePin(pin)]));
|
|
42
|
+
}
|
|
43
|
+
function encodeRangePinResiduals(values) {
|
|
44
|
+
return [...(values instanceof Map ? values.entries() : values ?? [])].map(([id, entries]) => [id, (entries ?? []).map((entry) => ({ ...entry }))]);
|
|
45
|
+
}
|
|
46
|
+
function decodeRangePinResiduals(rows) {
|
|
47
|
+
return new Map((rows ?? []).map(([id, entries]) => [id, (entries ?? []).map((entry) => ({ ...entry }))]));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { sameMotionIntent, emptyMotionLayer, encodeMotionKeys, decodeMotionKeys, cloneRangePin, encodeRangePins, decodeRangePins, encodeRangePinResiduals, decodeRangePinResiduals };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { rangePinTracks } from "../../ardy/range-pin.js";
|
|
2
|
+
import { ko } from "../../locale.js";
|
|
3
|
+
|
|
4
|
+
export function ensureRangePinState(state) {
|
|
5
|
+
if (!state) return state;
|
|
6
|
+
if (!(state.pins instanceof Map)) state.pins = new Map();
|
|
7
|
+
if (!(state.pinResiduals instanceof Map)) state.pinResiduals = new Map();
|
|
8
|
+
return state;
|
|
9
|
+
}
|
|
10
|
+
export function rangePinConflictFrames(draft, state) {
|
|
11
|
+
if (!Number.isInteger(draft?.startFrame) || !Number.isInteger(draft?.endFrame) || draft.endFrame < draft.startFrame) return [];
|
|
12
|
+
const tracks = rangePinTracks(draft), conflicts = [];
|
|
13
|
+
for (let frame = draft.startFrame; frame <= draft.endFrame; frame += 1) {
|
|
14
|
+
if (tracks.some((track) => { const key = state.keys.get(frame)?.get(track); return key && key.pin !== draft.id; })) conflicts.push(frame);
|
|
15
|
+
}
|
|
16
|
+
return conflicts;
|
|
17
|
+
}
|
|
18
|
+
export function overlappingRangePin(draft, state) {
|
|
19
|
+
return [...ensureRangePinState(state).pins.values()].find((pin) => pin.id !== draft.id && rangePinTracks(pin).some((track) => rangePinTracks(draft).includes(track)) && Math.max(pin.startFrame, draft.startFrame) <= Math.min(pin.endFrame, draft.endFrame));
|
|
20
|
+
}
|
|
21
|
+
export function rangePinOverlapMessage(pin) {
|
|
22
|
+
return pin ? ko(`A required body or limb track already has an overlapping pin (${pin.startFrame}–${pin.endFrame}).`, `필요한 몸통 또는 팔다리 파츠에 겹치는 고정이 있어요 (${pin.startFrame}–${pin.endFrame}).`) : "";
|
|
23
|
+
}
|