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.
Files changed (256) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +17 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/bin/agent/agent-routes.mjs +211 -36
  5. package/bin/agent/agent-runner.mjs +98 -4
  6. package/bin/agent/codex-client.mjs +2 -1
  7. package/bin/agent/fal-contract.mjs +0 -17
  8. package/bin/agent/h3-preservation.mjs +6 -8
  9. package/bin/agent/provider-keys.mjs +1 -1
  10. package/bin/agent/providers.mjs +124 -3
  11. package/bin/agent/script-tool.mjs +74 -0
  12. package/bin/agent/session-store.mjs +5 -5
  13. package/bin/agent/studio-prompt.mjs +2 -1
  14. package/bin/agent/studio-rebase.mjs +314 -0
  15. package/bin/agent/studio-tools.mjs +21 -14
  16. package/bin/agent/supervisor.mjs +311 -0
  17. package/bin/agent/video-adapters.mjs +0 -7
  18. package/bin/codex-auth.mjs +4 -4
  19. package/bin/live/cli.mjs +2 -3
  20. package/bin/live/client.mjs +1 -1
  21. package/bin/live/studio.mjs +5 -2
  22. package/bin/mcp-runtime.mjs +2 -2
  23. package/bin/telemetry-state.mjs +3 -3
  24. package/dist/app/index.html +6 -6
  25. package/dist/assets/analytics-B41FmPYI.js +1 -0
  26. package/dist/assets/app-Do-Og5Pu.css +1 -0
  27. package/dist/assets/app-DwP-tl2x.js +4920 -0
  28. package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
  29. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
  30. package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
  31. package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
  32. package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
  33. package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
  34. package/dist/assets/workflow-Br-K7Srs.js +179 -0
  35. package/dist/cozyclay-package.json +1 -1
  36. package/dist/index.html +328 -222
  37. package/dist/sitemap.xml +1 -1
  38. package/dist/workflow/index.html +4 -4
  39. package/mcp/LIVE-PROTOCOL.md +23 -2
  40. package/mcp/README.md +37 -2
  41. package/mcp/ardy-prompts.mjs +1 -1
  42. package/mcp/image-file.mjs +132 -0
  43. package/mcp/live-hub.mjs +2 -2
  44. package/mcp/package.json +1 -0
  45. package/mcp/runtime/package-lock.json +7 -0
  46. package/mcp/runtime/package.json +1 -0
  47. package/mcp/server.mjs +0 -6
  48. package/mcp/tool-handlers.mjs +289 -87
  49. package/package.json +11 -2
  50. package/src/App.jsx +789 -263
  51. package/src/agent-image-client.js +94 -0
  52. package/src/analytics.js +47 -3
  53. package/src/app-stage.jsx +129 -2745
  54. package/src/ardy/auto-physics.js +6 -16
  55. package/src/ardy/collision-blockers.js +3 -7
  56. package/src/ardy/fix-collisions.js +5 -35
  57. package/src/ardy/ik-drag.js +86 -5
  58. package/src/ardy/ik-key-json.js +3 -1
  59. package/src/ardy/ik.js +15 -13
  60. package/src/ardy/motion-calibration.js +1 -1
  61. package/src/ardy/npz.js +0 -8
  62. package/src/ardy/physics-panel.jsx +3 -2
  63. package/src/ardy/physics-review.js +3 -2
  64. package/src/ardy/physics-support.js +1 -1
  65. package/src/ardy/physics-surface.js +1 -1
  66. package/src/ardy/platform-fit-panel.jsx +2 -1
  67. package/src/ardy/playback.js +1 -5
  68. package/src/ardy/range-pin.js +2 -2
  69. package/src/ardy/root-drop.js +0 -1
  70. package/src/ardy/timeline.css +184 -6
  71. package/src/ardy/timeline.jsx +113 -28
  72. package/src/ardy/to-cskel27.js +0 -2
  73. package/src/ardy/waypoints.js +0 -3
  74. package/src/ardy-bridge-contract.js +281 -0
  75. package/src/asset-pane.jsx +19 -9
  76. package/src/auto-color.js +1 -1
  77. package/src/burn-in.js +1 -1
  78. package/src/camera-block.js +1 -1
  79. package/src/camera-follow.js +75 -32
  80. package/src/camera-move.js +0 -6
  81. package/src/camera-tutorial.jsx +6 -7
  82. package/src/character-kind.js +79 -0
  83. package/src/command-bus.js +76 -17
  84. package/src/commands/cast.js +31 -7
  85. package/src/commands/elements/object.js +1 -1
  86. package/src/commands/elements/shot.js +1 -1
  87. package/src/commands/elements.js +1 -1
  88. package/src/commands/export.js +3 -3
  89. package/src/commands/motion.js +2 -2
  90. package/src/commands/objects.js +105 -10
  91. package/src/commands/project.js +15 -2
  92. package/src/commands/sculpt-images.js +53 -0
  93. package/src/commands/shot.js +88 -10
  94. package/src/commands/view.js +10 -4
  95. package/src/coplanar-depth.js +216 -0
  96. package/src/cuts.js +7 -6
  97. package/src/domains/cast.js +60 -43
  98. package/src/domains/motion/motion-intent-codec.js +50 -0
  99. package/src/domains/motion/motion-range-pin-rules.js +23 -0
  100. package/src/domains/motion/motion-video-capture.js +664 -0
  101. package/src/domains/motion.js +253 -742
  102. package/src/domains/objects.js +106 -5
  103. package/src/domains/scenes.js +79 -28
  104. package/src/domains/shots.js +33 -7
  105. package/src/error-boundary.jsx +1 -1
  106. package/src/execution-telemetry.js +5 -5
  107. package/src/facing-marks.js +14 -1
  108. package/src/first-success-guide.jsx +35 -4
  109. package/src/gizmo-claim.js +3 -1
  110. package/src/grid-floor.jsx +1 -4
  111. package/src/hierarchy-model.js +48 -35
  112. package/src/hierarchy-panel.css +1 -0
  113. package/src/hierarchy-panel.jsx +54 -102
  114. package/src/history.js +0 -2
  115. package/src/i2v-motion-client.js +1 -4
  116. package/src/ime.js +6 -0
  117. package/src/keyframe-pack.js +15 -6
  118. package/src/line-edit.js +4 -4
  119. package/src/live-control.js +6 -6
  120. package/src/matte-editor.js +2 -8
  121. package/src/matte.js +0 -19
  122. package/src/mesh-graph-clone.js +16 -2
  123. package/src/mesh-sniff.js +0 -1
  124. package/src/monotone-cubic.js +40 -0
  125. package/src/motion-store.js +4 -4
  126. package/src/motion-trail.js +5 -11
  127. package/src/multimodel-ingest.js +1 -3
  128. package/src/object-catalog.jsx +7 -5
  129. package/src/object-gizmo.jsx +45 -31
  130. package/src/object-path.js +310 -67
  131. package/src/object-travel.js +224 -0
  132. package/src/otio.js +8 -3
  133. package/src/panels/CharacterTransformPanel.jsx +24 -6
  134. package/src/panels/ObjectTransformPanel.jsx +4 -3
  135. package/src/panels/PoseImagePanel.jsx +205 -0
  136. package/src/panels/PromptBlocksPanel.jsx +3 -1
  137. package/src/panels/ProxyFigurePanel.jsx +56 -0
  138. package/src/panels/ReferenceImageField.jsx +1 -1
  139. package/src/panels/RigControlPanel.jsx +24 -3
  140. package/src/panels/RigPanel.jsx +2 -1
  141. package/src/panels/SculptPanel.jsx +405 -0
  142. package/src/panels/SubjectBox.jsx +1 -1
  143. package/src/panels/SubjectsPanel.jsx +0 -2
  144. package/src/panels/VideoCapturePanel.jsx +2 -1
  145. package/src/panels/pose-image.css +42 -0
  146. package/src/panels/pose.css +3 -0
  147. package/src/panels/sculpt.css +162 -0
  148. package/src/plan-diagram.js +184 -0
  149. package/src/planview.jsx +127 -80
  150. package/src/playground.js +3 -5
  151. package/src/pose-fade.js +77 -0
  152. package/src/pose-gizmo.css +23 -0
  153. package/src/pose-gizmo.js +96 -0
  154. package/src/posestudio.jsx +134 -83
  155. package/src/project-browser.css +53 -0
  156. package/src/project-browser.jsx +49 -4
  157. package/src/project.js +55 -24
  158. package/src/props.jsx +177 -36
  159. package/src/proxy-figure.jsx +69 -0
  160. package/src/range-pin-panel.jsx +0 -1
  161. package/src/receipt-findings.js +20 -0
  162. package/src/render-passes.js +0 -1
  163. package/src/resource-status.jsx +1 -1
  164. package/src/root-path.js +48 -0
  165. package/src/route-owner.js +52 -0
  166. package/src/scene-asset-cache.js +1 -2
  167. package/src/scene-assets.js +12 -4
  168. package/src/scene-fbx.js +1 -1
  169. package/src/scene-mesh-cache.js +0 -1
  170. package/src/scene-objects.js +319 -19
  171. package/src/scenes.js +58 -9
  172. package/src/sculpt-blueprint.js +65 -0
  173. package/src/sculpt-compare.js +384 -0
  174. package/src/sculpt-geometry.js +141 -0
  175. package/src/sculpt-mode.js +168 -0
  176. package/src/sculpt-recipe.js +385 -0
  177. package/src/sculpt-run.js +351 -0
  178. package/src/sculpt-stage.jsx +221 -0
  179. package/src/settings-menu.jsx +1 -1
  180. package/src/shell/BoardSlot.jsx +341 -0
  181. package/src/shell/BottomDock.jsx +58 -14
  182. package/src/shell/DetailsSlot.jsx +26 -6
  183. package/src/shell/MenuBar.jsx +78 -6
  184. package/src/shell/OutlinerSlot.jsx +3 -1
  185. package/src/shell/TopBar.jsx +17 -3
  186. package/src/shell/ViewportToolbar.jsx +18 -3
  187. package/src/shell/dock.css +239 -0
  188. package/src/shell/glass-regions.css +3 -2
  189. package/src/shell/log-store.js +4 -4
  190. package/src/shell/mode.css +20 -0
  191. package/src/shell/studio-shell-context.js +4 -1
  192. package/src/shot-authoring.js +69 -6
  193. package/src/stable-items.js +1 -1
  194. package/src/stage-attach-frames.js +169 -0
  195. package/src/stage-canvas-rigs.jsx +402 -0
  196. package/src/stage-capture.jsx +216 -0
  197. package/src/stage-characters.jsx +302 -0
  198. package/src/stage-editor-gizmos.jsx +1229 -0
  199. package/src/stage-labels-ko.js +204 -0
  200. package/src/stage-startup-defaults.js +182 -0
  201. package/src/starter-scenes.js +1 -1
  202. package/src/storyboard.js +24 -5
  203. package/src/studio-actions.js +42 -9
  204. package/src/studio-agent-commands.js +186 -23
  205. package/src/studio-agent-context.js +11 -9
  206. package/src/studio-agent-motion.js +2 -1
  207. package/src/studio-agent-protocol.js +56 -23
  208. package/src/studio-app-binding.js +52 -23
  209. package/src/studio-contact-sheet.js +3 -3
  210. package/src/studio-elements.js +11 -2
  211. package/src/studio-geometry-facts.js +114 -0
  212. package/src/styles.css +92 -9
  213. package/src/ui-scrub.js +4 -4
  214. package/src/ui.jsx +6 -4
  215. package/src/use-asset-shelf.js +60 -0
  216. package/src/use-camera-tutorial.js +50 -0
  217. package/src/use-embed-bridge.js +12 -0
  218. package/src/use-first-success-prompt.js +7 -0
  219. package/src/use-i2v-capture-modal.js +15 -0
  220. package/src/use-sculpt-mode.js +95 -0
  221. package/src/workflow/AgentPanel.jsx +106 -18
  222. package/src/workflow/WorkflowBuilder.jsx +1 -1
  223. package/src/workflow/agent-client.js +113 -45
  224. package/src/workflow/agent-panel.css +35 -0
  225. package/src/workflow/attachment-image.js +1 -1
  226. package/src/workflow/canvas-commands.js +3 -3
  227. package/src/workflow/cozy-scene-node.js +3 -3
  228. package/src/workflow/local-workflow.js +2 -2
  229. package/src/workflow/motion-input.js +3 -3
  230. package/src/workflow/node-schema.js +1 -1
  231. package/src/workflow/scene-asset-sync.js +2 -9
  232. package/src/workflow/shot-prompt-node.js +1 -1
  233. package/src/workflow/video-contract.js +1 -1
  234. package/src/workflow/workflow-resources.js +0 -8
  235. package/src/workflow-send.js +87 -0
  236. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  237. package/tools/check-eslint.mjs +57 -0
  238. package/tools/check-knip.mjs +60 -0
  239. package/tools/check-loc.mjs +54 -0
  240. package/tools/demo-worker/api-client.mjs +7 -13
  241. package/tools/demo-worker/generate.mjs +2 -4
  242. package/tools/eslint-baseline.json +23 -0
  243. package/tools/kimodo/effector-constraints.mjs +4 -4
  244. package/tools/kimodo/generate.mjs +1 -1
  245. package/tools/knip-baseline.json +331 -0
  246. package/tools/loc-baseline.json +126 -0
  247. package/tools/process-supervisor.mjs +1 -1
  248. package/tools/projflow/replay.mjs +4 -4
  249. package/tools/projflow/runner.mjs +3 -3
  250. package/tools/qa/studio-control-count.mjs +2 -0
  251. package/tools/run-tests.mjs +94 -7
  252. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  253. package/dist/assets/app-BxBLC6GT.js +0 -4876
  254. package/dist/assets/app-CJdxvnrl.css +0 -1
  255. package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
  256. package/dist/assets/workflow-B5XZwVfe.js +0 -179
@@ -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
- export function scaledOffsets(prep, boneScale) {
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);
@@ -20,7 +20,7 @@ import { createIkState, ikBakeKeyframe, solveHipsTranslateToFloor, solveIk } fro
20
20
  * | { space: "object", objectId, local: [x,y,z] } }
21
21
  */
22
22
 
23
- export const RANGE_PIN_TRACKS = Object.freeze(["leftHand", "rightHand", "leftFoot", "rightFoot"]);
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
- export class RangePinError extends Error {
38
+ class RangePinError extends Error {
39
39
  constructor(code, message) {
40
40
  super(message);
41
41
  this.name = "RangePinError";
@@ -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,
@@ -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. Hover is kept because the bar is still a child. */
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: calc(100% + 2px);
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
- fill: var(--select, #e8a33d);
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; }
@@ -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
- ) : TRACKS.map((name) => (
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 && (
@@ -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],
@@ -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.