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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { ko, isKo } from "./locale.js";
|
|
2
|
+
import { LINE_EDIT_TRACK_IDS, PINNED_CURVE_ENDS } from "./line-edit.js";
|
|
3
|
+
import { FK_TRACKS, IK_TRACKS, MID_TRACKS } from "./ardy/ik.js";
|
|
4
|
+
|
|
5
|
+
// Two clocks, one boundary. The app timeline runs the production 24 fps —
|
|
6
|
+
// the rate the reference footage and the recorded export are counted in.
|
|
7
|
+
// ARDY Core generates on its trained 20 fps clock and that cannot move, so
|
|
8
|
+
// takes are RETIMED inbound (bridge → app, retimeMotion) and frame numbers
|
|
9
|
+
// are CONVERTED outbound (app → bridge, toArdyFrame). Nothing between the
|
|
10
|
+
// boundaries may mix the clocks.
|
|
11
|
+
export const TIMELINE_FPS = 24;
|
|
12
|
+
// The motion bridge uses the Studio's fixed 24 fps production clock.
|
|
13
|
+
export const ARDY_FPS = 24;
|
|
14
|
+
export const toArdyFrame = (frame) => Math.round((frame * ARDY_FPS) / TIMELINE_FPS);
|
|
15
|
+
|
|
16
|
+
// Outbound converters: timeline-frame entries → strictly-ascending bridge
|
|
17
|
+
// frames. Rounding can land two timeline frames on one bridge frame; the
|
|
18
|
+
// first wins — the bridge refuses non-ascending lists outright.
|
|
19
|
+
/** Where a placed pose lands, in TIMELINE frames, for a clip of `clipFrames`. */
|
|
20
|
+
export function posePlacementFrame(placement, clipFrames, playheadFrame) {
|
|
21
|
+
const last = Math.max(0, clipFrames - 1);
|
|
22
|
+
if (placement === "end") return last;
|
|
23
|
+
if (placement === "middle") return Math.round(last / 2);
|
|
24
|
+
if (placement === "playhead") return Math.max(0, Math.min(last, playheadFrame));
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function toArdyFrameEntries(entries) {
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const frame = toArdyFrame(entry.frame);
|
|
32
|
+
if (out.length && frame <= out[out.length - 1].frame) continue;
|
|
33
|
+
out.push({ ...entry, frame });
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function toArdySegments(segments) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const segment of segments) {
|
|
41
|
+
const startFrame = toArdyFrame(segment.startFrame);
|
|
42
|
+
const endFrame = toArdyFrame(segment.endFrame);
|
|
43
|
+
if (endFrame <= startFrame) continue; // a zero-length rounding remnant covers nothing
|
|
44
|
+
out.push({ ...segment, startFrame, endFrame });
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const DEFAULT_DURATION_S = 15; // pre-motion timeline duration; shown as duration × TIMELINE_FPS frames
|
|
50
|
+
export const DEFAULT_PLAYBACK_SPEED = 1;
|
|
51
|
+
export const BRIDGE_RECHECK_MS = 3000;
|
|
52
|
+
export const ARDY_PROMPT_HORIZON_FRAMES = 2 * TIMELINE_FPS; // core model horizon: 2 seconds, counted on the timeline clock
|
|
53
|
+
export const MAX_WAYPOINTS = 32; // ARDY bridge contract: a root path holds 2..32 distinct waypoint frames
|
|
54
|
+
export const ARDY_PROMPT_MAX = 500; // bridge contract: prompt must be non-empty, capped at 500 chars
|
|
55
|
+
export const ARDY_DURATION_MIN = 1; // the UI works in whole seconds; the bridge floor is 0.15 s
|
|
56
|
+
export const ARDY_DURATION_MAX = 1200; // bridge contract: duration capped at 1200 s
|
|
57
|
+
export const ARDY_SEED_MAX = 2 ** 31 - 1; // bridge contract: optional seed, integer in 0..2**31-1
|
|
58
|
+
// Scheduled inpainting (contract C3): how hard a regeneration holds onto the
|
|
59
|
+
// take that is already loaded. 0 turns preserving OFF entirely; 0.5 is the
|
|
60
|
+
// paper's recommended setting. The app ships this RAW number and nothing else:
|
|
61
|
+
// the strength -> denoising-schedule mapping (sigma_s = round(1000 * strength),
|
|
62
|
+
// sigma_e = min(50, sigma_s), diffusion time 0..1000) lives on the box side, and
|
|
63
|
+
// duplicating it here is exactly how the two would silently drift apart.
|
|
64
|
+
export const ARDY_PRESERVE_DEFAULT = 0.5;
|
|
65
|
+
export const DEFAULT_PROMPT_CLIPS = [];
|
|
66
|
+
|
|
67
|
+
/** The prompt-block schedule: every authored block in frame order, with the gaps
|
|
68
|
+
* between them filled by the base prompt so the bridge always receives one
|
|
69
|
+
* contiguous 0..clipFrames sequence. Blocks shorter than 3 frames are dropped —
|
|
70
|
+
* they cannot carry a generation.
|
|
71
|
+
* Pure and shared on purpose: the request assembly decides which blocks count as
|
|
72
|
+
* "edited", and the preserve panel has to name the SAME blocks. Re-deriving them
|
|
73
|
+
* approximately beside the slider is how the UI starts promising a locality the
|
|
74
|
+
* wire does not carry. */
|
|
75
|
+
export function buildPromptSchedule(sourcePromptClips, clipFrames, prompt) {
|
|
76
|
+
const segments = [];
|
|
77
|
+
let cursor = 0;
|
|
78
|
+
for (const clip of sourcePromptClips) {
|
|
79
|
+
const startFrame = Math.max(cursor, Math.min(clipFrames, clip.startFrame));
|
|
80
|
+
const endFrame = Math.max(startFrame, Math.min(clipFrames, clip.endFrame));
|
|
81
|
+
if (startFrame > cursor) segments.push({ startFrame: cursor, endFrame: startFrame, prompt });
|
|
82
|
+
if (endFrame - startFrame >= 3) segments.push({ startFrame, endFrame, prompt: clip.text.trim() || prompt });
|
|
83
|
+
cursor = Math.max(cursor, endFrame);
|
|
84
|
+
if (cursor >= clipFrames) break;
|
|
85
|
+
}
|
|
86
|
+
if (cursor < clipFrames) segments.push({ startFrame: cursor, endFrame: clipFrames, prompt });
|
|
87
|
+
if (segments.length === 0) segments.push({ startFrame: 0, endFrame: clipFrames, prompt });
|
|
88
|
+
return segments;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The DISTINCT ik track ids keyed inside the half-open timeline span
|
|
92
|
+
* [startFrame, endFrame). `ikState.keys` is Map(frame -> Map(trackId -> pose)),
|
|
93
|
+
* so a frame with several dragged effectors contributes all of them. */
|
|
94
|
+
export function ikTracksInRange(ikState, ikFrames, startFrame, endFrame) {
|
|
95
|
+
const tracks = new Set();
|
|
96
|
+
for (const frame of ikFrames) {
|
|
97
|
+
if (frame < startFrame || frame >= endFrame) continue;
|
|
98
|
+
for (const trackId of ikState?.keys?.get(frame)?.keys() ?? []) tracks.add(trackId);
|
|
99
|
+
}
|
|
100
|
+
return [...tracks];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Which limb a keyed IK track frees once the preserve mask is grouped
|
|
104
|
+
(contract C3v2). The single source of truth for track -> mask group is
|
|
105
|
+
TRACK_GROUPS in tools/kimodo/preserve-mask.mjs; nothing under tools/ may be
|
|
106
|
+
imported into src/, so the rule is transcribed here — as USER-FACING LABELS
|
|
107
|
+
only, never as a second copy of the mapping the wire depends on:
|
|
108
|
+
leftHand, leftElbow -> leftArm rightHand, rightElbow -> rightArm
|
|
109
|
+
leftFoot, leftKnee -> leftLeg rightFoot, rightKnee -> rightLeg
|
|
110
|
+
head, neck -> head spine, chest, leftShoulder,
|
|
111
|
+
hips -> torso AND root rightShoulder -> torso
|
|
112
|
+
The track ids are the same namespace on both sides (src/ardy/ik.js owns
|
|
113
|
+
them), so the app ships whatever ids it has and the mask builder maps them.
|
|
114
|
+
Torso and pelvis tracks are worded as the whole body: the pelvis carries the
|
|
115
|
+
global transform as well as a bone, and naming a limb beside it would promise
|
|
116
|
+
a locality the mask does not have. */
|
|
117
|
+
export const PRESERVE_TRACK_LIMBS = {
|
|
118
|
+
leftHand: "leftArm",
|
|
119
|
+
leftElbow: "leftArm",
|
|
120
|
+
rightHand: "rightArm",
|
|
121
|
+
rightElbow: "rightArm",
|
|
122
|
+
leftFoot: "leftLeg",
|
|
123
|
+
leftKnee: "leftLeg",
|
|
124
|
+
rightFoot: "rightLeg",
|
|
125
|
+
rightKnee: "rightLeg",
|
|
126
|
+
head: "head",
|
|
127
|
+
neck: "head",
|
|
128
|
+
spine: "body",
|
|
129
|
+
chest: "body",
|
|
130
|
+
leftShoulder: "body",
|
|
131
|
+
rightShoulder: "body",
|
|
132
|
+
hips: "body",
|
|
133
|
+
};
|
|
134
|
+
// A fixed reading order, so the same edit always reads back the same way.
|
|
135
|
+
export const PRESERVE_LIMB_ORDER = ["head", "leftArm", "rightArm", "leftLeg", "rightLeg"];
|
|
136
|
+
export const preserveLimbLabel = (limb) => ({
|
|
137
|
+
head: ko("head", "머리"),
|
|
138
|
+
leftArm: ko("left arm", "왼팔"),
|
|
139
|
+
rightArm: ko("right arm", "오른팔"),
|
|
140
|
+
leftLeg: ko("left leg", "왼발"),
|
|
141
|
+
rightLeg: ko("right leg", "오른발"),
|
|
142
|
+
}[limb]);
|
|
143
|
+
|
|
144
|
+
/** One muted line naming what a grouped preserve run will regenerate, or "" when
|
|
145
|
+
* no track can be attributed — then the request carries no `tracks` either and
|
|
146
|
+
* the panel's existing whole-take wording is already the truth. */
|
|
147
|
+
export function preserveTracksSummary(tracks) {
|
|
148
|
+
const limbs = new Set();
|
|
149
|
+
for (const trackId of tracks) {
|
|
150
|
+
const limb = PRESERVE_TRACK_LIMBS[trackId];
|
|
151
|
+
if (limb) limbs.add(limb);
|
|
152
|
+
}
|
|
153
|
+
if (limbs.size === 0) return "";
|
|
154
|
+
if (limbs.has("body")) return ko("regenerates the whole body", "몸 전체를 다시 생성");
|
|
155
|
+
const names = PRESERVE_LIMB_ORDER.filter((limb) => limbs.has(limb)).map(preserveLimbLabel);
|
|
156
|
+
return isKo ? `${names.join("·")}만 다시 생성` : `regenerates ${names.join(" · ")} only`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* --------------------------- line editing (C6) ---------------------------
|
|
160
|
+
* "Grab the joint's motion path and pull it; the joint follows the pulled path
|
|
161
|
+
* exactly" (ProjFlow). The arithmetic lives in ./line-edit.js; what stays here
|
|
162
|
+
* is the panel's own vocabulary and the one thing App owns that the pure module
|
|
163
|
+
* cannot — the mapping from a track id to a HUMAN LABEL, which is read straight
|
|
164
|
+
* out of ik.js's three tables so the picker can never drift from the pose
|
|
165
|
+
* studio's own naming. An id in LINE_EDIT_TRACK_IDS with no entry in those
|
|
166
|
+
* tables is a bug, and falling back to the raw id makes it visible instead of
|
|
167
|
+
* silent. */
|
|
168
|
+
export const LINE_EDIT_TRACK_OPTIONS = LINE_EDIT_TRACK_IDS.map((id) => ({
|
|
169
|
+
value: id,
|
|
170
|
+
label: [...IK_TRACKS, ...MID_TRACKS, ...FK_TRACKS].find((track) => track.id === id)?.label ?? id,
|
|
171
|
+
}));
|
|
172
|
+
/** The same human label, by id — what a version-strip chip says a refinement
|
|
173
|
+
* touched ("Refine · Left Hand"). */
|
|
174
|
+
export const lineTrackLabel = (id) => LINE_EDIT_TRACK_OPTIONS.find((option) => option.value === id)?.label ?? id;
|
|
175
|
+
/** The default edited track: a wrist is the joint the paper's own demo draws
|
|
176
|
+
* with, and the one whose screen trajectory a user can actually see. */
|
|
177
|
+
export const LINE_EDIT_DEFAULT_TRACK = "leftHand";
|
|
178
|
+
/** Draw a grab handle every Nth frame. Fewer would leave the curve looking
|
|
179
|
+
* un-grabbable in the gaps; more turns a 200-frame range into a solid bead of
|
|
180
|
+
* dots. The hit test tolerates CURVE_GRAB_RADIUS_PX regardless, so the handles
|
|
181
|
+
* are an affordance rather than the actual geometry. */
|
|
182
|
+
export const LINE_CURVE_MARKER_STRIDE = 4;
|
|
183
|
+
/** The shortest curve with anything draggable in it: both ends are hard-pinned
|
|
184
|
+
* to the original trajectory, so a range needs more than 2 x PINNED_CURVE_ENDS
|
|
185
|
+
* frames before the middle can move at all. */
|
|
186
|
+
export const MIN_CURVE_POINTS = PINNED_CURVE_ENDS * 2 + 1;
|
|
187
|
+
/** How long a released drag has to sit still before it costs a preview. Long
|
|
188
|
+
* enough that pull-pull-pull is one request instead of three, short enough that
|
|
189
|
+
* the loop still feels like it answers the gesture — a preview round trip is
|
|
190
|
+
* ~1 s on the warm service, so 150 ms is noise against it. */
|
|
191
|
+
export const LINE_PREVIEW_DEBOUNCE_MS = 150;
|
|
192
|
+
/** How often the line-edit capability is re-probed while the bridge is up and
|
|
193
|
+
* the answer is still no.
|
|
194
|
+
*
|
|
195
|
+
* The bridge answers `capabilities.lineEdit` from a LAZY ssh probe of the
|
|
196
|
+
* ProjFlow box behind a 5 s cache, so the very first /ardy/health of a session
|
|
197
|
+
* can land on a cold cache and come back false — a box that is perfectly fine
|
|
198
|
+
* two seconds later. Asking once per bridge-up transition therefore made the
|
|
199
|
+
* whole mode dead until a reload (first boot broken, second boot fine, which is
|
|
200
|
+
* exactly how it was reported). A capability is an ADVERTISEMENT, so it is
|
|
201
|
+
* allowed to arrive late; what is not allowed is to assume it. 4 s sits just
|
|
202
|
+
* under the bridge's own 5 s TTL, so a retry can actually see a new answer
|
|
203
|
+
* rather than re-reading the same cached false. */
|
|
204
|
+
export const LINE_CAPABILITY_RETRY_MS = 4000;
|
|
205
|
+
/** Why a validateLineEdit code refused, said once, in the user's language.
|
|
206
|
+
* Keyed by the pure module's stable codes so the copy and the check can never
|
|
207
|
+
* disagree about which field was wrong. */
|
|
208
|
+
export const LINE_EDIT_REFUSALS = {
|
|
209
|
+
sourceMotion: ["The current take has no bridge source — generate it once before editing a path", "현재 테이크에 브리지 원본이 없어요 — 궤적을 편집하기 전에 한 번 생성하세요"],
|
|
210
|
+
track: ["Pick a joint to edit first", "먼저 편집할 관절을 고르세요"],
|
|
211
|
+
frameRange: ["The frame range must sit inside the clip and span at least 2 frames", "프레임 구간은 클립 안에 있어야 하고 최소 2프레임이어야 해요"],
|
|
212
|
+
points: ["Pull a longer stretch of the path — a single point is not a path", "궤적을 좀 더 길게 잡아당겨 주세요 — 점 하나는 경로가 아니에요"],
|
|
213
|
+
camera: ["The camera could not be captured — nudge the view and try again", "카메라를 캡처하지 못했어요 — 뷰를 조금 움직인 뒤 다시 시도하세요"],
|
|
214
|
+
prompt: ["The motion prompt is not usable for a line edit", "모션 프롬프트를 라인 편집에 쓸 수 없어요"],
|
|
215
|
+
pins: ["The pinned moments could not be sent — try clearing them and pinning again", "찍은 순간을 보낼 수 없어요 — 지우고 다시 찍어 보세요"],
|
|
216
|
+
shape: ["The line edit could not be assembled", "라인 편집을 구성하지 못했어요"],
|
|
217
|
+
};
|
|
218
|
+
/** Why curveToPoints2d refused. Same shape, different stage: these are about
|
|
219
|
+
* the CURVE, before there is a payload to validate. */
|
|
220
|
+
export const LINE_CURVE_REFUSALS = {
|
|
221
|
+
empty: ["This joint is not visible over the chosen frames — frame it in view first", "선택한 구간에서 이 관절이 보이지 않아요 — 먼저 화면 안에 들어오도록 잡아 주세요"],
|
|
222
|
+
offscreen: ["The pulled path leaves the frame — keep it inside the dashed border", "잡아당긴 궤적이 화면을 벗어났어요 — 점선 테두리 안에 두세요"],
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Named ingest failures, in both locales. A reason the user cannot act on is
|
|
226
|
+
// not a message: each line says what was wrong with THIS source.
|
|
227
|
+
export const MULTIMODEL_REASONS = {
|
|
228
|
+
"url-empty": ["Enter a video URL first", "영상 URL을 먼저 입력하세요"],
|
|
229
|
+
"url-protocol-relative": ["Use a full https:// address", "https:// 로 시작하는 전체 주소를 쓰세요"],
|
|
230
|
+
"url-scheme-unsupported": ["Only http(s) or a local /path is accepted", "http(s) 또는 로컬 /경로만 사용할 수 있어요"],
|
|
231
|
+
"url-platform-page": [
|
|
232
|
+
"YouTube/Vimeo pages are players, not video files, and block browser downloads. Start the dev bridge to use the URL directly, or download the clip and use Choose video.",
|
|
233
|
+
"유튜브·비메오 주소는 영상 파일이 아니라 플레이어 페이지이고 브라우저 다운로드를 막습니다. 개발 브리지를 켜면 URL을 그대로 쓸 수 있고, 아니면 영상을 내려받아 '영상 선택'을 쓰세요.",
|
|
234
|
+
],
|
|
235
|
+
"url-malformed": ["That address could not be parsed", "주소를 해석할 수 없어요"],
|
|
236
|
+
"fetch-failed": ["The host refused a browser download (CORS or offline)", "호스트가 브라우저 다운로드를 거부했어요(CORS 또는 오프라인)"],
|
|
237
|
+
"not-a-video": ["That address did not return a video (check the path)", "그 주소는 영상을 반환하지 않았어요(경로를 확인하세요)"],
|
|
238
|
+
"decode-failed": ["This browser cannot decode that video", "이 브라우저가 디코딩할 수 없는 영상이에요"],
|
|
239
|
+
"duration-unreadable": ["The clip length could not be read", "클립 길이를 읽지 못했어요"],
|
|
240
|
+
"dimensions-unreadable": ["The frame size could not be read", "프레임 크기를 읽지 못했어요"],
|
|
241
|
+
"probe-timeout": ["The video never reported metadata", "영상이 메타데이터를 보내지 않았어요"],
|
|
242
|
+
"fetch-unavailable": ["This browser cannot download files", "이 브라우저는 파일을 내려받을 수 없어요"],
|
|
243
|
+
"footage-load-timeout": ["The clip never became seekable", "클립이 탐색 가능한 상태가 되지 않았어요"],
|
|
244
|
+
"seek-timeout": ["Seeking inside the clip stalled", "클립 내부 탐색이 멈췄어요"],
|
|
245
|
+
"pose-runtime-unavailable": ["The pose engine is missing from this build", "이 빌드에 포즈 엔진이 없어요"],
|
|
246
|
+
"pose-model-download-failed": ["The pose model could not be downloaded (offline?)", "포즈 모델을 내려받지 못했어요(오프라인인가요?)"],
|
|
247
|
+
"pose-engine-download-failed": ["The pose engine could not be downloaded (offline?)", "포즈 엔진을 내려받지 못했어요(오프라인인가요?)"],
|
|
248
|
+
"pose-engine-init-failed": ["The pose engine failed to start on this device", "이 기기에서 포즈 엔진을 시작하지 못했어요"],
|
|
249
|
+
"rest-unavailable": ["The rig rest data (/ardy/cskel27-rest.json) could not be loaded", "리그 레스트 데이터(/ardy/cskel27-rest.json)를 불러오지 못했어요"],
|
|
250
|
+
"no-person-found": ["No person was detected in the footage", "영상에서 사람을 감지하지 못했어요"],
|
|
251
|
+
"no-person-in-photo": ["No person was detected in that photograph", "사진에서 사람을 감지하지 못했어요"],
|
|
252
|
+
"image-load-timeout": ["The photograph never finished decoding", "사진 디코딩이 끝나지 않았어요"],
|
|
253
|
+
"pose-partly-occluded": ["Shoulders and hips must both be visible in the photograph", "사진에 어깨와 골반이 모두 보여야 해요"],
|
|
254
|
+
"no-usable-pose": ["A person was seen, but no stable pose could be fitted", "사람은 보였지만 안정적인 포즈를 만들지 못했어요"],
|
|
255
|
+
"rig-not-loaded": ["Subject 1's rig is not loaded yet", "인물 1 리그가 아직 로드되지 않았어요"],
|
|
256
|
+
"bridge-unreachable": ["The dev bridge did not answer", "개발 브리지가 응답하지 않아요"],
|
|
257
|
+
"bridge-footage-incomplete": ["The bridge stream ended without footage", "브리지 전송이 영상 없이 끝났어요"],
|
|
258
|
+
"footage-url-invalid": ["That address could not be used for a bridge download", "이 주소는 브리지 다운로드에 쓸 수 없어요"],
|
|
259
|
+
"footage-probe-failed": ["The bridge could not read that page's video info", "브리지가 그 페이지의 영상 정보를 읽지 못했어요"],
|
|
260
|
+
"footage-live-unsupported": ["Live streams have no fixed length and cannot be ingested", "라이브 스트림은 길이가 없어 인제스트할 수 없어요"],
|
|
261
|
+
"footage-too-long": ["That video is over 15 minutes — trim or pick a shorter one", "15분이 넘는 영상이에요 — 잘라내거나 더 짧은 걸 골라 주세요"],
|
|
262
|
+
"footage-download-failed": ["The bridge could not download that video", "브리지가 영상을 내려받지 못했어요"],
|
|
263
|
+
"footage-normalize-failed": ["The bridge could not convert that video for extraction", "브리지가 영상을 추출용으로 변환하지 못했어요"],
|
|
264
|
+
"footage-timeout": ["The bridge download took too long and was stopped", "브리지 다운로드가 너무 오래 걸려 중단됐어요"],
|
|
265
|
+
"bridge-extract-incomplete": ["The bridge stream ended without a take", "브리지 전송이 테이크 없이 끝났어요"],
|
|
266
|
+
"extract-host-missing": ["The bridge has no GPU box configured (CCLAY_EXTRACT_HOST)", "브리지에 GPU 박스가 설정돼 있지 않아요(CCLAY_EXTRACT_HOST)"],
|
|
267
|
+
"extract-bridge-required": ["GVHMR extraction requires the local GPU bridge", "GVHMR 추출에는 로컬 GPU 브리지가 필요해요"],
|
|
268
|
+
"extract-backend-unsupported": ["This bridge is not configured for GVHMR", "이 브리지가 GVHMR로 설정되지 않았어요"],
|
|
269
|
+
"extract-upload-failed": ["The footage could not be copied to the GPU box", "영상을 GPU 박스로 복사하지 못했어요"],
|
|
270
|
+
"extract-upload-too-large": ["That clip is too large to upload for extraction (300 MB cap)", "추출 업로드 한도(300MB)를 넘는 영상이에요"],
|
|
271
|
+
"extract-upload-empty": ["No video bytes arrived at the bridge", "브리지에 영상 데이터가 도착하지 않았어요"],
|
|
272
|
+
"extract-footage-unknown": ["The bridge no longer holds that download — re-ingest the URL", "브리지에 그 다운로드가 더 이상 없어요 — URL을 다시 넣어 주세요"],
|
|
273
|
+
"extract-run-failed": ["GVHMR failed on the GPU box (see the bridge log)", "GPU 박스에서 GVHMR 실행이 실패했어요(브리지 로그 확인)"],
|
|
274
|
+
"extract-no-person": ["The GPU box tracked no person in that footage", "GPU 박스가 영상에서 사람을 추적하지 못했어요"],
|
|
275
|
+
"extract-convert-failed": ["The extracted take could not be converted for the timeline", "추출된 테이크를 타임라인용으로 변환하지 못했어요"],
|
|
276
|
+
"extract-timeout": ["GPU extraction took too long and was stopped", "GPU 추출이 너무 오래 걸려 중단됐어요"],
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Extraction samples and bakes straight onto the production clock — an
|
|
280
|
+
// extracted take never touches ARDY, so it never needs retiming either.
|
|
281
|
+
export const MULTIMODEL_SAMPLE_FPS = TIMELINE_FPS;
|
package/src/asset-pane.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
2
2
|
import { ko } from "./locale.js";
|
|
3
|
-
import { CHARACTER_MODEL_IDS } from "./scenes.js";
|
|
3
|
+
import { CHARACTER_MODEL_IDS, PROXY_FIGURE_MODEL } from "./scenes.js";
|
|
4
4
|
import { OBJECT_LIBRARY } from "./scene-objects.js";
|
|
5
5
|
import { displayObjectLabel } from "./object-catalog.jsx";
|
|
6
6
|
import { assetAspect, isMeshAssetId, isSupportedMeshType } from "./scene-assets.js";
|
|
@@ -10,14 +10,25 @@ import ResourceStatus from "./resource-status.jsx";
|
|
|
10
10
|
import { logStore } from "./shell/log-store.js";
|
|
11
11
|
import "./asset-pane.css";
|
|
12
12
|
|
|
13
|
-
/** Casting assets offered in the Content browser's Characters folder. `id` doubles as
|
|
14
|
-
* file stem and the ARDY wire rig name (see scenes.js). */
|
|
15
|
-
export const CHARACTER_ASSETS =
|
|
16
|
-
id
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
/** Casting assets offered in the Content browser's Characters folder. A rig's `id` doubles as
|
|
14
|
+
* the FBX file stem and the ARDY wire rig name (see scenes.js); the capsule figure has no FBX. */
|
|
15
|
+
export const CHARACTER_ASSETS = [
|
|
16
|
+
...CHARACTER_MODEL_IDS.map((id) => ({
|
|
17
|
+
id,
|
|
18
|
+
label: id === "y-bot-tpose" ? "Y Bot" : "X Bot",
|
|
19
|
+
})),
|
|
20
|
+
{ id: PROXY_FIGURE_MODEL, label: ko("Capsule figure", "캡슐 인물") },
|
|
21
|
+
];
|
|
19
22
|
|
|
20
23
|
function CharacterPreview({ model }) {
|
|
24
|
+
if (model === PROXY_FIGURE_MODEL) {
|
|
25
|
+
return (
|
|
26
|
+
<svg className="asset-card-preview" viewBox="0 0 48 48" aria-hidden="true" data-preview="proxy-figure">
|
|
27
|
+
<circle cx="24" cy="8.5" r="5.5" />
|
|
28
|
+
<rect x="16" y="16" width="16" height="26" rx="8" />
|
|
29
|
+
</svg>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
21
32
|
const yBot = model === "y-bot-tpose";
|
|
22
33
|
return (
|
|
23
34
|
<svg className="asset-card-preview" viewBox="0 0 48 48" aria-hidden="true">
|
|
@@ -392,7 +403,7 @@ function EmptyNote({ children }) {
|
|
|
392
403
|
return <p className="content-empty">{children}</p>;
|
|
393
404
|
}
|
|
394
405
|
|
|
395
|
-
function FolderGrid({ folder, query, ...props }) {
|
|
406
|
+
export function FolderGrid({ folder, query, ...props }) {
|
|
396
407
|
const { imageAssetIds, meshAssetIds, shots = [], takeVersions = [], poses = [], onShotOpen, onTakeOpen, selectedKey, onSelect } = props;
|
|
397
408
|
let tiles = [];
|
|
398
409
|
let empty = null;
|
|
@@ -653,7 +664,6 @@ export default function AssetPane({
|
|
|
653
664
|
</button>
|
|
654
665
|
</nav>
|
|
655
666
|
<div className="content-main">
|
|
656
|
-
{/* Only a project that carries resources has totals worth a line. */}
|
|
657
667
|
{resourceManifest?.items?.length ? <ResourceStatus manifest={resourceManifest} compact /> : null}
|
|
658
668
|
{manageStorage ? (
|
|
659
669
|
<StorageManager unusedAssetIds={unusedAssetIds} usedAssetIds={usedAssetIds} usageCounts={usageCounts} graphSignature={graphSignature} trashCount={trashCount} onDelete={onDeleteUnusedAsset} onUndo={onUndoDelete} deletingAssetId={deletingAssetId} />
|
package/src/auto-color.js
CHANGED
package/src/burn-in.js
CHANGED
|
@@ -51,7 +51,7 @@ export function compositeStamp(pixels, width, height, stamp, { marginX = 0, marg
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** Rasterize one stamp line into RGBA bytes (browser-only: Canvas 2D). */
|
|
54
|
-
|
|
54
|
+
function renderStampBitmap(text, { fontPx = 18, padX = 10, padY = 6, maxWidth = Infinity } = {}) {
|
|
55
55
|
const probe = new OffscreenCanvas(1, 1).getContext("2d");
|
|
56
56
|
probe.font = `600 ${fontPx}px ui-monospace, Menlo, monospace`;
|
|
57
57
|
const textW = Math.ceil(probe.measureText(text).width);
|
package/src/camera-block.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createTiming, timingIsFlat } from "./speed-envelope.js";
|
|
|
5
5
|
|
|
6
6
|
export const CAMERA_MODES = Object.freeze(["keys", "follow", "rail"]);
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const CAMERA_FOLLOW_DEFAULTS = Object.freeze({
|
|
9
9
|
distance: 3,
|
|
10
10
|
height: 1.6,
|
|
11
11
|
response: 0.7,
|
package/src/camera-follow.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { timingProgress } from "./speed-envelope.js";
|
|
2
|
+
import { monotoneCubicAt } from "./monotone-cubic.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Follow camera: a real crew in three pieces of math.
|
|
@@ -200,7 +201,7 @@ export function buildFollowTrack(subject, fps, params = {}) {
|
|
|
200
201
|
const p = { ...FOLLOW_DEFAULTS, ...params };
|
|
201
202
|
if (!subject || subject.length === 0) return [];
|
|
202
203
|
const dt = followFixedTimestep(fps);
|
|
203
|
-
const dirs = travelDirections(subject, fps, p.initialDir
|
|
204
|
+
const dirs = travelDirections(subject, fps, p.initialDir, p.dirBlend);
|
|
204
205
|
const offsets = dirs.map((dir) => rotateDirection({ x: -dir.x, z: -dir.z }, p.orbitOffsetDeg));
|
|
205
206
|
const vels = smoothedVelocities(subject, fps);
|
|
206
207
|
const omega = omegaFor(p.response);
|
|
@@ -269,8 +270,6 @@ export function buildFollowTrack(subject, fps, params = {}) {
|
|
|
269
270
|
return track;
|
|
270
271
|
}
|
|
271
272
|
|
|
272
|
-
/* ----------------------------------------------------------- the crane --- */
|
|
273
|
-
|
|
274
273
|
/** Accepts the canonical { points } crane or the legacy { start, end } pair. */
|
|
275
274
|
function cranePoints(value) {
|
|
276
275
|
if (!value || typeof value !== "object") return null;
|
|
@@ -290,34 +289,9 @@ function cranePoints(value) {
|
|
|
290
289
|
export function craneHeightAt(craneHeight, progress) {
|
|
291
290
|
const points = cranePoints(craneHeight);
|
|
292
291
|
if (!points) return NaN;
|
|
293
|
-
|
|
294
|
-
if (clamped <= points[0].t) return points[0].height;
|
|
295
|
-
if (clamped >= points[points.length - 1].t) return points[points.length - 1].height;
|
|
296
|
-
let i = 0;
|
|
297
|
-
while (i < points.length - 2 && clamped > points[i + 1].t) i += 1;
|
|
298
|
-
const p0 = points[i];
|
|
299
|
-
const p1 = points[i + 1];
|
|
300
|
-
const h = p1.t - p0.t;
|
|
301
|
-
if (h < 1e-9) return p1.height;
|
|
302
|
-
const secant = (a, b) => (b.height - a.height) / Math.max(b.t - a.t, 1e-9);
|
|
303
|
-
const d = secant(p0, p1);
|
|
304
|
-
// harmonic mean of neighbouring secants; zero across a local extremum
|
|
305
|
-
const mono = (sa, sb) => (sa * sb <= 0 ? 0 : (2 * sa * sb) / (sa + sb));
|
|
306
|
-
const m0 = i > 0 ? mono(secant(points[i - 1], p0), d) : d;
|
|
307
|
-
const m1 = i < points.length - 2 ? mono(d, secant(p1, points[i + 2])) : d;
|
|
308
|
-
const u = (clamped - p0.t) / h;
|
|
309
|
-
const u2 = u * u;
|
|
310
|
-
const u3 = u2 * u;
|
|
311
|
-
return (
|
|
312
|
-
(2 * u3 - 3 * u2 + 1) * p0.height +
|
|
313
|
-
(u3 - 2 * u2 + u) * h * m0 +
|
|
314
|
-
(-2 * u3 + 3 * u2) * p1.height +
|
|
315
|
-
(u3 - u2) * h * m1
|
|
316
|
-
);
|
|
292
|
+
return monotoneCubicAt(points, Math.max(0, Math.min(1, progress)), (point) => point.height);
|
|
317
293
|
}
|
|
318
294
|
|
|
319
|
-
/* ------------------------------------------------------------ the rail --- */
|
|
320
|
-
|
|
321
295
|
/**
|
|
322
296
|
* Ramer–Douglas–Peucker: an authored stroke is hundreds of jittery pointer
|
|
323
297
|
* samples; the rail wants the handful of points that carry its shape.
|
|
@@ -463,7 +437,7 @@ export function buildRailFollowTrack(subject, fps, rail, params = {}) {
|
|
|
463
437
|
const crane = cranePoints(p.craneHeight) ? p.craneHeight : null;
|
|
464
438
|
const craneTargetAt = (arc) => {
|
|
465
439
|
if (!crane) return p.height;
|
|
466
|
-
return craneHeightAt(crane,
|
|
440
|
+
return craneHeightAt(crane, arc / rail.length);
|
|
467
441
|
};
|
|
468
442
|
|
|
469
443
|
const distanceErrorAt = (s, subj) => {
|
|
@@ -512,13 +486,13 @@ export function buildRailFollowTrack(subject, fps, rail, params = {}) {
|
|
|
512
486
|
// area is the rail length, and a cut pins "at this frame, be at
|
|
513
487
|
// this point of the rail". Null timing is the identity — the
|
|
514
488
|
// constant-speed schedule this line always was.
|
|
515
|
-
const authoredS = rail.length * timingProgress(p.dollyTiming
|
|
489
|
+
const authoredS = rail.length * timingProgress(p.dollyTiming, progress);
|
|
516
490
|
const distanceS = bestSNear(authoredS, subject[f], p.searchWindow, 0);
|
|
517
491
|
// Distance influence fades to zero at both authored endpoints, so
|
|
518
492
|
// it refines the middle of the move without shifting its marks.
|
|
519
493
|
const correctionEnvelope = Math.sin(Math.PI * progress) ** 2;
|
|
520
494
|
const correction = (distanceS - authoredS) * clamp(p.distanceInfluence, 0, 1) * correctionEnvelope;
|
|
521
|
-
const ledS = rail.length * timingProgress(p.dollyTiming
|
|
495
|
+
const ledS = rail.length * timingProgress(p.dollyTiming, Math.min(1, progress + leadFraction));
|
|
522
496
|
const sTarget = Math.max(s, Math.min(rail.length, ledS + correction));
|
|
523
497
|
[s, sVel] = cappedSpringStep(s, sVel, sTarget, omega, dt, p.maxDollySpeed);
|
|
524
498
|
s = Math.max(0, Math.min(s, rail.length));
|
|
@@ -535,3 +509,72 @@ export function buildRailFollowTrack(subject, fps, rail, params = {}) {
|
|
|
535
509
|
}
|
|
536
510
|
return track;
|
|
537
511
|
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Prepare an x/z rail bend for one crane mark: a copy of the drawn control
|
|
515
|
+
* points (with one inserted at the mark's base when none is close enough to
|
|
516
|
+
* anchor the bend) plus a per-point weight — a cosine bump centred on the
|
|
517
|
+
* anchor that fades to zero by the neighbouring crane marks, so dragging one
|
|
518
|
+
* mark can never tow the marks beside it. For an interior mark both rail
|
|
519
|
+
* ends are pinned outright. `maxControls` stops the seeding at a schema
|
|
520
|
+
* ceiling (the nearest point anchors the bend instead).
|
|
521
|
+
*/
|
|
522
|
+
export function prepareRailBend(controlPoints, base, marks, markIndex, { maxControls = Infinity } = {}) {
|
|
523
|
+
const controls = (controlPoints ?? []).map((entry) => ({ ...entry }));
|
|
524
|
+
if (controls.length < 2) return null;
|
|
525
|
+
// nearest control point / segment to the mark's base
|
|
526
|
+
let nearestIdx = 0;
|
|
527
|
+
let nearestD = Infinity;
|
|
528
|
+
for (let i = 0; i < controls.length; i += 1) {
|
|
529
|
+
const d = Math.hypot(controls[i].x - base.x, controls[i].z - base.z);
|
|
530
|
+
if (d < nearestD) {
|
|
531
|
+
nearestD = d;
|
|
532
|
+
nearestIdx = i;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const interior = markIndex > 0 && markIndex < marks.length - 1;
|
|
536
|
+
let anchorIdx = nearestIdx;
|
|
537
|
+
if (interior && nearestD > 0.25 && controls.length < maxControls) {
|
|
538
|
+
// no control point near the mark: bend needs a vertex to pull, so one
|
|
539
|
+
// is seeded at the base, on the segment the base projects onto
|
|
540
|
+
let segmentIdx = 0;
|
|
541
|
+
let segmentD = Infinity;
|
|
542
|
+
for (let i = 0; i < controls.length - 1; i += 1) {
|
|
543
|
+
const ax = controls[i].x;
|
|
544
|
+
const az = controls[i].z;
|
|
545
|
+
const bx = controls[i + 1].x;
|
|
546
|
+
const bz = controls[i + 1].z;
|
|
547
|
+
const lenSq = Math.max((bx - ax) ** 2 + (bz - az) ** 2, 1e-9);
|
|
548
|
+
const u = Math.max(0, Math.min(1, ((base.x - ax) * (bx - ax) + (base.z - az) * (bz - az)) / lenSq));
|
|
549
|
+
const d = Math.hypot(ax + (bx - ax) * u - base.x, az + (bz - az) * u - base.z);
|
|
550
|
+
if (d < segmentD) {
|
|
551
|
+
segmentD = d;
|
|
552
|
+
segmentIdx = i;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// An object route's points carry a height; the seed takes the one the
|
|
556
|
+
// curve has there. (The camera rail's points have none.)
|
|
557
|
+
const seed = { x: base.x, z: base.z };
|
|
558
|
+
if (base.y !== undefined) seed.y = base.y;
|
|
559
|
+
controls.splice(segmentIdx + 1, 0, seed);
|
|
560
|
+
anchorIdx = segmentIdx + 1;
|
|
561
|
+
}
|
|
562
|
+
// normalized arc parameter of each control point along the control polygon
|
|
563
|
+
const ts = [0];
|
|
564
|
+
for (let i = 1; i < controls.length; i += 1) {
|
|
565
|
+
ts.push(ts[i - 1] + Math.hypot(controls[i].x - controls[i - 1].x, controls[i].z - controls[i - 1].z));
|
|
566
|
+
}
|
|
567
|
+
const total = Math.max(ts[ts.length - 1], 1e-9);
|
|
568
|
+
for (let i = 0; i < ts.length; i += 1) ts[i] /= total;
|
|
569
|
+
// the bump reaches exactly to the neighbouring crane marks
|
|
570
|
+
const t0 = ts[anchorIdx];
|
|
571
|
+
const previous = markIndex > 0 ? marks[markIndex - 1].t : null;
|
|
572
|
+
const next = markIndex < marks.length - 1 ? marks[markIndex + 1].t : null;
|
|
573
|
+
const radius = Math.max(0.15, Math.min(previous != null ? t0 - previous : 1, next != null ? next - t0 : 1));
|
|
574
|
+
const weights = ts.map((t, i) => {
|
|
575
|
+
if (interior && (i === 0 || i === ts.length - 1)) return 0;
|
|
576
|
+
const d = Math.abs(t - t0);
|
|
577
|
+
return d >= radius ? 0 : 0.5 * (1 + Math.cos((Math.PI * d) / radius));
|
|
578
|
+
});
|
|
579
|
+
return { startControls: controls, weights, t0, radius };
|
|
580
|
+
}
|
package/src/camera-move.js
CHANGED
|
@@ -30,8 +30,6 @@ export function cameraPresetFraming(id, subject = {}, filmback = {}) {
|
|
|
30
30
|
return { id, pos, yaw, pitch, fovDeg, focalMm: preset.focalMm };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/* ------------------------------------------------------------ framing --- */
|
|
34
|
-
|
|
35
33
|
/**
|
|
36
34
|
* Snapshot the shot camera as a plain framing record.
|
|
37
35
|
* yaw/pitch follow the YXZ free-look convention in controls.jsx.
|
|
@@ -151,8 +149,6 @@ export function interpolateFraming(a, b, anchor, t, ease = easeInOut, filmback =
|
|
|
151
149
|
};
|
|
152
150
|
}
|
|
153
151
|
|
|
154
|
-
/* ----------------------------------------------------------- naming ---- */
|
|
155
|
-
|
|
156
152
|
const DEG = 180 / Math.PI;
|
|
157
153
|
|
|
158
154
|
// Below these deltas the camera is, cinematically speaking, not doing that.
|
|
@@ -288,8 +284,6 @@ export function moveSlate(move) {
|
|
|
288
284
|
return `${end[0]} → ${end[1]} · ${move.label.toUpperCase()}`;
|
|
289
285
|
}
|
|
290
286
|
|
|
291
|
-
/* ------------------------------------------------- multi-key sequences --- */
|
|
292
|
-
|
|
293
287
|
/**
|
|
294
288
|
* Sample an N-key camera move at a frame. Keys are sorted frame-unique
|
|
295
289
|
* records `{ frame, framing }`. Before the first key and after the last the
|
package/src/camera-tutorial.jsx
CHANGED
|
@@ -98,8 +98,6 @@ export const CAMERA_TUTORIAL_STEPS = [
|
|
|
98
98
|
const NAV_KINDS = new Set(["fly", "walk", "dolly", "orbit"]);
|
|
99
99
|
const SIGNAL_KINDS = new Set(["shot", "rail"]);
|
|
100
100
|
|
|
101
|
-
/* ------------------------------------------------------------- beacons --- */
|
|
102
|
-
|
|
103
101
|
// Where each of the last three steps actually happens (#211). The card names
|
|
104
102
|
// the gesture; these pin a numbered circle and a caption onto the control
|
|
105
103
|
// itself, so "click + Add shot" stops being a treasure hunt across two panes.
|
|
@@ -249,8 +247,6 @@ export function TutorialBeacon({ kind, step, role, selector, label, requires = n
|
|
|
249
247
|
);
|
|
250
248
|
}
|
|
251
249
|
|
|
252
|
-
/* -------------------------------------------------------- gesture cues --- */
|
|
253
|
-
|
|
254
250
|
// Steps 1–4 have no control to point at: the gesture IS the interface. The cue
|
|
255
251
|
// draws the mouse and the keys the step wants, animated the way the hand is
|
|
256
252
|
// supposed to move, and sits centre-bottom of the viewport where the eye
|
|
@@ -330,7 +326,7 @@ export function GestureCue({ kind, walked, leaving = false, onLeft }) {
|
|
|
330
326
|
* Top-View inset, and never takes the pointer except on its own close button
|
|
331
327
|
* — every step is completed by working the studio underneath it.
|
|
332
328
|
*/
|
|
333
|
-
export function CameraTutorial({ previewing = false, shotCount = 0, railReady = false, analytics, onStepChange, onClose, onComplete, handoff = null, shotId = null, onOpenExport, onContinue }) {
|
|
329
|
+
export function CameraTutorial({ previsMode, previewing = false, shotCount = 0, railReady = false, analytics, onStepChange, onClose, onComplete, handoff = null, shotId = null, onOpenExport, onContinue }) {
|
|
334
330
|
const [done, setDone] = useState(() => new Set());
|
|
335
331
|
const [walked, setWalked] = useState(() => new Set());
|
|
336
332
|
const [cue, setCue] = useState(null);
|
|
@@ -429,6 +425,9 @@ export function CameraTutorial({ previewing = false, shotCount = 0, railReady =
|
|
|
429
425
|
});
|
|
430
426
|
}, [currentKind]);
|
|
431
427
|
|
|
428
|
+
// Storyboard projects never show the camera tutorial (its steps are shot/rail/play).
|
|
429
|
+
if (previsMode === "storyboard") return null;
|
|
430
|
+
|
|
432
431
|
return (
|
|
433
432
|
<>
|
|
434
433
|
<aside
|
|
@@ -510,8 +509,8 @@ export function CameraTutorial({ previewing = false, shotCount = 0, railReady =
|
|
|
510
509
|
role={beacon.role}
|
|
511
510
|
selector={beacon.selector}
|
|
512
511
|
label={beacon.label}
|
|
513
|
-
requires={beacon.requires
|
|
514
|
-
absent={beacon.absent
|
|
512
|
+
requires={beacon.requires}
|
|
513
|
+
absent={beacon.absent}
|
|
515
514
|
/>
|
|
516
515
|
))}
|
|
517
516
|
</>
|