cozyclay 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +211 -36
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +4 -4
- package/bin/live/cli.mjs +2 -3
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +2 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-B41FmPYI.js +1 -0
- package/dist/assets/app-Do-Og5Pu.css +1 -0
- package/dist/assets/app-DwP-tl2x.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
- package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
- package/dist/assets/workflow-Br-K7Srs.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +789 -263
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +184 -6
- package/src/ardy/timeline.jsx +113 -28
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +0 -3
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +6 -7
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +15 -2
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +79 -28
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +49 -4
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +58 -9
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +58 -14
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/TopBar.jsx +17 -3
- package/src/shell/ViewportToolbar.jsx +18 -3
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +186 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +56 -23
- package/src/studio-app-binding.js +52 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +92 -9
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +106 -18
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +94 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-BxBLC6GT.js +0 -4876
- package/dist/assets/app-CJdxvnrl.css +0 -1
- package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
- package/dist/assets/workflow-B5XZwVfe.js +0 -179
package/src/object-path.js
CHANGED
|
@@ -13,14 +13,61 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { createTiming, timingIsFlat, timingProgress } from "./speed-envelope.js";
|
|
16
|
+
import { monotoneCubicAt } from "./monotone-cubic.js";
|
|
16
17
|
|
|
17
18
|
const MAX_PATH_POINTS = 64;
|
|
19
|
+
// The camera rail's crane marks, mirrored: the two ends are always marks, up to
|
|
20
|
+
// MAX_PATH_MARKS in all, never closer than MARK_GAP of the route's length.
|
|
21
|
+
const MAX_PATH_MARKS = 8;
|
|
22
|
+
const MARK_GAP = 0.03;
|
|
18
23
|
const ROOM_LIMIT = 240;
|
|
19
24
|
const MAX_HEIGHT = 60;
|
|
25
|
+
// A mark's lean (bank / pitch) is a body tilt, not a flip: ±90° is a vehicle on
|
|
26
|
+
// its side, past that it is a different (and unreachable-by-slider) gesture.
|
|
27
|
+
const MAX_LEAN = 90;
|
|
20
28
|
|
|
21
29
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
22
30
|
const finite = (value, fallback = 0) => (typeof value === "number" && Number.isFinite(value) ? value : fallback);
|
|
23
31
|
|
|
32
|
+
const tidyDegrees = (value) => {
|
|
33
|
+
const rounded = Math.round(value * 1e4) / 1e4;
|
|
34
|
+
return rounded === 0 ? 0 : rounded;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** One stored mark: a bare number is a level dot (bank 0, pitch 0). */
|
|
38
|
+
function readMark(entry) {
|
|
39
|
+
if (entry && typeof entry === "object") {
|
|
40
|
+
return {
|
|
41
|
+
t: finite(entry.t, -1),
|
|
42
|
+
bank: tidyDegrees(clamp(finite(entry.bank), -MAX_LEAN, MAX_LEAN)),
|
|
43
|
+
pitch: tidyDegrees(clamp(finite(entry.pitch), -MAX_LEAN, MAX_LEAN)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { t: finite(entry, -1), bank: 0, pitch: 0 };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The interior marks a hand has dropped on the route: `{ t, bank, pitch }`,
|
|
51
|
+
* `t` the arc fraction, `bank` the roll about the travel direction (degrees,
|
|
52
|
+
* positive = right side down) and `pitch` the nose angle (positive = nose up)
|
|
53
|
+
* the body holds when it passes the dot. A plain number is accepted as a level
|
|
54
|
+
* dot. The ends are implicit marks 0 and 1, always level, and never stored.
|
|
55
|
+
*/
|
|
56
|
+
function normalizeMarks(value) {
|
|
57
|
+
const marks = [];
|
|
58
|
+
const sorted = (Array.isArray(value) ? value : [])
|
|
59
|
+
.map(readMark)
|
|
60
|
+
.filter((mark) => mark.t > MARK_GAP && mark.t < 1 - MARK_GAP)
|
|
61
|
+
.sort((a, b) => a.t - b.t);
|
|
62
|
+
for (const mark of sorted) {
|
|
63
|
+
const previous = marks.length ? marks[marks.length - 1].t : 0;
|
|
64
|
+
if (mark.t - previous < MARK_GAP || 1 - mark.t < MARK_GAP) continue;
|
|
65
|
+
if (marks.length >= MAX_PATH_MARKS - 2) break;
|
|
66
|
+
marks.push(mark);
|
|
67
|
+
}
|
|
68
|
+
return marks;
|
|
69
|
+
}
|
|
70
|
+
|
|
24
71
|
/** One authored path point: a floor position plus the height it travels at. */
|
|
25
72
|
function createPathPoint(value) {
|
|
26
73
|
const source = value && typeof value === "object" ? value : {};
|
|
@@ -55,6 +102,9 @@ export function createObjectPath(value) {
|
|
|
55
102
|
const timing = createTiming(value.timing);
|
|
56
103
|
return {
|
|
57
104
|
points,
|
|
105
|
+
// The grabbable dots on the route (see pathMarkFractions); the points
|
|
106
|
+
// are the route's shape, the marks are where a hand takes hold of it.
|
|
107
|
+
marks: normalizeMarks(value.marks),
|
|
58
108
|
timing: timingIsFlat(timing) ? null : timing,
|
|
59
109
|
// 0 means "fill the timeline": the length/duration answer is computed at
|
|
60
110
|
// sample time, where the take's duration is known.
|
|
@@ -86,52 +136,223 @@ export function translateObjectPath(path, delta) {
|
|
|
86
136
|
});
|
|
87
137
|
}
|
|
88
138
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
139
|
+
/*
|
|
140
|
+
* The route as travelled is a curve, not the polyline of its points: a
|
|
141
|
+
* centripetal Catmull-Rom through every authored point (the camera rail's
|
|
142
|
+
* curve, in 3D so lifted points stay lifted), sampled every few centimetres.
|
|
143
|
+
* A car rounds a corner instead of snapping to the next leg, and its heading
|
|
144
|
+
* turns with the curve instead of jumping at each point. The ends are
|
|
145
|
+
* extended by mirroring their neighbour, so a two-point route is still a
|
|
146
|
+
* straight line and the curve still starts and ends on the authored points.
|
|
147
|
+
*/
|
|
148
|
+
const CURVE_SPACING = 0.05;
|
|
149
|
+
const CURVE_MAX_STEPS = 400;
|
|
150
|
+
const CURVE_ALPHA = 0.5; // centripetal: no loops or overshoot at tight corners
|
|
151
|
+
const curveCache = new Map();
|
|
152
|
+
const CURVE_CACHE_SIZE = 64;
|
|
153
|
+
|
|
154
|
+
const mirror = (point, neighbour) => ({ x: 2 * point.x - neighbour.x, y: 2 * point.y - neighbour.y, z: 2 * point.z - neighbour.z });
|
|
155
|
+
const span = (a, b) => Math.hypot(b.x - a.x, b.y - a.y, b.z - a.z);
|
|
156
|
+
|
|
157
|
+
function catmullRom(p0, p1, p2, p3, t) {
|
|
158
|
+
const t0 = 0;
|
|
159
|
+
const t1 = t0 + Math.max(span(p0, p1) ** CURVE_ALPHA, 1e-6);
|
|
160
|
+
const t2 = t1 + Math.max(span(p1, p2) ** CURVE_ALPHA, 1e-6);
|
|
161
|
+
const t3 = t2 + Math.max(span(p2, p3) ** CURVE_ALPHA, 1e-6);
|
|
162
|
+
const at = t1 + (t2 - t1) * t;
|
|
163
|
+
const lerp = (a, b, ta, tb) => {
|
|
164
|
+
const w = tb - ta < 1e-12 ? 0 : (at - ta) / (tb - ta);
|
|
165
|
+
return { x: a.x + (b.x - a.x) * w, y: a.y + (b.y - a.y) * w, z: a.z + (b.z - a.z) * w };
|
|
166
|
+
};
|
|
167
|
+
const a1 = lerp(p0, p1, t0, t1);
|
|
168
|
+
const a2 = lerp(p1, p2, t1, t2);
|
|
169
|
+
const a3 = lerp(p2, p3, t2, t3);
|
|
170
|
+
return lerp(lerp(a1, a2, t0, t2), lerp(a2, a3, t1, t3), t1, t2);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function segmentControls(points, index) {
|
|
174
|
+
const p1 = points[index];
|
|
175
|
+
const p2 = points[index + 1];
|
|
176
|
+
const p0 = index > 0 ? points[index - 1] : mirror(p1, p2);
|
|
177
|
+
const p3 = index + 2 < points.length ? points[index + 2] : mirror(p2, p1);
|
|
178
|
+
return [p0, p1, p2, p3];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const asPoint = (point) => ({ x: finite(point?.x), y: finite(point?.y), z: finite(point?.z) });
|
|
182
|
+
|
|
183
|
+
/** The point a fraction `t` along the curve between authored points
|
|
184
|
+
* `index` and `index + 1` — where a point inserted on the drawn line goes. */
|
|
185
|
+
export function pathCurvePointBetween(points, index, t) {
|
|
186
|
+
const list = (points ?? []).map(asPoint);
|
|
187
|
+
if (index < 0 || index + 1 >= list.length) return null;
|
|
188
|
+
const at = catmullRom(...segmentControls(list, index), clamp(finite(t), 0, 1));
|
|
189
|
+
return { x: at.x, y: Math.max(0, at.y), z: at.z };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The interior marks, normalized: `{ t, bank, pitch }` ascending by `t`. */
|
|
193
|
+
export function pathMarks(path) {
|
|
194
|
+
return normalizeMarks(path?.marks);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Every mark as an arc fraction, ends included: the dots the editors draw. */
|
|
198
|
+
export function pathMarkFractions(path) {
|
|
199
|
+
return [0, ...normalizeMarks(path?.marks).map((mark) => mark.t), 1];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The marks with one more dot at arc fraction `t`, and the editor index it
|
|
204
|
+
* takes (ends count: the first interior dot is 1). The new dot is born with the
|
|
205
|
+
* lean the route already has there, so dropping it changes nothing visible; its
|
|
206
|
+
* neighbours keep theirs. Null when the route is full or `t` is too close to a
|
|
207
|
+
* dot or an end.
|
|
208
|
+
*/
|
|
209
|
+
export function insertPathMark(path, t) {
|
|
210
|
+
const marks = normalizeMarks(path?.marks);
|
|
211
|
+
const at = finite(t, -1);
|
|
212
|
+
if (marks.length >= MAX_PATH_MARKS - 2) return null;
|
|
213
|
+
if (!(at > MARK_GAP && at < 1 - MARK_GAP)) return null;
|
|
214
|
+
if (marks.some((mark) => Math.abs(mark.t - at) < MARK_GAP)) return null;
|
|
215
|
+
const lean = pathLeanAt(path, at);
|
|
216
|
+
const next = [...marks, { t: at, bank: tidyDegrees(lean.bank), pitch: tidyDegrees(lean.pitch) }].sort((a, b) => a.t - b.t);
|
|
217
|
+
return { marks: next, index: next.findIndex((mark) => mark.t === at) + 1 };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The body's tilt at arc fraction `t`: a monotone piecewise cubic through the
|
|
222
|
+
* marks' bank and pitch (ends level), the camera crane's own interpolation —
|
|
223
|
+
* it hits every dot exactly, never overshoots between two, and is level again
|
|
224
|
+
* at both ends and wherever the neighbouring dots are.
|
|
225
|
+
*/
|
|
226
|
+
export function pathLeanAt(path, t) {
|
|
227
|
+
const marks = normalizeMarks(path?.marks);
|
|
228
|
+
if (!marks.some((mark) => mark.bank || mark.pitch)) return { bank: 0, pitch: 0 };
|
|
229
|
+
const knots = [{ t: 0, bank: 0, pitch: 0 }, ...marks, { t: 1, bank: 0, pitch: 0 }];
|
|
230
|
+
const at = clamp(finite(t), 0, 1);
|
|
231
|
+
const tidy = (value) => (Math.abs(value) < 1e-12 ? 0 : value);
|
|
232
|
+
return {
|
|
233
|
+
bank: tidy(monotoneCubicAt(knots, at, (knot) => knot.bank)),
|
|
234
|
+
pitch: tidy(monotoneCubicAt(knots, at, (knot) => knot.pitch)),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The point at arc fraction `t` of the travelled curve. */
|
|
239
|
+
export function pathPointAtFraction(path, t) {
|
|
240
|
+
const curve = pathCurve(path);
|
|
241
|
+
if (!curve.points.length) return null;
|
|
242
|
+
const at = pointAtDistance({ extend: false }, curve, clamp(finite(t), 0, 1) * curve.length);
|
|
243
|
+
return { x: at.x, y: at.y, z: at.z };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Where on the travelled curve a screen/plan position is nearest: `project`
|
|
248
|
+
* maps a world point to the editor's 2D space (pixels in the scene, metres on
|
|
249
|
+
* the board). Returns `{ d, t, point }` — distance in that space, arc fraction,
|
|
250
|
+
* the curve point — or null for a route with no length.
|
|
251
|
+
*/
|
|
252
|
+
export function nearestPathFraction(path, project, x, y) {
|
|
253
|
+
const curve = pathCurve(path);
|
|
254
|
+
if (curve.length <= 1e-9) return null;
|
|
255
|
+
let best = null;
|
|
256
|
+
let prev = null;
|
|
257
|
+
for (let i = 0; i < curve.points.length; i += 1) {
|
|
258
|
+
const screen = project(curve.points[i]);
|
|
259
|
+
if (screen && prev) {
|
|
260
|
+
const dx = screen.x - prev.screen.x;
|
|
261
|
+
const dy = screen.y - prev.screen.y;
|
|
262
|
+
const lenSq = dx * dx + dy * dy;
|
|
263
|
+
const w = lenSq < 1e-12 ? 0 : clamp(((x - prev.screen.x) * dx + (y - prev.screen.y) * dy) / lenSq, 0, 1);
|
|
264
|
+
const d = Math.hypot(prev.screen.x + dx * w - x, prev.screen.y + dy * w - y);
|
|
265
|
+
if (!best || d < best.d) {
|
|
266
|
+
const distance = curve.cumulative[prev.i] + (curve.cumulative[i] - curve.cumulative[prev.i]) * w;
|
|
267
|
+
best = { d, t: distance / curve.length };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
prev = screen ? { screen, i } : null;
|
|
97
271
|
}
|
|
98
|
-
|
|
272
|
+
if (best) best.point = pathPointAtFraction(path, best.t);
|
|
273
|
+
return best;
|
|
99
274
|
}
|
|
100
275
|
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
276
|
+
/**
|
|
277
|
+
* The travelled curve: dense points, cumulative arc length, the total, and
|
|
278
|
+
* a heading (yaw degrees, or null when the curve does not move on the floor)
|
|
279
|
+
* at every dense point.
|
|
280
|
+
*/
|
|
281
|
+
export function pathCurve(path) {
|
|
282
|
+
const points = (path?.points ?? []).map(asPoint);
|
|
283
|
+
const key = points.map((point) => `${point.x},${point.y},${point.z}`).join(";");
|
|
284
|
+
const cached = curveCache.get(key);
|
|
285
|
+
if (cached) return cached;
|
|
286
|
+
const dense = points.length ? [{ ...points[0] }] : [];
|
|
287
|
+
for (let index = 0; index + 1 < points.length; index += 1) {
|
|
288
|
+
const controls = segmentControls(points, index);
|
|
289
|
+
const steps = clamp(Math.ceil(span(points[index], points[index + 1]) / CURVE_SPACING), 2, CURVE_MAX_STEPS);
|
|
290
|
+
for (let step = 1; step <= steps; step += 1) {
|
|
291
|
+
const at = step === steps ? { ...points[index + 1] } : catmullRom(...controls, step / steps);
|
|
292
|
+
dense.push({ x: at.x, y: Math.max(0, at.y), z: at.z });
|
|
293
|
+
}
|
|
108
294
|
}
|
|
295
|
+
const cumulative = dense.length ? [0] : [];
|
|
296
|
+
for (let i = 1; i < dense.length; i += 1) cumulative.push(cumulative[i - 1] + span(dense[i - 1], dense[i]));
|
|
297
|
+
// The tangent at each sample, from its neighbours: headings blend between
|
|
298
|
+
// samples instead of stepping with every 5 cm segment.
|
|
299
|
+
const headings = dense.map((_, i) => headingBetween(dense[Math.max(0, i - 1)], dense[Math.min(dense.length - 1, i + 1)]));
|
|
300
|
+
const curve = { points: dense, cumulative, length: cumulative[cumulative.length - 1] ?? 0, headings };
|
|
301
|
+
if (curveCache.size >= CURVE_CACHE_SIZE) curveCache.delete(curveCache.keys().next().value);
|
|
302
|
+
curveCache.set(key, curve);
|
|
303
|
+
return curve;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Arc length along the travelled curve, plus the total. */
|
|
307
|
+
export function pathMetrics(path) {
|
|
308
|
+
const { cumulative, length } = pathCurve(path);
|
|
309
|
+
return { cumulative, length };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Shortest-way blend between two yaws in degrees; null-safe. */
|
|
313
|
+
function blendHeading(a, b, weight) {
|
|
314
|
+
if (a === null) return b;
|
|
315
|
+
if (b === null) return a;
|
|
316
|
+
const delta = ((((b - a) % 360) + 540) % 360) - 180;
|
|
317
|
+
return a + delta * weight;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The point at `distance` along the curve, extrapolating past the end. */
|
|
321
|
+
function pointAtDistance(path, curve, distance) {
|
|
322
|
+
const { points, cumulative, length, headings } = curve;
|
|
323
|
+
if (distance <= 0) return { ...points[0], heading: headings[0] };
|
|
324
|
+
const lastIndex = points.length - 1;
|
|
109
325
|
if (distance >= length) {
|
|
110
|
-
const last = points[
|
|
111
|
-
const heading =
|
|
326
|
+
const last = points[lastIndex];
|
|
327
|
+
const heading = headings[lastIndex];
|
|
112
328
|
if (!path.extend) return { ...last, heading };
|
|
113
329
|
// Past the end the object keeps its final direction and speed.
|
|
114
330
|
const overshoot = distance - length;
|
|
115
|
-
const previous = points[
|
|
116
|
-
const
|
|
331
|
+
const previous = points[lastIndex - 1];
|
|
332
|
+
const step = span(previous, last) || 1;
|
|
117
333
|
return {
|
|
118
|
-
x: last.x + ((last.x - previous.x) /
|
|
119
|
-
y: Math.max(0, last.y + ((last.y - previous.y) /
|
|
120
|
-
z: last.z + ((last.z - previous.z) /
|
|
334
|
+
x: last.x + ((last.x - previous.x) / step) * overshoot,
|
|
335
|
+
y: Math.max(0, last.y + ((last.y - previous.y) / step) * overshoot),
|
|
336
|
+
z: last.z + ((last.z - previous.z) / step) * overshoot,
|
|
121
337
|
heading,
|
|
122
338
|
};
|
|
123
339
|
}
|
|
124
|
-
let
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
340
|
+
let lo = 0;
|
|
341
|
+
let hi = lastIndex;
|
|
342
|
+
while (hi - lo > 1) {
|
|
343
|
+
const mid = (lo + hi) >> 1;
|
|
344
|
+
if (cumulative[mid] <= distance) lo = mid;
|
|
345
|
+
else hi = mid;
|
|
346
|
+
}
|
|
347
|
+
const a = points[lo];
|
|
348
|
+
const b = points[hi];
|
|
349
|
+
const segment = cumulative[hi] - cumulative[lo];
|
|
350
|
+
const weight = segment > 1e-9 ? (distance - cumulative[lo]) / segment : 0;
|
|
130
351
|
return {
|
|
131
352
|
x: a.x + (b.x - a.x) * weight,
|
|
132
353
|
y: a.y + (b.y - a.y) * weight,
|
|
133
354
|
z: a.z + (b.z - a.z) * weight,
|
|
134
|
-
heading:
|
|
355
|
+
heading: blendHeading(headings[lo], headings[hi], weight),
|
|
135
356
|
};
|
|
136
357
|
}
|
|
137
358
|
|
|
@@ -145,67 +366,89 @@ function headingBetween(a, b) {
|
|
|
145
366
|
}
|
|
146
367
|
|
|
147
368
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* @param {object} object a scene object record
|
|
152
|
-
* @param {number} frame absolute timeline frame
|
|
153
|
-
* @param {{ frameCount: number, fps: number }} take timeline geometry
|
|
369
|
+
* How far along the travelled curve the route has taken the object at
|
|
370
|
+
* `frame`, in metres (past `length` only when the route extends), with the
|
|
371
|
+
* curve itself. Shared by the pose and the progress readout.
|
|
154
372
|
*/
|
|
155
|
-
|
|
156
|
-
const path = createObjectPath(object?.path);
|
|
157
|
-
if (!path) return null;
|
|
158
|
-
const metrics = pathMetrics(path);
|
|
159
|
-
if (metrics.length <= 1e-9) return null;
|
|
373
|
+
function travelledDistance(path, curve, frame, take) {
|
|
160
374
|
const frameCount = Math.max(1, Math.round(finite(take.frameCount, 1)));
|
|
161
375
|
const fps = Math.max(1, finite(take.fps, 24));
|
|
162
376
|
const sampled = clamp(finite(frame), 0, Math.max(0, frameCount - 1));
|
|
163
377
|
const seconds = sampled / fps;
|
|
164
378
|
// speed 0 = fill the timeline: cover the whole path across the take.
|
|
165
379
|
const duration = Math.max(1e-6, (frameCount - 1) / fps);
|
|
166
|
-
const speed = path.speed > 0 ? path.speed :
|
|
380
|
+
const speed = path.speed > 0 ? path.speed : curve.length / duration;
|
|
167
381
|
// The travel window: the stretch of the take the route is walked in. The
|
|
168
382
|
// timing envelope shapes progress INSIDE this window; the window's length
|
|
169
383
|
// (and so the arrival frame) never moves — the area is the distance.
|
|
170
|
-
const window =
|
|
384
|
+
const window = curve.length / speed;
|
|
171
385
|
const u = seconds / Math.max(1e-6, window);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
386
|
+
if (path.loop && curve.length > 1e-9) return curve.length * timingProgress(path.timing, u - Math.floor(u));
|
|
387
|
+
// Past the window: arrived. Extend keeps walking the final heading at
|
|
388
|
+
// the plain average speed, exactly as it did before envelopes existed.
|
|
389
|
+
if (u >= 1) return curve.length + (path.extend ? speed * (seconds - window) : 0);
|
|
390
|
+
return curve.length * timingProgress(path.timing, u);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The arc fraction 0..1 the route has the object at on `frame` — what the
|
|
395
|
+
* dots are measured in — or null when the path cannot describe travel.
|
|
396
|
+
*
|
|
397
|
+
* @param {object} path a path record
|
|
398
|
+
* @param {number} frame absolute timeline frame
|
|
399
|
+
* @param {{ frameCount: number, fps: number }} take timeline geometry
|
|
400
|
+
*/
|
|
401
|
+
export function pathProgressAt(path, frame, take = {}) {
|
|
402
|
+
const normal = createObjectPath(path);
|
|
403
|
+
if (!normal) return null;
|
|
404
|
+
const curve = pathCurve(normal);
|
|
405
|
+
if (curve.length <= 1e-9) return null;
|
|
406
|
+
return clamp(travelledDistance(normal, curve, frame, take) / curve.length, 0, 1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Where an object stands at `frame`. Returns null when the object has no
|
|
411
|
+
* usable path, so callers fall back to its authored transform untouched.
|
|
412
|
+
* `bank` and `pitch` (degrees) are the lean the route's marks ask for there.
|
|
413
|
+
*
|
|
414
|
+
* @param {object} object a scene object record
|
|
415
|
+
* @param {number} frame absolute timeline frame
|
|
416
|
+
* @param {{ frameCount: number, fps: number }} take timeline geometry
|
|
417
|
+
*/
|
|
418
|
+
export function objectTransformAt(object, frame, take = {}) {
|
|
419
|
+
const path = createObjectPath(object?.path);
|
|
420
|
+
if (!path) return null;
|
|
421
|
+
const curve = pathCurve(path);
|
|
422
|
+
if (curve.length <= 1e-9) return null;
|
|
423
|
+
const distance = travelledDistance(path, curve, frame, take);
|
|
424
|
+
const at = pointAtDistance(path, curve, distance);
|
|
425
|
+
const lean = pathLeanAt(path, distance / curve.length);
|
|
183
426
|
return {
|
|
184
427
|
x: at.x,
|
|
185
428
|
y: at.y,
|
|
186
429
|
z: at.z,
|
|
187
430
|
rot: path.faceTravel && at.heading !== null ? at.heading : null,
|
|
431
|
+
bank: lean.bank,
|
|
432
|
+
pitch: lean.pitch,
|
|
188
433
|
};
|
|
189
434
|
}
|
|
190
435
|
|
|
191
436
|
/**
|
|
192
|
-
* A drawn stroke
|
|
437
|
+
* A drawn stroke keeps its shape, exactly as the camera rail's does.
|
|
193
438
|
*
|
|
194
|
-
* The rail
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* Points are added deliberately afterwards, by double-clicking the line.
|
|
439
|
+
* The rail runs the stroke through the same Douglas-Peucker pass (0.12 m) and
|
|
440
|
+
* keeps every vertex it asks for; a route is the same kind of line, so it gets
|
|
441
|
+
* the same treatment instead of a two-or-three point caricature. Those
|
|
442
|
+
* vertices are the shape; what the hand grabs are the MARKS (pathMarkFractions)
|
|
443
|
+
* — the ends, plus dots added by double-clicking the line. The schema's point
|
|
444
|
+
* ceiling is the only limit: a busier stroke coarsens until it fits.
|
|
201
445
|
*/
|
|
202
|
-
const
|
|
446
|
+
const STROKE_EPSILON = 0.12;
|
|
447
|
+
const STROKE_MAX_POINTS = MAX_PATH_POINTS;
|
|
203
448
|
|
|
204
|
-
export function strokeToPathPoints(stroke, simplify, { maxPoints = STROKE_MAX_POINTS, epsilon =
|
|
449
|
+
export function strokeToPathPoints(stroke, simplify, { maxPoints = STROKE_MAX_POINTS, epsilon = STROKE_EPSILON } = {}) {
|
|
205
450
|
if (!stroke || stroke.length < 2) return [];
|
|
206
451
|
let points = simplify(stroke, epsilon);
|
|
207
|
-
// Escalate rather than pick a single magic epsilon: the right coarseness
|
|
208
|
-
// depends on how big the drawn route is, which only the stroke knows.
|
|
209
452
|
for (let step = 0; step < 12 && points.length > maxPoints; step += 1) {
|
|
210
453
|
epsilon *= 1.8;
|
|
211
454
|
points = simplify(stroke, epsilon);
|
|
@@ -221,4 +464,4 @@ export function strokeToPathPoints(stroke, simplify, { maxPoints = STROKE_MAX_PO
|
|
|
221
464
|
return points.map((point) => ({ x: point.x, y: 0, z: point.z }));
|
|
222
465
|
}
|
|
223
466
|
|
|
224
|
-
export { MAX_PATH_POINTS, MAX_HEIGHT as MAX_PATH_HEIGHT, STROKE_MAX_POINTS };
|
|
467
|
+
export { MAX_PATH_POINTS, MAX_PATH_MARKS, MARK_GAP, MAX_LEAN as MAX_PATH_LEAN, MAX_HEIGHT as MAX_PATH_HEIGHT, STROKE_MAX_POINTS };
|