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/ardy/playback.js
CHANGED
|
@@ -314,9 +314,6 @@ export function motionBones(rig) {
|
|
|
314
314
|
return prepOf(rig).bones;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
/** QA introspection: expose the cached per-rig prep (scale, offsets, chain
|
|
318
|
-
* parents) so headless checks can compare app-side prep against node-side
|
|
319
|
-
* expectations. Not used by the app itself. */
|
|
320
317
|
/** Recover the current ARDY-space root from the live retargeted rig.
|
|
321
318
|
*
|
|
322
319
|
* Positional skinning writes each mapped bone as
|
|
@@ -326,7 +323,6 @@ export function motionBones(rig) {
|
|
|
326
323
|
* unrelated reference clip. */
|
|
327
324
|
export function captureArdyRoot(rig) {
|
|
328
325
|
const prep = prepOf(rig);
|
|
329
|
-
if (!prep) throw new Error("captureArdyRoot: rig is not prepared for ARDY playback");
|
|
330
326
|
const rootIndex = CSKEL27_JOINTS.indexOf("Hips");
|
|
331
327
|
const bone = prep.bones[rootIndex];
|
|
332
328
|
if (!bone) throw new Error("captureArdyRoot: mapped Hips bone is unavailable");
|
|
@@ -354,7 +350,7 @@ export function captureArdyRoot(rig) {
|
|
|
354
350
|
* floor-shifted the same way prepOf does, in rig units via prep.scale.
|
|
355
351
|
* Cached per (rig, boneScale array) — one take, one skeleton. */
|
|
356
352
|
const scaledOffsetCache = new WeakMap();
|
|
357
|
-
|
|
353
|
+
function scaledOffsets(prep, boneScale) {
|
|
358
354
|
let cached = scaledOffsetCache.get(boneScale);
|
|
359
355
|
if (cached && cached.prep === prep) return cached.offsets;
|
|
360
356
|
const grown = new Array(CSKEL27_JOINTS.length);
|
package/src/ardy/range-pin.js
CHANGED
|
@@ -20,7 +20,7 @@ import { createIkState, ikBakeKeyframe, solveHipsTranslateToFloor, solveIk } fro
|
|
|
20
20
|
* | { space: "object", objectId, local: [x,y,z] } }
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
const RANGE_PIN_TRACKS = Object.freeze(["leftHand", "rightHand", "leftFoot", "rightFoot"]);
|
|
24
24
|
const LEGS = ["leftFoot", "rightFoot"];
|
|
25
25
|
const REACH_MARGIN = 0.005;
|
|
26
26
|
const BODY_STEP = 0.01;
|
|
@@ -35,7 +35,7 @@ export function rangePinTracks(pin) {
|
|
|
35
35
|
const BLEND_MAX = 240;
|
|
36
36
|
const ID_MAX = 64;
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
class RangePinError extends Error {
|
|
39
39
|
constructor(code, message) {
|
|
40
40
|
super(message);
|
|
41
41
|
this.name = "RangePinError";
|
package/src/ardy/root-drop.js
CHANGED
|
@@ -119,7 +119,6 @@ export function applySupportRise(
|
|
|
119
119
|
const rise = (motion.rootPos[evidenceFrame * 3 + 1] - motion.rootPos[from * 3 + 1]) * scale;
|
|
120
120
|
const slope = rise / ((evidenceFrame - from) / motion.fps);
|
|
121
121
|
if (rise < Number(minRise) || slope < Number(minSlope)) continue;
|
|
122
|
-
const feet = SUPPORT_FEET.filter((joint) => joint < joints);
|
|
123
122
|
if (!feet.length) continue;
|
|
124
123
|
const supportRect = {
|
|
125
124
|
x: Number(support.x) || 0, z: Number(support.z) || 0,
|
package/src/ardy/timeline.css
CHANGED
|
@@ -858,10 +858,11 @@
|
|
|
858
858
|
/* The bar hangs above the block's top-left corner instead of inside it: a
|
|
859
859
|
short cut (40 frames ~ 38px) is narrower than Split + Duplicate + Delete,
|
|
860
860
|
and an in-block bar would sit on the shot's name, turning a click on the
|
|
861
|
-
name into Delete.
|
|
861
|
+
name into Delete. Keep the edges flush so the pointer can travel from the
|
|
862
|
+
block into the bar without ending :hover in a gap. */
|
|
862
863
|
.v2-sequencer .tl-shot-actions {
|
|
863
864
|
top: auto;
|
|
864
|
-
bottom:
|
|
865
|
+
bottom: 100%;
|
|
865
866
|
left: 0;
|
|
866
867
|
right: auto;
|
|
867
868
|
background: var(--overlay, rgba(12, 12, 13, .78));
|
|
@@ -870,10 +871,17 @@
|
|
|
870
871
|
/* The block clips its own box (overflow hidden) so the hanging bar only exists
|
|
871
872
|
while it is wanted; the label clips its own text, so nothing else escapes. */
|
|
872
873
|
.v2-sequencer .tl-shot-block:hover,
|
|
873
|
-
.v2-sequencer .tl-shot-block:focus-within
|
|
874
|
+
.v2-sequencer .tl-shot-block:focus-within,
|
|
875
|
+
.v2-sequencer .tl-shot-block.selected,
|
|
876
|
+
.v2-sequencer .tl-shot-block.active {
|
|
874
877
|
overflow: visible;
|
|
875
878
|
}
|
|
876
879
|
|
|
880
|
+
.v2-sequencer .tl-shot-block.selected .tl-shot-actions,
|
|
881
|
+
.v2-sequencer .tl-shot-block.active .tl-shot-actions {
|
|
882
|
+
display: flex;
|
|
883
|
+
}
|
|
884
|
+
|
|
877
885
|
.v2-sequencer .tl-shot-actions button {
|
|
878
886
|
height: var(--h-row, 24px);
|
|
879
887
|
border-radius: var(--r-1, 4px);
|
|
@@ -1167,10 +1175,17 @@
|
|
|
1167
1175
|
fill: var(--select-tint, rgba(232, 163, 61, .14));
|
|
1168
1176
|
}
|
|
1169
1177
|
|
|
1170
|
-
.v2-sequencer .sg-cut
|
|
1171
|
-
.v2-sequencer .sg-cut-diamond {
|
|
1178
|
+
.v2-sequencer .sg-cut {
|
|
1172
1179
|
stroke: var(--select, #e8a33d);
|
|
1173
|
-
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
.v2-sequencer .sg-cut-pin {
|
|
1183
|
+
background: var(--select, #e8a33d);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
.v2-sequencer .sg-cut-pin.selected {
|
|
1187
|
+
background: var(--select, #e8a33d);
|
|
1188
|
+
box-shadow: 0 0 0 1.5px var(--surface-panel, #1a1a1c), 0 0 0 3px var(--select, #e8a33d);
|
|
1174
1189
|
}
|
|
1175
1190
|
|
|
1176
1191
|
.v2-sequencer .sg-playhead {
|
|
@@ -1212,3 +1227,166 @@
|
|
|
1212
1227
|
width: var(--tl-track-tree-width, 190px);
|
|
1213
1228
|
}
|
|
1214
1229
|
}
|
|
1230
|
+
|
|
1231
|
+
/* ------------------------------------------------- Route strip (v2) ----
|
|
1232
|
+
A prop's route rows wear the sequencer's own chips and type: neutral
|
|
1233
|
+
pills, the segmented "on" look for toggles, muted facts, and the select
|
|
1234
|
+
colour only on the curve. The legacy green instrument stays for the
|
|
1235
|
+
classic shell (styles.css). */
|
|
1236
|
+
.v2-sequencer .tl-track.objmo .tl-track-label {
|
|
1237
|
+
gap: 6px;
|
|
1238
|
+
color: var(--text-1, #e4e4e7);
|
|
1239
|
+
font: 500 12px var(--sans);
|
|
1240
|
+
}
|
|
1241
|
+
.v2-sequencer .tl-track.objmo .tl-track-label .tl-subject-kind {
|
|
1242
|
+
color: var(--label, #8e8e96);
|
|
1243
|
+
font: 500 10px var(--sans);
|
|
1244
|
+
letter-spacing: .04em;
|
|
1245
|
+
}
|
|
1246
|
+
.v2-sequencer .tl-track.objmo .tl-lane {
|
|
1247
|
+
background: none;
|
|
1248
|
+
border-bottom: 1px solid var(--hairline, rgba(255,255,255,.07));
|
|
1249
|
+
}
|
|
1250
|
+
.v2-sequencer .objmo-tools {
|
|
1251
|
+
gap: 6px;
|
|
1252
|
+
padding: 4px 10px;
|
|
1253
|
+
color: var(--text-2, #6e6e73);
|
|
1254
|
+
font: 400 11px var(--sans);
|
|
1255
|
+
}
|
|
1256
|
+
.v2-sequencer .objmo-tools .tl-camera-tool,
|
|
1257
|
+
.v2-sequencer .objmo .sg-head .tl-camera-tool {
|
|
1258
|
+
height: 24px;
|
|
1259
|
+
padding: 0 10px;
|
|
1260
|
+
border: 0;
|
|
1261
|
+
border-radius: 12px;
|
|
1262
|
+
background: var(--chip-fill, rgba(255,255,255,.06));
|
|
1263
|
+
color: var(--text-soft, #c9c9ce);
|
|
1264
|
+
font: 500 11px var(--sans);
|
|
1265
|
+
}
|
|
1266
|
+
.v2-sequencer .objmo-tools .tl-camera-tool:hover:not(:disabled),
|
|
1267
|
+
.v2-sequencer .objmo .sg-head .tl-camera-tool:hover:not(:disabled) {
|
|
1268
|
+
background: var(--field-fill, rgba(255,255,255,.05));
|
|
1269
|
+
color: var(--text-1, #e4e4e7);
|
|
1270
|
+
}
|
|
1271
|
+
.v2-sequencer .objmo-tools .tl-camera-tool.active {
|
|
1272
|
+
background: var(--seg-on, rgba(255,255,255,.14));
|
|
1273
|
+
box-shadow: var(--seg-on-shadow, none);
|
|
1274
|
+
color: var(--seg-on-text, #e08a55);
|
|
1275
|
+
}
|
|
1276
|
+
.v2-sequencer .objmo-tools .tl-camera-tool.danger,
|
|
1277
|
+
.v2-sequencer .objmo .sg-head .tl-camera-tool.danger {
|
|
1278
|
+
background: none;
|
|
1279
|
+
color: var(--text-3, #636369);
|
|
1280
|
+
}
|
|
1281
|
+
.v2-sequencer .objmo-tools .tl-camera-tool.danger:hover:not(:disabled),
|
|
1282
|
+
.v2-sequencer .objmo .sg-head .tl-camera-tool.danger:hover:not(:disabled) {
|
|
1283
|
+
background: var(--chip-fill, rgba(255,255,255,.06));
|
|
1284
|
+
color: var(--danger, #e5484d);
|
|
1285
|
+
}
|
|
1286
|
+
.v2-sequencer .objmo-tools .objmo-end { margin-left: auto; }
|
|
1287
|
+
.v2-sequencer .objmo-speed {
|
|
1288
|
+
height: 24px;
|
|
1289
|
+
padding: 0 10px 0 9px;
|
|
1290
|
+
border: 0;
|
|
1291
|
+
border-radius: 12px;
|
|
1292
|
+
background: var(--chip-fill, rgba(255,255,255,.06));
|
|
1293
|
+
color: var(--label, #8e8e96);
|
|
1294
|
+
font: 500 11px var(--sans);
|
|
1295
|
+
}
|
|
1296
|
+
.v2-sequencer .objmo-speed input[type="range"]::-webkit-slider-runnable-track { background: var(--divider-strong, rgba(255,255,255,.18)); }
|
|
1297
|
+
.v2-sequencer .objmo-speed input[type="range"]::-moz-range-track { background: var(--divider-strong, rgba(255,255,255,.18)); }
|
|
1298
|
+
.v2-sequencer .objmo-speed input[type="range"]::-webkit-slider-thumb {
|
|
1299
|
+
background: var(--seg-on, #fff);
|
|
1300
|
+
box-shadow: var(--seg-on-shadow, 0 1px 2px rgba(0,0,0,.3));
|
|
1301
|
+
}
|
|
1302
|
+
.v2-sequencer .objmo-speed input[type="range"]::-moz-range-thumb {
|
|
1303
|
+
background: var(--seg-on, #fff);
|
|
1304
|
+
box-shadow: var(--seg-on-shadow, 0 1px 2px rgba(0,0,0,.3));
|
|
1305
|
+
}
|
|
1306
|
+
.v2-sequencer .objmo-speed input[type="range"]:focus-visible { outline-color: var(--select, #e8a33d); }
|
|
1307
|
+
.v2-sequencer .objmo-speed .objmo-speed-value { color: var(--text-1, #e4e4e7); }
|
|
1308
|
+
.v2-sequencer .objmo-lean .number-field {
|
|
1309
|
+
height: 24px;
|
|
1310
|
+
border: 0;
|
|
1311
|
+
border-radius: 12px;
|
|
1312
|
+
background: var(--chip-fill, rgba(255,255,255,.06));
|
|
1313
|
+
color: var(--text-1, #e4e4e7);
|
|
1314
|
+
overflow: hidden;
|
|
1315
|
+
}
|
|
1316
|
+
.v2-sequencer .objmo-lean .number-field .axis {
|
|
1317
|
+
background: none;
|
|
1318
|
+
color: var(--label, #8e8e96);
|
|
1319
|
+
font: 500 11px var(--sans);
|
|
1320
|
+
}
|
|
1321
|
+
.v2-sequencer .objmo-lean .number-field input {
|
|
1322
|
+
border: 0;
|
|
1323
|
+
background: none;
|
|
1324
|
+
color: var(--text-1, #e4e4e7);
|
|
1325
|
+
font: 500 11px var(--sans);
|
|
1326
|
+
}
|
|
1327
|
+
.v2-sequencer .objmo-lean .number-field:focus-within { box-shadow: 0 0 0 1.5px var(--select, #e8a33d) inset; }
|
|
1328
|
+
.v2-sequencer .tl-path-hint {
|
|
1329
|
+
color: var(--text-3, #636369);
|
|
1330
|
+
font: 400 10.5px var(--sans);
|
|
1331
|
+
white-space: nowrap;
|
|
1332
|
+
overflow: hidden;
|
|
1333
|
+
text-overflow: ellipsis;
|
|
1334
|
+
}
|
|
1335
|
+
.v2-sequencer .objmo .sg-head {
|
|
1336
|
+
gap: 8px;
|
|
1337
|
+
padding: 4px 10px 3px;
|
|
1338
|
+
color: var(--label, #8e8e96);
|
|
1339
|
+
font: 400 11px var(--sans);
|
|
1340
|
+
}
|
|
1341
|
+
.v2-sequencer .objmo .sg-head .sg-facts { color: var(--text-1, #e4e4e7); font-weight: 500; }
|
|
1342
|
+
/* The body spans the lane edge to edge: the SVG maps time across its own
|
|
1343
|
+
width, so a side margin would put the curve on a different clock from the
|
|
1344
|
+
ruler and the playhead above it. */
|
|
1345
|
+
.v2-sequencer .objmo .sg-body {
|
|
1346
|
+
margin: 0 0 6px;
|
|
1347
|
+
border: 0;
|
|
1348
|
+
border-radius: 0;
|
|
1349
|
+
background: var(--tile-fill, rgba(255,255,255,.04));
|
|
1350
|
+
}
|
|
1351
|
+
/* The sequencer's own playhead already crosses the strip. */
|
|
1352
|
+
.v2-sequencer .objmo .sg-playhead { display: none; }
|
|
1353
|
+
.v2-sequencer .objmo .sg-line { stroke: var(--select, #e8a33d); }
|
|
1354
|
+
.v2-sequencer .objmo .sg-fill { fill: var(--select-tint, rgba(232,163,61,.14)); }
|
|
1355
|
+
.v2-sequencer .objmo .sg-axis,
|
|
1356
|
+
.v2-sequencer .objmo .sg-average { stroke: var(--divider-strong, rgba(255,255,255,.18)); }
|
|
1357
|
+
.v2-sequencer .objmo .sg-outside { fill: var(--chip-fill, rgba(255,255,255,.06)); }
|
|
1358
|
+
.v2-sequencer .objmo .sg-scale-top,
|
|
1359
|
+
.v2-sequencer .objmo .sg-scale-avg,
|
|
1360
|
+
.v2-sequencer .objmo .sg-scale-zero { color: var(--text-3, #636369); }
|
|
1361
|
+
.v2-sequencer .objmo .sg-readout { background: var(--text-1, #e4e4e7); color: var(--bg, #fff); }
|
|
1362
|
+
|
|
1363
|
+
/* The prop strip is three rows of its own — controls, dots, the speed graph —
|
|
1364
|
+
not the performer's row / group / row / group grid: give the surface that
|
|
1365
|
+
shape while it is loaded, and let the graph take whatever height is left
|
|
1366
|
+
instead of spilling past the dock. */
|
|
1367
|
+
.v2-sequencer .tl-surface.prop-subject {
|
|
1368
|
+
grid-template-rows:
|
|
1369
|
+
var(--tl-ruler-height, 24px)
|
|
1370
|
+
var(--tl-prop-row-height, 32px)
|
|
1371
|
+
var(--tl-prop-row-height, 32px)
|
|
1372
|
+
minmax(0, 1fr);
|
|
1373
|
+
}
|
|
1374
|
+
.v2-sequencer .tl-track.objmo {
|
|
1375
|
+
height: var(--tl-prop-row-height, 32px);
|
|
1376
|
+
min-height: 0;
|
|
1377
|
+
}
|
|
1378
|
+
.v2-sequencer .tl-track.objmo .tl-lane {
|
|
1379
|
+
min-height: 0;
|
|
1380
|
+
height: 100%;
|
|
1381
|
+
overflow: hidden;
|
|
1382
|
+
}
|
|
1383
|
+
.v2-sequencer .tl-track.objmo.sg-row,
|
|
1384
|
+
.v2-sequencer .tl-track.objmo.sg-row .tl-lane {
|
|
1385
|
+
height: 100%;
|
|
1386
|
+
min-height: 0;
|
|
1387
|
+
}
|
|
1388
|
+
.v2-sequencer .tl-track.objmo.sg-row .tl-track-label { padding-top: 6px; }
|
|
1389
|
+
.v2-sequencer .objmo-tools { height: 100%; padding: 0 10px; flex-wrap: nowrap; overflow: hidden; }
|
|
1390
|
+
.v2-sequencer .objmo .sg { position: absolute; inset: 0; }
|
|
1391
|
+
.v2-sequencer .objmo .sg-head { padding: 3px 10px 2px; }
|
|
1392
|
+
.v2-sequencer .objmo .sg-body { min-height: 36px; margin: 0 0 6px; }
|
package/src/ardy/timeline.jsx
CHANGED
|
@@ -4,8 +4,10 @@ import { motionSegmentSpeedForFrames } from "./motion-edit.js";
|
|
|
4
4
|
import { createPlaybackClock } from "./playback-clock.js";
|
|
5
5
|
import { promptResizeFrame } from "./timeline-resize.js";
|
|
6
6
|
import { ko, isKo } from "../locale.js";
|
|
7
|
+
import { isImeComposing } from "../ime.js";
|
|
7
8
|
import { buildRail, craneHeightAt } from "../camera-follow.js";
|
|
8
|
-
import { pathMetrics } from "../object-path.js";
|
|
9
|
+
import { insertPathMark, pathMarks, pathMetrics, pathProgressAt, MAX_PATH_LEAN } from "../object-path.js";
|
|
10
|
+
import { NumberField } from "../ui.jsx";
|
|
9
11
|
import { flatTiming, timingIsFlat, envelopeDrag, insertCut, removeCut, CUT_MIN_GAP } from "../speed-envelope.js";
|
|
10
12
|
import { checkShotAgainstPreset, presetById } from "../model-presets.js";
|
|
11
13
|
import "./timeline.css";
|
|
@@ -70,8 +72,6 @@ const framePct = (f, count) => (count > 1 ? f / (count - 1) : 0);
|
|
|
70
72
|
// number of frames divided by its clock. Keeping this conversion local makes
|
|
71
73
|
// the readout honest at any loaded fps (the final frame is just shy of the
|
|
72
74
|
// displayed total duration, as it is in an actual frame sequence).
|
|
73
|
-
const formatTimelineSeconds = (seconds) => `${Math.max(0, Number(seconds) || 0).toFixed(2)}s`;
|
|
74
|
-
|
|
75
75
|
const CAMERA_BLOCK_DEFAULTS = {
|
|
76
76
|
distance: 3,
|
|
77
77
|
height: 1.6,
|
|
@@ -317,7 +317,6 @@ const GRAPH_MIN_SCALE = 2;
|
|
|
317
317
|
const DRAG_TRAVEL_PX = 220;
|
|
318
318
|
const FINE_DRAG_FACTOR = 0.25;
|
|
319
319
|
|
|
320
|
-
/** Value under the pointer for a rate-based vertical drag. */
|
|
321
320
|
function dragValue(start, event, unitsPerPx) {
|
|
322
321
|
const gain = unitsPerPx * (event.shiftKey ? FINE_DRAG_FACTOR : 1);
|
|
323
322
|
return start.value + (start.clientY - event.clientY) * gain;
|
|
@@ -507,9 +506,6 @@ function SpeedGraph({
|
|
|
507
506
|
<div className={"sg" + (bare ? " sg-bare" : "")}>
|
|
508
507
|
{!bare && <header className="sg-head">
|
|
509
508
|
<span className="sg-facts">{facts ?? `${averageSpeed.toFixed(1)} ${speedUnit} ${ko("average", "평균")}`}</span>
|
|
510
|
-
<span className="tl-path-hint">
|
|
511
|
-
{ko("drag the curve · double-click or the button cuts · Delete removes a cut", "곡선을 끌어 조절 · 더블클릭이나 버튼으로 컷 · 컷 선택 후 Delete로 삭제")}
|
|
512
|
-
</span>
|
|
513
509
|
<button
|
|
514
510
|
type="button"
|
|
515
511
|
className="tl-camera-tool"
|
|
@@ -533,7 +529,7 @@ function SpeedGraph({
|
|
|
533
529
|
</button>
|
|
534
530
|
)}
|
|
535
531
|
</header>}
|
|
536
|
-
<div className="sg-body">
|
|
532
|
+
<div className="sg-body" title={ko("Drag the curve · double-click or the button cuts · Delete removes a cut", "곡선을 끌어 조절 · 더블클릭이나 버튼으로 컷 · 컷 선택 후 Delete로 삭제")}>
|
|
537
533
|
<svg
|
|
538
534
|
ref={svgRef}
|
|
539
535
|
viewBox="0 0 1 1"
|
|
@@ -550,7 +546,6 @@ function SpeedGraph({
|
|
|
550
546
|
<stop offset="100%" stopOpacity="0.05" />
|
|
551
547
|
</linearGradient>
|
|
552
548
|
</defs>
|
|
553
|
-
{/* axes: the floor is zero speed, the dashed line is the average */}
|
|
554
549
|
<line className="sg-axis" x1="0" y1="1" x2="1" y2="1" />
|
|
555
550
|
<line className="sg-average" x1={origin} y1={avgY} x2={origin + span} y2={avgY} />
|
|
556
551
|
{origin > 0.001 && <rect className="sg-outside" x="0" y="0" width={origin} height="1" />}
|
|
@@ -570,7 +565,6 @@ function SpeedGraph({
|
|
|
570
565
|
return (
|
|
571
566
|
<g key={index} className={"sg-pin" + (index === selectedCut ? " selected" : "")}>
|
|
572
567
|
<line className="sg-cut" x1={x} y1="0.06" x2={x} y2="1" />
|
|
573
|
-
<path className="sg-cut-diamond" d={`M ${x} 0.055 l 0.007 -0.045 l -0.014 0 l 0.007 0.045 z`} />
|
|
574
568
|
<line
|
|
575
569
|
className="sg-cut-pick"
|
|
576
570
|
x1={x}
|
|
@@ -587,6 +581,26 @@ function SpeedGraph({
|
|
|
587
581
|
})}
|
|
588
582
|
<line className="sg-playhead" x1={playheadTakeX} y1="0" x2={playheadTakeX} y2="1" />
|
|
589
583
|
</svg>
|
|
584
|
+
{/* The pin heads are HTML, not SVG: the graph's 1×1 viewBox is
|
|
585
|
+
stretched to the lane, so a shape drawn inside it comes out as
|
|
586
|
+
wide as the lane is long, and a stroke on it swallowed the whole
|
|
587
|
+
graph. A positioned square keeps its size whatever the lane does. */}
|
|
588
|
+
{shown.cuts.map((cut, index) => (
|
|
589
|
+
<button
|
|
590
|
+
key={index}
|
|
591
|
+
type="button"
|
|
592
|
+
className={"sg-cut-pin" + (index === selectedCut ? " selected" : "")}
|
|
593
|
+
style={{ left: `${((origin + cut.t * span) * 100).toFixed(3)}%` }}
|
|
594
|
+
aria-pressed={index === selectedCut}
|
|
595
|
+
aria-label={ko("Cut", "컷")}
|
|
596
|
+
title={ko("Cut: click to select, Delete removes it", "컷: 클릭해 선택, Delete로 삭제")}
|
|
597
|
+
onPointerDown={(event) => {
|
|
598
|
+
event.stopPropagation();
|
|
599
|
+
event.preventDefault();
|
|
600
|
+
setSelectedCut(index === selectedCut ? null : index);
|
|
601
|
+
}}
|
|
602
|
+
/>
|
|
603
|
+
))}
|
|
590
604
|
<span className="sg-scale-top">{Math.round(yMax * averageSpeed * 10) / 10}</span>
|
|
591
605
|
<span className="sg-scale-avg" style={{ top: `${avgY * 100}%` }}>{averageSpeed.toFixed(1)}</span>
|
|
592
606
|
<span className="sg-scale-zero">0</span>
|
|
@@ -600,7 +614,6 @@ function SpeedGraph({
|
|
|
600
614
|
);
|
|
601
615
|
}
|
|
602
616
|
|
|
603
|
-
/** Frames the prop is actually travelling for, given its speed. */
|
|
604
617
|
function travelSpan(path, metrics, frameCount, fps) {
|
|
605
618
|
if (!path || !metrics || metrics.length <= 0) return null;
|
|
606
619
|
const last = Math.max(1, frameCount - 1);
|
|
@@ -617,12 +630,21 @@ function travelSpan(path, metrics, frameCount, fps) {
|
|
|
617
630
|
* row ask "whose?". The frame ruler and the transport above stay put, because
|
|
618
631
|
* those belong to the take, not to any one subject.
|
|
619
632
|
*/
|
|
620
|
-
function ObjectTravelTrack({ object, frame, frameCount, fps, pathDraw, onPathDrawToggle, onPathChange, onPathClear, onTimingGestureStart, onTimingGestureEnd }) {
|
|
633
|
+
function ObjectTravelTrack({ object, frame, frameCount, fps, pathDraw, onPathDrawToggle, onPathChange, onPathClear, onTimingGestureStart, onTimingGestureEnd, markIndex = null, onMarkAdd, onMarkLean, onMarkScrubStart, onMarkScrubEnd }) {
|
|
621
634
|
const path = object?.path ?? null;
|
|
622
635
|
const metrics = useMemo(() => (path ? pathMetrics(path) : null), [path]);
|
|
623
636
|
const span = useMemo(() => travelSpan(path, metrics, frameCount, fps), [path, metrics, frameCount, fps]);
|
|
624
637
|
const patch = (change) => onPathChange?.({ ...path, ...change });
|
|
625
638
|
const seconds = span ? (span.end - span.start) / Math.max(1, fps) : 0;
|
|
639
|
+
// The dot under the playhead: where on the route the prop is this frame.
|
|
640
|
+
// Null when the route is full or the prop is standing on a dot or an end.
|
|
641
|
+
const playheadMark = useMemo(() => {
|
|
642
|
+
if (!path) return null;
|
|
643
|
+
const progress = pathProgressAt(path, frame, { frameCount, fps });
|
|
644
|
+
return progress == null ? null : insertPathMark(path, progress);
|
|
645
|
+
}, [path, frame, frameCount, fps]);
|
|
646
|
+
const marks = useMemo(() => (path ? pathMarks(path) : []), [path]);
|
|
647
|
+
const selectedMark = markIndex != null && markIndex > 0 && markIndex <= marks.length ? marks[markIndex - 1] : null;
|
|
626
648
|
return (
|
|
627
649
|
<>
|
|
628
650
|
<div className="tl-track objmo">
|
|
@@ -681,22 +703,16 @@ function ObjectTravelTrack({ object, frame, frameCount, fps, pathDraw, onPathDra
|
|
|
681
703
|
>
|
|
682
704
|
{ko("Loop", "반복")}
|
|
683
705
|
</button>
|
|
706
|
+
{/* Destructive, so it sits apart at the row's end and only
|
|
707
|
+
turns red under the pointer. */}
|
|
684
708
|
<button
|
|
685
709
|
type="button"
|
|
686
|
-
className="tl-camera-tool danger"
|
|
710
|
+
className="tl-camera-tool danger objmo-end"
|
|
687
711
|
title={ko("Delete this route; the object stands still again", "경로를 지웁니다. 오브젝트는 다시 제자리에 섭니다")}
|
|
688
712
|
onClick={() => onPathClear?.()}
|
|
689
713
|
>
|
|
690
714
|
{ko("Delete path", "경로 삭제")}
|
|
691
715
|
</button>
|
|
692
|
-
{/* The two gestures nobody guesses, on the same row rather than
|
|
693
|
-
a lane of their own — an empty track reads as broken. */}
|
|
694
|
-
<span className="tl-path-hint">
|
|
695
|
-
{ko(
|
|
696
|
-
`${metrics.length.toFixed(1)} m · ${path.points.length} points · double-click the line to add a point · Delete removes it`,
|
|
697
|
-
`${metrics.length.toFixed(1)} m · 점 ${path.points.length}개 · 선을 더블클릭하면 점 추가 · Delete로 삭제`,
|
|
698
|
-
)}
|
|
699
|
-
</span>
|
|
700
716
|
</>
|
|
701
717
|
) : (
|
|
702
718
|
<span className="tl-path-hint">
|
|
@@ -705,6 +721,59 @@ function ObjectTravelTrack({ object, frame, frameCount, fps, pathDraw, onPathDra
|
|
|
705
721
|
)}
|
|
706
722
|
</div>
|
|
707
723
|
</div>
|
|
724
|
+
{path && (
|
|
725
|
+
<div className="tl-track objmo objmo-dots">
|
|
726
|
+
<span className="tl-track-label">{ko("Dots", "점")}</span>
|
|
727
|
+
<div className="tl-lane objmo-tools">
|
|
728
|
+
<button
|
|
729
|
+
type="button"
|
|
730
|
+
className="tl-camera-tool"
|
|
731
|
+
data-testid="route-add-dot"
|
|
732
|
+
disabled={!playheadMark}
|
|
733
|
+
title={playheadMark
|
|
734
|
+
? ko("Add a dot where the prop is on this frame", "지금 프레임에서 소품이 있는 곳에 점을 추가합니다")
|
|
735
|
+
: ko("No room for a dot here: the route is full, or the playhead is on a dot or an end", "여기엔 점을 추가할 수 없어요: 경로가 가득 찼거나, 재생 위치가 점이나 끝에 있어요")}
|
|
736
|
+
onClick={() => playheadMark && onMarkAdd?.(playheadMark.marks[playheadMark.index - 1].t)}
|
|
737
|
+
>
|
|
738
|
+
{ko("Add dot at playhead", "현재 위치에 점 추가")}
|
|
739
|
+
</button>
|
|
740
|
+
<span className="objmo-lean" data-testid="route-lean-fields" title={selectedMark ? undefined : ko("Ends are always level; select a dot between them to lean the prop there", "양 끝은 항상 수평이에요. 사이의 점을 선택하면 그 지점에서 기울일 수 있어요")}>
|
|
741
|
+
<NumberField
|
|
742
|
+
label={ko("Bank", "기울기")}
|
|
743
|
+
title={ko("Roll about the direction of travel; positive leans onto the right side", "진행 방향을 축으로 한 기울기; 양수는 오른쪽이 내려갑니다")}
|
|
744
|
+
value={selectedMark ? selectedMark.bank : 0}
|
|
745
|
+
step={1}
|
|
746
|
+
precision={1}
|
|
747
|
+
scrubRange={MAX_PATH_LEAN}
|
|
748
|
+
disabled={!selectedMark}
|
|
749
|
+
onChange={(value, token) => onMarkLean?.("bank", value, token)}
|
|
750
|
+
onScrubStart={onMarkScrubStart}
|
|
751
|
+
onScrubEnd={onMarkScrubEnd}
|
|
752
|
+
/>
|
|
753
|
+
<NumberField
|
|
754
|
+
label={ko("Pitch", "피치")}
|
|
755
|
+
title={ko("Nose up (positive) or down; the prop holds this angle at the dot", "코가 위(양수) 또는 아래로 향하는 각도; 이 점에서 이 각도를 유지합니다")}
|
|
756
|
+
value={selectedMark ? selectedMark.pitch : 0}
|
|
757
|
+
step={1}
|
|
758
|
+
precision={1}
|
|
759
|
+
scrubRange={MAX_PATH_LEAN}
|
|
760
|
+
disabled={!selectedMark}
|
|
761
|
+
onChange={(value, token) => onMarkLean?.("pitch", value, token)}
|
|
762
|
+
onScrubStart={onMarkScrubStart}
|
|
763
|
+
onScrubEnd={onMarkScrubEnd}
|
|
764
|
+
/>
|
|
765
|
+
</span>
|
|
766
|
+
{/* One short status, muted: the long form is in the tooltips. */}
|
|
767
|
+
<span className="tl-path-hint">
|
|
768
|
+
{selectedMark
|
|
769
|
+
? ko("Eases back to level at the next dot · E shows rings on the dot", "다음 점에서 수평으로 돌아와요 · E 키로 점에 회전 링")
|
|
770
|
+
: markIndex === 0 || (markIndex != null && markIndex === marks.length + 1)
|
|
771
|
+
? ko("Ends stay level", "양 끝은 수평")
|
|
772
|
+
: ko("Select a dot to lean the prop there", "점을 선택하면 그 지점에서 기울기")}
|
|
773
|
+
</span>
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
)}
|
|
708
777
|
{path && span && (
|
|
709
778
|
<div className="tl-track objmo sg-row">
|
|
710
779
|
<span className="tl-track-label">{ko("Speed", "속도 곡선")}</span>
|
|
@@ -947,6 +1016,8 @@ export default function Timeline({
|
|
|
947
1016
|
// not rendered anywhere else. The Root path (Waypoint) toggle stays in
|
|
948
1017
|
// every mode: blocking uses it to stage where a subject walks.
|
|
949
1018
|
workflowMode = "motion",
|
|
1019
|
+
// A storyboard works in panels: it has no Prompts lane, chips or + Block.
|
|
1020
|
+
previsMode = null,
|
|
950
1021
|
waypoints = [],
|
|
951
1022
|
pathSpeed = null, // { min, max, warn } in m/s, shown on the 2D Root label
|
|
952
1023
|
badge,
|
|
@@ -1017,6 +1088,11 @@ export default function Timeline({
|
|
|
1017
1088
|
onObjectPathClear,
|
|
1018
1089
|
onObjectTimingGestureStart,
|
|
1019
1090
|
onObjectTimingGestureEnd,
|
|
1091
|
+
objectPathPointIndex = null,
|
|
1092
|
+
onObjectMarkAdd,
|
|
1093
|
+
onObjectMarkLean,
|
|
1094
|
+
onObjectMarkScrubStart,
|
|
1095
|
+
onObjectMarkScrubEnd,
|
|
1020
1096
|
onCameraRailDelete,
|
|
1021
1097
|
onShotSelect,
|
|
1022
1098
|
onShotBoundaryMove,
|
|
@@ -1056,7 +1132,7 @@ export default function Timeline({
|
|
|
1056
1132
|
// The window key/interval handlers register once; the latest callbacks
|
|
1057
1133
|
// are read through a ref so they never go stale mid-playback.
|
|
1058
1134
|
const handlers = useRef({});
|
|
1059
|
-
handlers.current = { onScrub, onAdvance, onStep, onPlayToggle, onWaypointToggle, onMarkerSelect, onMarkerRemove, onRootKeyframeAdd, onPromptAdd, onPromptSelect, onPromptChange, onPromptResize, onPromptMove, onPromptRemove, onIkKeyframeAdd, onIkKeyframeRemove, onPinSelect, onFootSnapToggle, onBodyContactToggle, onCameraMoveSelect, onCameraKeyframeAdd, onCameraKeyframeMove, onCameraKeyframeRemove, onCameraBlockSelect, onCameraBlockChange, onCameraPreview, onCameraRailDrawToggle, onCameraRailDelete, onObjectPathDrawToggle, onObjectPathChange, onObjectPathClear, onObjectTimingGestureStart, onObjectTimingGestureEnd, onShotSelect, onShotBoundaryMove, onShotRename, onShotRemove, onShotDuplicate, onShotCut, onShotSplit, onShotMove, onMotionTrim, onMotionTrimReset, onMotionCut, onMotionSpeedChange, onMotionSegmentRemove, onEditGestureStart };
|
|
1135
|
+
handlers.current = { onScrub, onAdvance, onStep, onPlayToggle, onWaypointToggle, onMarkerSelect, onMarkerRemove, onRootKeyframeAdd, onPromptAdd, onPromptSelect, onPromptChange, onPromptResize, onPromptMove, onPromptRemove, onIkKeyframeAdd, onIkKeyframeRemove, onPinSelect, onFootSnapToggle, onBodyContactToggle, onCameraMoveSelect, onCameraKeyframeAdd, onCameraKeyframeMove, onCameraKeyframeRemove, onCameraBlockSelect, onCameraBlockChange, onCameraPreview, onCameraRailDrawToggle, onCameraRailDelete, onObjectPathDrawToggle, onObjectPathChange, onObjectPathClear, onObjectTimingGestureStart, onObjectTimingGestureEnd, onObjectMarkAdd, onObjectMarkLean, onObjectMarkScrubStart, onObjectMarkScrubEnd, onShotSelect, onShotBoundaryMove, onShotRename, onShotRemove, onShotDuplicate, onShotCut, onShotSplit, onShotMove, onMotionTrim, onMotionTrimReset, onMotionCut, onMotionSpeedChange, onMotionSegmentRemove, onEditGestureStart };
|
|
1060
1136
|
|
|
1061
1137
|
// Trackpad/wheel zoom over the FRAME ruler lane only. React registers
|
|
1062
1138
|
// onWheel as passive, so a synthetic onWheel could never preventDefault —
|
|
@@ -1714,6 +1790,8 @@ export default function Timeline({
|
|
|
1714
1790
|
// The motion-layer track tools (Prompts +, the Full-Body Cut, the trim and
|
|
1715
1791
|
// retime grips) edit the take, so Scene and Camera never render them.
|
|
1716
1792
|
const motionTools = workflowMode === "motion";
|
|
1793
|
+
const promptBlocksHidden = previsMode === "storyboard";
|
|
1794
|
+
const tracks = promptBlocksHidden ? TRACKS.filter((name) => name !== "Prompts") : TRACKS;
|
|
1717
1795
|
const activeShotForCut = shots[activeShotIdx] ?? null;
|
|
1718
1796
|
const canCutShot = Boolean(activeShotForCut && frame > activeShotForCut.startFrame && frame <= activeShotForCut.endFrame);
|
|
1719
1797
|
// A trim or retime grip edits ONE segment: it belongs to the segment the
|
|
@@ -1775,7 +1853,7 @@ export default function Timeline({
|
|
|
1775
1853
|
>
|
|
1776
1854
|
{ko("Cut", "컷")}
|
|
1777
1855
|
</button>
|
|
1778
|
-
<button
|
|
1856
|
+
{!promptBlocksHidden && <button
|
|
1779
1857
|
type="button"
|
|
1780
1858
|
className="tl-seq-action"
|
|
1781
1859
|
data-testid="sequencer-add-block"
|
|
@@ -1783,7 +1861,7 @@ export default function Timeline({
|
|
|
1783
1861
|
onClick={() => handlers.current.onPromptAdd?.(frame)}
|
|
1784
1862
|
>
|
|
1785
1863
|
{ko("+ Block", "+ 블록")}
|
|
1786
|
-
</button>
|
|
1864
|
+
</button>}
|
|
1787
1865
|
</>) : workflowMode === "pose" || ikMode ? (
|
|
1788
1866
|
<button
|
|
1789
1867
|
type="button"
|
|
@@ -1914,7 +1992,7 @@ export default function Timeline({
|
|
|
1914
1992
|
type="button"
|
|
1915
1993
|
className="tl-btn clear"
|
|
1916
1994
|
aria-label={ko("Clear loaded motion", "불러온 모션 지우기")}
|
|
1917
|
-
title={ko("Clear motion and restore the blocking pose", "
|
|
1995
|
+
title={ko("Clear motion and prompt blocks, then restore the blocking pose", "모션과 프롬프트 블록을 지우고 블로킹 포즈로 되돌리기")}
|
|
1918
1996
|
onClick={onClearMotion}
|
|
1919
1997
|
>
|
|
1920
1998
|
✕ {ko("Clear motion", "모션 지우기")}
|
|
@@ -1970,7 +2048,7 @@ export default function Timeline({
|
|
|
1970
2048
|
)}
|
|
1971
2049
|
|
|
1972
2050
|
<div className="tl-body" ref={bodyRef}>
|
|
1973
|
-
<div className={"tl-surface" + (!shots.length ? " empty-shots" : "")} style={{ "--tl-zoom": surfaceZoom }}>
|
|
2051
|
+
<div className={"tl-surface" + (!shots.length ? " empty-shots" : "") + (pathObject ? " prop-subject" : "")} style={{ "--tl-zoom": surfaceZoom }}>
|
|
1974
2052
|
<div className="tl-ruler">
|
|
1975
2053
|
<span className="tl-ruler-label">{ko("Frame", "프레임")}</span>
|
|
1976
2054
|
<div
|
|
@@ -2024,8 +2102,13 @@ export default function Timeline({
|
|
|
2024
2102
|
onPathClear={() => handlers.current.onObjectPathClear?.()}
|
|
2025
2103
|
onTimingGestureStart={() => handlers.current.onObjectTimingGestureStart?.()}
|
|
2026
2104
|
onTimingGestureEnd={() => handlers.current.onObjectTimingGestureEnd?.()}
|
|
2105
|
+
markIndex={objectPathPointIndex}
|
|
2106
|
+
onMarkAdd={(t) => handlers.current.onObjectMarkAdd?.(t)}
|
|
2107
|
+
onMarkLean={(key, value, token) => handlers.current.onObjectMarkLean?.(key, value, token)}
|
|
2108
|
+
onMarkScrubStart={(...args) => handlers.current.onObjectMarkScrubStart?.(...args)}
|
|
2109
|
+
onMarkScrubEnd={(...args) => handlers.current.onObjectMarkScrubEnd?.(...args)}
|
|
2027
2110
|
/>
|
|
2028
|
-
) :
|
|
2111
|
+
) : tracks.map((name) => (
|
|
2029
2112
|
<div className={"tl-track" + (name === "Prompts" ? " prompts" : "") + (name === IK_LANE ? " ik" : "") + (name === SHOTS_LANE ? " shots" : "")} data-track-id={name} key={name}>
|
|
2030
2113
|
<span className="tl-track-label tl-track-tree">
|
|
2031
2114
|
{name === IK_LANE ? (
|
|
@@ -2152,6 +2235,7 @@ export default function Timeline({
|
|
|
2152
2235
|
<div
|
|
2153
2236
|
key={shot.id}
|
|
2154
2237
|
className={"tl-shot-block" + (index === cameraBlockIdx ? " selected" : "") + (index === activeShotIdx ? " active" : "") + (movingShotId === shot.id ? " moving" : "") + (!railOff && railRange && railLengthByShot.has(shot.id) ? " has-dolly" : "")}
|
|
2238
|
+
data-shot-kind={shot.kind === "still" ? "still" : undefined}
|
|
2155
2239
|
style={{ "--tl-f-start": geometry.startPct, "--tl-f-end": geometry.endPct }}
|
|
2156
2240
|
title={isKo ? `${shot.name} · ${shot.startFrame}–${lastFrame}프레임 · 드래그해 순서 이동, 양끝으로 컷 조절, 아래 빈 줄을 클릭해 카메라 키 추가` : `${shot.name} · frames ${shot.startFrame}–${lastFrame} · drag to reorder, trim cuts at either edge, click the empty lower strip to add a camera key`}
|
|
2157
2241
|
onPointerDown={(e) => beginShotMove(e, shot, index)}
|
|
@@ -2191,7 +2275,7 @@ export default function Timeline({
|
|
|
2191
2275
|
onClick={(e) => e.stopPropagation()}
|
|
2192
2276
|
onBlur={(e) => finishShotRename(shot, index, e.target.value)}
|
|
2193
2277
|
onKeyDown={(e) => {
|
|
2194
|
-
if (e.key === "Enter") e.currentTarget.blur();
|
|
2278
|
+
if (e.key === "Enter" && !isImeComposing(e)) e.currentTarget.blur();
|
|
2195
2279
|
if (e.key === "Escape") setRenamingShotId(null);
|
|
2196
2280
|
}}
|
|
2197
2281
|
/>
|
|
@@ -2199,6 +2283,7 @@ export default function Timeline({
|
|
|
2199
2283
|
<span className="tl-shot-label">
|
|
2200
2284
|
<b>{shot.name}</b>
|
|
2201
2285
|
<small>{shot.startFrame}–{lastFrame} · {durationS.toFixed(1)}{ko("s", "초")}</small>
|
|
2286
|
+
{shot.kind === "still" && shot.caption && <small className="tl-shot-caption" title={shot.caption}>{shot.caption}</small>}
|
|
2202
2287
|
</span>
|
|
2203
2288
|
)}
|
|
2204
2289
|
{modelWarnings.length > 0 && (
|
package/src/ardy/to-cskel27.js
CHANGED
|
@@ -140,7 +140,6 @@ export function poseToCskel27({ pose, rest, reference }) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
// Every joint CozyClay authors must be present and a unit quaternion.
|
|
144
143
|
for (const bone of COZYCLAY_BONES) {
|
|
145
144
|
if (!(bone in pose.bones)) {
|
|
146
145
|
throw new Error(`poseToCskel27: pose.bones is missing "${bone}"`);
|
|
@@ -198,7 +197,6 @@ export function poseToCskel27({ pose, rest, reference }) {
|
|
|
198
197
|
}
|
|
199
198
|
}
|
|
200
199
|
|
|
201
|
-
// Every emitted matrix must be a real rotation.
|
|
202
200
|
for (let index = 0; index < CSKEL27_JOINTS.length; index += 1) {
|
|
203
201
|
requireRotationMatrix(
|
|
204
202
|
local_rot_mats[index],
|
package/src/ardy/waypoints.js
CHANGED
|
@@ -93,7 +93,6 @@ export function densifyArdyWaypoints(waypoints, actorRotationDeg = 0, maxPoints
|
|
|
93
93
|
for (let i = 0; i < gaps.length; i += 1) gapSizes[i] = base[i];
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// Sample frames: authored frames plus evenly spaced integer fillers.
|
|
97
96
|
const frames = [];
|
|
98
97
|
for (let i = 0; i < authored.length; i += 1) {
|
|
99
98
|
if (i > 0) {
|
|
@@ -243,8 +242,6 @@ export function poseConstraintFrame(requestedFrame, clipFrames, hasRootPath) {
|
|
|
243
242
|
return Math.max(0, Math.min(Math.round(requestedFrame) || 0, clipFrames - 1));
|
|
244
243
|
}
|
|
245
244
|
|
|
246
|
-
/* ---------------------------------------------------- path limits ------ */
|
|
247
|
-
|
|
248
245
|
// What the generator can actually express, learned the hard way: waypoints
|
|
249
246
|
// are inpainting observations ARDY cannot refuse, so a request outside the
|
|
250
247
|
// training distribution collapses into foot-sliding instead of erroring.
|