cozyclay 3.0.2 → 3.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 (221) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/INSTALL_FOR_AGENTS.md +146 -0
  3. package/README.md +74 -192
  4. package/THIRD_PARTY_NOTICES.md +0 -21
  5. package/bin/agent/agent-routes.mjs +22 -56
  6. package/bin/agent/agent-runner.mjs +5 -3
  7. package/bin/agent/main-agent.mjs +70 -0
  8. package/bin/agent/main-prompt.mjs +17 -0
  9. package/bin/agent/session-store.mjs +2 -2
  10. package/bin/agent/studio-prompt.mjs +1 -1
  11. package/bin/agent/studio-task-routes.mjs +185 -0
  12. package/bin/agent/studio-tasks.mjs +156 -0
  13. package/bin/agent/studio-tools.mjs +3 -2
  14. package/bin/agent/studio-turn-transaction.mjs +76 -0
  15. package/bin/cozyclay.mjs +5 -7
  16. package/dist/app/index.html +7 -6
  17. package/dist/assets/TaskBoard-DdDXMgZm.js +1 -0
  18. package/dist/assets/app-B2BpfCl2.js +5088 -0
  19. package/dist/assets/app-BlwOpest.css +1 -0
  20. package/dist/assets/compile-worker-D0pKioa0.js +5 -0
  21. package/dist/assets/landing-y3EqDgSZ.js +1 -0
  22. package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  23. package/dist/assets/motion3-compile-B887n77J.js +1 -0
  24. package/dist/assets/scene-asset-sync-COGOlF5T.css +1 -0
  25. package/dist/assets/scene-asset-sync-CQzEdsqu.js +13 -0
  26. package/dist/assets/task-board-BKaM7d-9.js +14 -0
  27. package/dist/assets/workflow-B8Dhzexj.js +179 -0
  28. package/dist/assets/x-bot-tpose-body-BusyR3eg.json +1 -0
  29. package/dist/assets/y-bot-tpose-body-Dy1VKChZ.json +1 -0
  30. package/dist/cozyclay-package.json +1 -1
  31. package/dist/greybox-to-video/index.html +2 -3
  32. package/dist/index.html +7 -57
  33. package/dist/llms.txt +2 -1
  34. package/dist/previs-software/index.html +2 -2
  35. package/dist/privacy/index.html +3 -7
  36. package/dist/seedance-camera-control/index.html +2 -2
  37. package/dist/sitemap.xml +5 -5
  38. package/dist/sw.js +5 -1
  39. package/dist/thumbs/blank-stage-dark.webp +0 -0
  40. package/dist/thumbs/blank-stage-light.webp +0 -0
  41. package/dist/workflow/index.html +5 -4
  42. package/mcp/mesh-file.mjs +3 -1
  43. package/mcp/server.mjs +3 -0
  44. package/package.json +3 -3
  45. package/src/App.jsx +50 -281
  46. package/src/analytics.js +3 -148
  47. package/src/app-stage.jsx +1 -1
  48. package/src/asset-pane.jsx +5 -10
  49. package/src/auto-flat.js +10 -0
  50. package/src/bus-task-scopes.js +69 -0
  51. package/src/bus-task-targets.js +48 -0
  52. package/src/bus-tasks.js +181 -0
  53. package/src/command-bus.js +15 -40
  54. package/src/command-history.js +36 -0
  55. package/src/commands/cast.js +10 -3
  56. package/src/commands/motion.js +2 -1
  57. package/src/commands/motion3-compile.js +51 -27
  58. package/src/commands/motion3.js +13 -7
  59. package/src/commands/project.js +1 -1
  60. package/src/commands/shared.js +9 -0
  61. package/src/commands/shot.js +16 -4
  62. package/src/controls.jsx +0 -11
  63. package/src/cuts.js +11 -0
  64. package/src/domains/cast.js +3 -2
  65. package/src/domains/motion.js +3 -10
  66. package/src/domains/playback.js +32 -0
  67. package/src/domains/scenes.js +3 -22
  68. package/src/domains/shots.js +5 -5
  69. package/src/dualview.jsx +11 -15
  70. package/src/hierarchy-panel.css +18 -4
  71. package/src/hierarchy-panel.jsx +12 -11
  72. package/src/kits/held.js +3 -2
  73. package/src/light-blockers.js +2 -2
  74. package/src/main.jsx +5 -0
  75. package/src/motion/ground.js +5 -14
  76. package/src/motion/playback-frame-store.js +79 -0
  77. package/src/motion/playback-probe.js +16 -0
  78. package/src/motion/playback-subject.js +7 -0
  79. package/src/motion/timeline.css +43 -5
  80. package/src/motion/timeline.jsx +4 -3
  81. package/src/motion3/body.js +37 -5
  82. package/src/motion3/carried.js +11 -3
  83. package/src/motion3/compile-job.js +1 -1
  84. package/src/motion3/compiler/anchors.js +33 -5
  85. package/src/motion3/compiler/compile.js +27 -4
  86. package/src/motion3/compiler/finger-close.js +137 -0
  87. package/src/motion3/compiler/grip.js +9 -3
  88. package/src/motion3/compiler/limbs.js +18 -3
  89. package/src/motion3/compiler/pose.js +6 -0
  90. package/src/motion3/compiler/socket-grip.js +34 -10
  91. package/src/motion3/compiler/validate-objects.js +17 -2
  92. package/src/motion3/compiler/validate-parts.js +2 -1
  93. package/src/motion3/compiler/vocab.js +4 -4
  94. package/src/motion3/describe.js +2 -2
  95. package/src/motion3/docs.js +1 -1
  96. package/src/motion3/fix/index.js +7 -3
  97. package/src/motion3/flight.js +38 -0
  98. package/src/motion3/free-motion.js +10 -7
  99. package/src/motion3/load.js +125 -0
  100. package/src/motion3/mass.js +23 -0
  101. package/src/motion3/object-keys.js +11 -6
  102. package/src/motion3/object-pose.js +11 -3
  103. package/src/motion3/pipeline.js +5 -0
  104. package/src/motion3/qa/explain.js +3 -1
  105. package/src/motion3/qa/grasp-checks.js +123 -0
  106. package/src/motion3/qa/hints-grasp.js +71 -0
  107. package/src/motion3/qa/index.js +3 -1
  108. package/src/motion3/qa/object-checks.js +7 -2
  109. package/src/motion3/qa/patch.js +2 -1
  110. package/src/motion3/qa/spec-checks.js +21 -1
  111. package/src/motion3/schema/fields.js +3 -3
  112. package/src/motion3/schema/topics.js +11 -10
  113. package/src/motion3/schema/types.js +5 -1
  114. package/src/motion3/seat.js +84 -0
  115. package/src/motion3/settle.js +11 -6
  116. package/src/motion3/shapes.js +70 -0
  117. package/src/motion3/skills/basic.js +61 -13
  118. package/src/motion3/skills/catch.js +71 -0
  119. package/src/motion3/skills/index.js +13 -4
  120. package/src/motion3/skills/info.js +14 -1
  121. package/src/motion3/skills/reach-human.js +139 -0
  122. package/src/motion3/skills/stance.js +92 -0
  123. package/src/motion3/skills/throw.js +72 -0
  124. package/src/motion3/sockets.js +21 -4
  125. package/src/motion3/views.js +28 -5
  126. package/src/object-catalog.jsx +0 -4
  127. package/src/object-gizmo.jsx +2 -1
  128. package/src/object-path.js +1 -1
  129. package/src/panels/ProjectPanel.jsx +42 -21
  130. package/src/panels/details.css +15 -4
  131. package/src/popover.js +37 -0
  132. package/src/posestudio.jsx +15 -10
  133. package/src/project-browser-art.jsx +19 -0
  134. package/src/project-browser.css +74 -121
  135. package/src/project-browser.jsx +36 -60
  136. package/src/props.jsx +4 -7
  137. package/src/retired-kinds.js +37 -0
  138. package/src/scene-objects.js +3 -7
  139. package/src/scenes.js +4 -5
  140. package/src/settings-menu.jsx +1 -20
  141. package/src/shell/BottomDock.jsx +9 -9
  142. package/src/shell/MenuBar.jsx +1 -9
  143. package/src/shell/PreferencesDialog.jsx +3 -4
  144. package/src/shell/ViewportToolbar.jsx +3 -3
  145. package/src/shell/add-camera-shot.js +11 -0
  146. package/src/shell/glass-regions.css +3 -2
  147. package/src/shell/glass.css +0 -5
  148. package/src/shell/lens-scrub-math.js +51 -0
  149. package/src/shell/lens-scrub.css +73 -0
  150. package/src/shell/lens-scrub.jsx +98 -0
  151. package/src/shell/mode.css +6 -5
  152. package/src/shell/preferences.css +1 -32
  153. package/src/shell/shell.css +18 -1
  154. package/src/shell/studio-shell-context.js +5 -0
  155. package/src/shot-frame.js +26 -0
  156. package/src/stage-canvas-rigs.jsx +7 -8
  157. package/src/stage-characters.jsx +26 -0
  158. package/src/stage-gizmo.js +43 -0
  159. package/src/stage-labels-ko.js +0 -6
  160. package/src/stage-startup-defaults.js +3 -9
  161. package/src/starter-scenes.js +5 -11
  162. package/src/studio-agent-commands.js +2 -2
  163. package/src/studio-agent-protocol.js +2 -7
  164. package/src/studio-app-binding.js +3 -3
  165. package/src/studio-elements.js +1 -1
  166. package/src/studio-error-codes.js +1 -1
  167. package/src/studio-motion-visual.js +4 -4
  168. package/src/styles/themes.css +13 -0
  169. package/src/styles.css +6 -674
  170. package/src/text/text-object.jsx +1 -1
  171. package/src/ui.jsx +8 -16
  172. package/src/use-embed-bridge.js +3 -6
  173. package/src/use-render-activity.js +3 -1
  174. package/src/view-ghosts.jsx +5 -3
  175. package/src/viewport-visible-rect.js +37 -0
  176. package/src/workflow/AgentPanel.jsx +75 -42
  177. package/src/workflow/AgentProgressCard.jsx +1 -1
  178. package/src/workflow/TaskBoard.jsx +102 -0
  179. package/src/workflow/TaskInline.jsx +114 -0
  180. package/src/workflow/agent-client.js +11 -25
  181. package/src/workflow/agent-panel.css +14 -1
  182. package/src/workflow/attachment-image.js +10 -3
  183. package/src/workflow/attachment-video.js +91 -0
  184. package/src/workflow/task-board.css +98 -0
  185. package/src/workflow/task-board.js +169 -0
  186. package/tools/check-eslint.mjs +2 -1
  187. package/tools/check-knip.mjs +2 -1
  188. package/tools/dev/pages/greybox-to-video.html +2 -3
  189. package/tools/dev/pages/previs-software.html +2 -2
  190. package/tools/dev/pages/privacy.html +3 -7
  191. package/tools/dev/pages/seedance-camera-control.html +2 -2
  192. package/tools/dev-full.mjs +11 -2
  193. package/tools/eslint-baseline.json +6 -6
  194. package/tools/knip-baseline.json +0 -11
  195. package/tools/loc-baseline.json +30 -31
  196. package/tools/metrics-snapshot.mjs +43 -5
  197. package/tools/render-start-thumbs.mjs +175 -0
  198. package/tools/run-tests.mjs +7 -14
  199. package/tools/sitemap.mjs +1 -1
  200. package/dist/assets/analytics-DbKG0NgD.js +0 -6
  201. package/dist/assets/app-34i-iUy1.js +0 -5088
  202. package/dist/assets/app-f-fkV5uP.css +0 -1
  203. package/dist/assets/compile-worker-DYpa2nvO.js +0 -5
  204. package/dist/assets/first-shot-handoff-CrGZ3n_O.js +0 -1
  205. package/dist/assets/landing-mq5bnqFE.js +0 -1
  206. package/dist/assets/scene-asset-sync-7J-w5oNh.js +0 -20
  207. package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
  208. package/dist/assets/workflow-dUhurYUc.js +0 -179
  209. package/dist/scenes/city-block.cclayproject +0 -1
  210. package/src/burn-in.js +0 -91
  211. package/src/camera-tutorial.jsx +0 -520
  212. package/src/facing-marks.js +0 -88
  213. package/src/first-shot-handoff.js +0 -44
  214. package/src/panels/motion.css +0 -197
  215. package/src/set-pieces.jsx +0 -183
  216. package/src/tutorial-analytics.js +0 -86
  217. package/src/tutorial-walk.js +0 -17
  218. package/src/use-camera-tutorial.js +0 -50
  219. package/src/use-semantic-state.js +0 -12
  220. package/tools/metrics/archive-traffic.mjs +0 -138
  221. package/tools/perf/ik-camera-drag.mjs +0 -138
package/src/App.jsx CHANGED
@@ -42,7 +42,6 @@ import { browserSculptImages } from "./commands/sculpt-images.js";
42
42
  import { sculptAgentClient, sculptModelLabel } from "./sculpt-mode.js";
43
43
  import { useSculptMode } from "./use-sculpt-mode.js";
44
44
  import { useFirstSuccessPrompt } from "./use-first-success-prompt.js";
45
- import { useCameraTutorial } from "./use-camera-tutorial.js";
46
45
  import { useEmbedBridge } from "./use-embed-bridge.js";
47
46
  import { useAssetShelf } from "./use-asset-shelf.js";
48
47
  import { StudioProtocolError } from "./studio-agent-protocol.js";
@@ -120,9 +119,6 @@ import {
120
119
  import ProjectBrowser, { ProjectNameDialog } from "./project-browser.jsx";
121
120
  import FirstSuccessGuide from "./first-success-guide.jsx";
122
121
  import UseCaseQuestion from "./use-case-question.jsx";
123
- import { CameraTutorial } from "./camera-tutorial.jsx";
124
- import { createTutorialAnalytics } from "./tutorial-analytics.js";
125
- import { cameraTutorialSuppressed, createFirstShotHandoff, rememberCameraTutorialTerminal } from "./first-shot-handoff.js";
126
122
  import ObjectGizmo from "./object-gizmo.jsx";
127
123
  import { useHeldObjectEdit } from "./held-object-edit.jsx";
128
124
 
@@ -141,7 +137,7 @@ import {
141
137
  pathMetrics,
142
138
  strokeToPathPoints,
143
139
  } from "./object-path.js";
144
- import { carriedPointAt, sceneObjectsAt } from "./object-travel.js"; import { objectTracksOf } from "./motion3/object-tracks.js"; import { tutorialWalkSpec } from "./tutorial-walk.js"; import { ViewGhosts } from "./view-ghosts.jsx"; import { DraftGhost } from "./draft-ghost.jsx";
140
+ import { carriedPointAt, sceneObjectsAt } from "./object-travel.js"; import { objectTracksOf } from "./motion3/object-tracks.js"; import { ViewGhosts } from "./view-ghosts.jsx"; import { DraftGhost } from "./draft-ghost.jsx";
145
141
  import { routeOwnerFor } from "./route-owner.js";
146
142
  import {
147
143
  analyticsActive,
@@ -157,7 +153,7 @@ import {
157
153
  } from "./analytics.js";
158
154
  import { ko, isKo } from "./locale.js";
159
155
  import { characterCapabilities, kindRefusal, proxyFigureBounds } from "./character-kind.js";
160
- import { STARTER_SCENES } from "./starter-scenes.js";
156
+ import { STARTER_SCENES, resolveSceneParam } from "./starter-scenes.js";
161
157
  import { PART_COLOURS } from "./part-colours.js";
162
158
  import {
163
159
  DEFAULT_POSE,
@@ -217,11 +213,14 @@ import {
217
213
  shotAtFrame,
218
214
  shotIndexAtFrame,
219
215
  } from "./cuts.js";
216
+ import { playbackFrame } from "./motion/playback-frame-store.js";
217
+ import { usePlaybackSubjectPosition } from "./motion/playback-subject.js";
218
+ import { characterGizmoObject as buildCharacterGizmoObject, characterGizmoView, followCameraActive, playbackSceneWithTrack, updateShotMetrics } from "./stage-gizmo.js";
220
219
  import {
221
220
  CameraGlide,
222
221
  CameraRailScenePreview,
223
222
  CaptureRig,
224
- Character,
223
+ PlaybackCharacter,
225
224
  ContextLossGuard,
226
225
  CraneHandles,
227
226
  DEFAULT_CAMERA_POSITION,
@@ -381,35 +380,9 @@ export default function App() {
381
380
  getBus: () => studioBindingRef.current.bus,
382
381
  notify: (...args) => setToast(...args),
383
382
  }));
384
- const { embedMode, playgroundMode, playgroundHint, setPlaygroundHint, playgroundExportRef } = useEmbedBridge();
385
- const { cameraTutorialEntry, cameraTutorialQuery, cameraTutorial, setCameraTutorial, cameraTutorialAnalytics, cameraTutorialAttempt, setCameraTutorialAttempt, cameraTutorialHandoff, setCameraTutorialHandoff, cameraTutorialCompletedRef, exportMenuTriggerRef, exportShotIdRef, cameraTutorialStep, setCameraTutorialStep, startCameraTutorialRef, cameraTutorialStarted, tutorialStarterRef, tutorialLoadingRef, tutorialProjectEpochRef, tutorialSeedEpochRef, tutorialSeedPending, setTutorialSeedPending } = useCameraTutorial({ embedMode, playgroundMode });
386
- useEffect(() => {
387
- if (!cameraTutorialQuery || cameraTutorialStarted.current) return;
388
- // Storyboard projects never auto-open the seven-step camera tutorial.
389
- if (scenesDomain.previsMode === "storyboard") return;
390
- cameraTutorialStarted.current = true;
391
- void startCameraTutorialRef.current?.({ source: cameraTutorialEntry });
392
- }, [cameraTutorialQuery, cameraTutorialEntry]);
393
- useEffect(() => {
394
- const onTutorial = (event) => {
395
- if (event.detail?.open === false) {
396
- setCameraTutorial(false);
397
- cameraTutorialAnalytics.current?.dismiss();
398
- rememberCameraTutorialTerminal(cameraTutorialCompletedRef.current ? "completed" : "dismissed");
399
- tutorialProjectEpochRef.current += 1;
400
- setTutorialSeedPending(false);
401
- setCameraTutorialHandoff(null);
402
- return;
403
- }
404
- if (scenesDomain.previsMode === "storyboard") return;
405
- void startCameraTutorialRef.current?.({ source: event.detail?.source ?? "settings" });
406
- };
407
- window.addEventListener("cozyclay:camera-tutorial", onTutorial);
408
- return () => window.removeEventListener("cozyclay:camera-tutorial", onTutorial);
409
- }, []);
410
- useEffect(() => {
411
- if (cameraTutorial) trackFeature("camera_tutorial");
412
- }, [cameraTutorial]);
383
+ const { embedMode, playgroundMode } = useEmbedBridge();
384
+ const exportMenuTriggerRef = useRef(null);
385
+ const exportShotIdRef = useRef(null);
413
386
  useEffect(() => {
414
387
  if (!embedMode) return undefined;
415
388
  // The Workflow page's Scene node embeds the studio as its preview, so the
@@ -472,7 +445,7 @@ export default function App() {
472
445
  if (!firstEditRef.current) {
473
446
  const firstEdit = createFirstEditTracker(track);
474
447
  // Observe the existing semantic boundary once, even after its telemetry
475
- // gate is satisfied. The tutorial/semantic hook itself stays unchanged.
448
+ // gate is satisfied. The semantic hook itself stays unchanged.
476
449
  firstEditRef.current = (surface, domain, before, after) => {
477
450
  if (before !== after) sceneRevisionRef.current += 1;
478
451
  studioBindingRef.current?.invalidate(domain, before, after);
@@ -493,8 +466,6 @@ export default function App() {
493
466
  // One semantic hook for authored UI and programmatic mutations. Passive
494
467
  // setters intentionally bypass it (navigation, load, seed, restore, history).
495
468
  const markSemanticEdit = (domain, before, after) => {
496
- tutorialProjectEpochRef.current += 1;
497
- if (tutorialSeedEpochRef.current !== null) setTutorialSeedPending(false);
498
469
  return firstEditRef.current(playgroundMode ? "playground" : "craft", domain, before, after);
499
470
  };
500
471
  const craftActionTrackedRef = useRef(false);
@@ -506,31 +477,11 @@ export default function App() {
506
477
  track(playgroundMode ? "playground:first_action" : "craft:first_action", { action_kind: actionKind });
507
478
  };
508
479
  useEffect(() => {
509
- if (!playgroundMode) return undefined;
480
+ if (!playgroundMode) return;
510
481
  track("playground:opened");
511
482
  // The landing page keeps a loading veil over the iframe until the
512
483
  // studio has actually mounted; a bare `load` fires far too early.
513
484
  window.parent?.postMessage({ type: "cozyclay:playground-ready" }, "*");
514
- // Camera gestures feed the landing page's tutorial checklist, and the
515
- // checklist points back at one control (the shot look-through) by hint.
516
- const onNav = (event) => window.parent?.postMessage({ type: "cozyclay:playground-nav", kind: event.detail?.kind, key: event.detail?.key ?? null }, "*");
517
- const onSignal = (event) => window.parent?.postMessage({ type: "cozyclay:playground-nav", kind: event.detail?.kind }, "*");
518
- window.addEventListener("cozyclay:playground-signal", onSignal);
519
- const onHint = (event) => {
520
- if (event.source !== window.parent || event.origin !== window.location.origin) return;
521
- if (event.data?.type === "cozyclay:playground-hint") setPlaygroundHint(typeof event.data.kind === "string" ? event.data.kind : null);
522
- if (event.data?.type === "cozyclay:playground-export") {
523
- // The visitor keeps what they made: the landing page turns this
524
- // into a .cclayproject download they can open after npx cozyclay.
525
- playgroundExportRef.current?.("City Block").then(
526
- (serialized) => window.parent?.postMessage({ type: "cozyclay:playground-export-result", serialized }, "*"),
527
- (error) => window.parent?.postMessage({ type: "cozyclay:playground-export-result", error: String(error?.message ?? error) }, "*"),
528
- );
529
- }
530
- };
531
- window.addEventListener("cozyclay:nav", onNav);
532
- window.addEventListener("message", onHint);
533
- return () => { window.removeEventListener("cozyclay:nav", onNav); window.removeEventListener("cozyclay:playground-signal", onSignal); window.removeEventListener("message", onHint); };
534
485
  // eslint-disable-next-line react-hooks/exhaustive-deps
535
486
  }, []);
536
487
  const [startup] = useState(loadSceneStartup);
@@ -649,26 +600,11 @@ export default function App() {
649
600
  // bridge read this global, and the render path is still PlayView's.
650
601
  globalThis.playMode = preview;
651
602
  const playMode = preview;
652
- // #441 moved shot authoring into commands and dropped the Shot and Rail
653
- // signals the landing checklist waits for. Read them from the authored state,
654
- // as the studio tutorial does (#586): a shot added here, and a rail drawn.
655
- // Declared before the play signal: a drawn rail starts playback in the same
656
- // commit, and the checklist expects Rail before Play.
657
- const playgroundShotCount = useRef(null);
658
- useEffect(() => {
659
- if (!playgroundMode) return;
660
- if (playgroundShotCount.current !== null && shots.length > playgroundShotCount.current) {
661
- window.parent?.postMessage({ type: "cozyclay:playground-nav", kind: "shot" }, "*");
662
- }
663
- playgroundShotCount.current = shots.length;
664
- }, [playgroundMode, shots.length]);
665
- const playgroundRailReady = playgroundMode && shots.some((shot) => Array.isArray(shot.camera?.cameraRail) && shot.camera.cameraRail.length >= 2);
666
- useEffect(() => {
667
- if (playgroundRailReady) window.parent?.postMessage({ type: "cozyclay:playground-nav", kind: "rail" }, "*");
668
- }, [playgroundRailReady]);
669
603
  // Preview is the player for the finished motion: entering starts playback,
670
604
  // leaving pauses it. The editor view stays the manipulation surface.
671
605
  const [tlPlaying, setTlPlaying] = useState(false);
606
+ // Playback ticks stay outside React; stopping commits the live frame back to
607
+ // the edit-time state so exports and agent reads see the stopped frame.
672
608
  useEffect(() => {
673
609
  if (playgroundMode && tlPlaying) window.parent?.postMessage({ type: "cozyclay:playground-nav", kind: "play" }, "*");
674
610
  }, [playgroundMode, tlPlaying]);
@@ -684,9 +620,6 @@ export default function App() {
684
620
  // The Workflow page embeds this Studio as the Scene node's preview; that
685
621
  // preview must show what the node captures on Run: the shot camera's view.
686
622
  const [lookThroughShot, setLookThroughShot] = useState(embedMode);
687
- useEffect(() => {
688
- if (playgroundMode && lookThroughShot) window.parent?.postMessage({ type: "cozyclay:playground-nav", kind: "shot" }, "*");
689
- }, [playgroundMode, lookThroughShot]);
690
623
  useEffect(() => {
691
624
  if (!lookThroughShot || embedMode) return undefined;
692
625
  const onKey = (event) => {
@@ -1135,7 +1068,8 @@ export default function App() {
1135
1068
  get motionFullRef() { return motionFullRef; },
1136
1069
  get poseMemberAtFrame() { return poseMemberAtFrame; },
1137
1070
  get posedRig() { return posedRig; },
1138
- get poserCamRef() { return poserCamRef; },
1071
+ // Pose mode enters from the view the author is looking through (motion.js toggleIkMode).
1072
+ get poserCamRef() { return poserCamRef; }, get editorCamRef() { return editorCamRef; }, get editorLook() { return editorLook; },
1139
1073
  get poserLook() { return poserLook; },
1140
1074
  get posing() { return posing; },
1141
1075
  get posingChar() { return posingChar; },
@@ -1163,9 +1097,9 @@ export default function App() {
1163
1097
  get tlFrame() { return tlFrame; },
1164
1098
  get tlFrameCount() { return tlFrameCount; },
1165
1099
  get tlFrameRef() { return tlFrameRef; },
1100
+ get propFrameRef() { return propFrameRef; },
1166
1101
 
1167
1102
 
1168
- get tutorialProjectEpochRef() { return tutorialProjectEpochRef; },
1169
1103
  get waitForRig() { return waitForRig; },
1170
1104
  get waypointMode() { return waypointMode; },
1171
1105
  get waypoints() { return waypoints; },
@@ -1392,7 +1326,6 @@ export default function App() {
1392
1326
  const scenesDomain = useScenes(appContext.forRender({
1393
1327
  get activeSceneIdRef() { return activeSceneIdRef; },
1394
1328
  get actorStageRef() { return actorStageRef; },
1395
- get cameraTutorialQuery() { return cameraTutorialQuery; },
1396
1329
  get customPoses() { return customPoses; },
1397
1330
  get dirtyRef() { return dirtyRef; },
1398
1331
  get exportShotIdRef() { return exportShotIdRef; },
@@ -1414,8 +1347,6 @@ export default function App() {
1414
1347
  get saveFailureToastRef() { return saveFailureToastRef; },
1415
1348
  get setActiveCharacterId() { return setActiveCharacterId; },
1416
1349
  get setActiveWaypointId() { return setActiveWaypointId; },
1417
- get setCameraTutorial() { return setCameraTutorial; },
1418
- get setCameraTutorialHandoff() { return setCameraTutorialHandoff; },
1419
1350
  get setFirstSuccessGuideOpen() { return setFirstSuccessGuideOpen; },
1420
1351
  get setHasCharSheet() { return setHasCharSheet; },
1421
1352
  get setMovePlaying() { return setMovePlaying; },
@@ -1428,7 +1359,6 @@ export default function App() {
1428
1359
  get setShots() { return setShots; },
1429
1360
  get setTlFrame() { return setTlFrame; },
1430
1361
  get setTlFrameCount() { return setTlFrameCount; },
1431
- get setTutorialSeedPending() { return setTutorialSeedPending; },
1432
1362
  get setWaypoints() { return setWaypoints; },
1433
1363
  get setWorkspaceLayout() { return setWorkspaceLayout; },
1434
1364
  get shotDocumentRef() { return shotDocumentRef; },
@@ -1439,9 +1369,6 @@ export default function App() {
1439
1369
  get studioDocumentEpochRef() { return studioDocumentEpochRef; },
1440
1370
  get studioHistoryRef() { return studioHistoryRef; },
1441
1371
  get studioSceneEpochRef() { return studioSceneEpochRef; },
1442
- get tutorialProjectEpochRef() { return tutorialProjectEpochRef; },
1443
- get tutorialSeedEpochRef() { return tutorialSeedEpochRef; },
1444
- get tutorialStarterRef() { return tutorialStarterRef; },
1445
1372
  get castDomain() { return castDomain; },
1446
1373
  get objectsDomain() { return objectsDomain; },
1447
1374
  }));
@@ -1821,11 +1748,6 @@ export default function App() {
1821
1748
  const [mode, setMode] = useState("image");
1822
1749
  const [imageModel, setImageModel] = useState("gpt_image_2");
1823
1750
 
1824
- useEffect(() => {
1825
- if (playgroundMode && movePlaying) window.parent?.postMessage({ type: "cozyclay:playground-nav", kind: "play" }, "*");
1826
- // eslint-disable-next-line react-hooks/exhaustive-deps
1827
- }, [playgroundMode, movePlaying]);
1828
-
1829
1751
  // Object travel-path drawing: the same Top-View stroke gesture as the rail,
1830
1752
  // aimed at the selected object instead of the shot camera.
1831
1753
  const [pathDraw, setPathDraw] = useState(false);
@@ -1843,9 +1765,9 @@ export default function App() {
1843
1765
  // The frame props follow. Live playback keeps it at the playhead; the
1844
1766
  // offscreen export drives it per captured frame without re-rendering.
1845
1767
  const propFrameRef = useRef(0);
1846
-
1847
1768
  const [cameraPos, setCameraPos] = useState(DEFAULT_CAMERA_POSITION);
1848
1769
  const [subjectVisible, setSubjectVisible] = useState(true);
1770
+ const subjectVisibleRef = useRef(true);
1849
1771
  const mcpCaptureRef = useRef(null);
1850
1772
  const liveControlRef = useRef(null);
1851
1773
  const [liveWorkspaceHandle, setLiveWorkspaceHandle] = useState(null);
@@ -2058,7 +1980,11 @@ export default function App() {
2058
1980
  }
2059
1981
 
2060
1982
 
2061
- const renderActive = useRenderActivity(tlPlaying || movePlaying);
1983
+ const renderActive = useRenderActivity(tlPlaying || movePlaying, { playing: tlPlaying, commit: shotsDomain.commitPlaybackFrame, onStop: () => {
1984
+ const position = shotCameraPosRef.current;
1985
+ if (position) setCameraPos(position);
1986
+ setSubjectVisible(subjectVisibleRef.current);
1987
+ } });
2062
1988
  // the clip length on the production clock
2063
1989
 
2064
1990
  // Keep the imperative frame in step with the playhead for live playback;
@@ -2348,96 +2274,18 @@ export default function App() {
2348
2274
  };
2349
2275
  }, []);
2350
2276
 
2351
- const tutorialInitialSnapshotRef = useRef(null);
2352
- if (tutorialInitialSnapshotRef.current === null) tutorialInitialSnapshotRef.current = collectProjectSnapshot("Tutorial");
2353
-
2354
- playgroundExportRef.current = collectProjectSerialized;
2355
-
2356
- function closeCameraTutorial(reason = null) {
2357
- const terminal = reason ?? (cameraTutorialCompletedRef.current ? "completed" : "dismissed");
2358
- rememberCameraTutorialTerminal(terminal);
2359
- tutorialProjectEpochRef.current += 1;
2360
- setTutorialSeedPending(false);
2361
- cameraTutorialHandoff?.dismiss();
2362
- setCameraTutorialHandoff(null);
2363
- cameraTutorialAnalytics.current?.dismiss();
2364
- cameraTutorialAnalytics.current = null;
2365
- cameraTutorialCompletedRef.current = false;
2366
- setCameraTutorial(false);
2367
- setCameraTutorialStep(null);
2368
- }
2369
-
2370
- function openExportMenuForShot(shotId) {
2371
- const target = shots.find((entry) => entry.id === shotId);
2372
- if (!target) return;
2373
- exportShotIdRef.current = target.id;
2374
- const trigger = exportMenuTriggerRef.current;
2375
- if (trigger) {
2376
- const box = trigger.getBoundingClientRect();
2377
- const menuWidth = Math.min(340, window.innerWidth - 16);
2378
- setExportMenuAnchor({
2379
- top: box.bottom + 6,
2380
- right: Math.min(Math.max(8, window.innerWidth - box.right), Math.max(8, window.innerWidth - menuWidth - 8)),
2381
- });
2382
- }
2383
- setExportMenuOpen(true);
2384
- cameraTutorialHandoff?.dismiss();
2385
- setCameraTutorialHandoff(null);
2386
- }
2387
-
2388
- /** The camera tutorial's single entry (#209), for both /app/?tutorial=camera
2389
- * and the Settings ▾ item.
2390
- *
2391
- * The seven steps teach Shot / Rail / Play, which need a set and somebody
2392
- * moving through it. On cozyclay.org they get both for free: the landing
2393
- * playground opens the city-block starter and the hosted-demo seed below
2394
- * loads the walk take because a statically served build has no motion
2395
- * bridge. A local session HAS a bridge, so that seed is skipped and the
2396
- * tutorial used to open on whatever was loaded — usually an empty room.
2397
- * This puts the studio in the landing page's state explicitly. */
2398
- async function startCameraTutorial({ source = "settings" } = {}) {
2399
- if (embedMode || playgroundMode || tutorialLoadingRef.current) return;
2400
- // QA hook, same spirit as window.__cozyclayRenders: which door the tutorial
2401
- // came in by, so a headless run can prove both of them land here.
2402
- window.__cozyclayTutorialSource = source;
2403
- // Only a pristine, newly created document receives the sample. An
2404
- // existing project (even unnamed), or a restart, keeps all current work.
2405
- const seed = !tutorialStarterRef.current && startupCreatedScene && projectName === null
2406
- && !projectDirty && !cameraTutorialSuppressed()
2407
- && tutorialInitialSnapshotRef.current === collectProjectSnapshot("Tutorial");
2408
- if (seed) {
2409
- tutorialLoadingRef.current = true;
2410
- try {
2411
- const opened = await openStarterScene("city-block", "tutorial");
2412
- if (opened) {
2413
- tutorialStarterRef.current = true;
2414
- tutorialSeedEpochRef.current = tutorialProjectEpochRef.current;
2415
- setTutorialSeedPending(true);
2416
- exitPreview();
2417
- setTlFrame(0);
2418
- }
2419
- } finally {
2420
- tutorialLoadingRef.current = false;
2421
- }
2422
- }
2423
- setProjectStartupOpen(false);
2424
- setFirstSuccessGuideOpen(false);
2425
- setCameraTutorialHandoff(createFirstShotHandoff());
2426
- // Explicit start, including while already open, resets the existing
2427
- // done/walked component state. Passive renders never create an attempt.
2428
- cameraTutorialAnalytics.current = createTutorialAnalytics({ surface: "studio", startSource: source });
2429
- setCameraTutorialAttempt((attempt) => attempt + 1);
2430
- setCameraTutorial(true);
2431
- cameraTutorialCompletedRef.current = false;
2432
- }
2433
- startCameraTutorialRef.current = startCameraTutorial;
2434
-
2435
2277
  const starterOpened = useRef(false);
2436
2278
  useEffect(() => {
2437
2279
  if (starterOpened.current || playgroundMode) return;
2438
2280
  const requested = new URLSearchParams(globalThis.location?.search || "").get("scene");
2439
2281
  if (!requested) return;
2440
2282
  starterOpened.current = true;
2283
+ // An id this build does not ship (a removed starter, a typo) is ignored:
2284
+ // the studio opens as it would without ?scene=.
2285
+ if (!resolveSceneParam(requested)) {
2286
+ console.warn(`cozyclay: ?scene=${requested} is not a starter scene in this build; ignored.`);
2287
+ return;
2288
+ }
2441
2289
  void openStarterScene(requested, "launch");
2442
2290
  // eslint-disable-next-line react-hooks/exhaustive-deps
2443
2291
  }, []);
@@ -2731,6 +2579,8 @@ export default function App() {
2731
2579
  });
2732
2580
  return [{
2733
2581
  id: entry.id,
2582
+ entry,
2583
+ clip,
2734
2584
  model: entry.model,
2735
2585
  ...(isProxyFigure(entry) ? {} : { url: characterModelUrl(entry.model) }),
2736
2586
  position: [placement.x, entry.y ?? 0, placement.z],
@@ -2754,33 +2604,14 @@ export default function App() {
2754
2604
  // character so the motion tab keeps working, but a move gizmo hanging in
2755
2605
  // the viewport while the camera or a prop is selected reads as a stray
2756
2606
  // widget.
2757
- const gizmoView = useMemo(() => {
2758
- if (!charIdFromHierarchyId(selectedHierarchyId)) return null;
2759
- const entry = characters.find((item) => item.id === activeChar.id);
2760
- if (!entry || entry.hidden) return null;
2761
- const hasTake = activeChar.id === activeCharacterId ? Boolean(motion) : Boolean(activeChar.sessionMotion);
2762
- const placement = hasTake ? entry : placementAt(entry, tlFrame, { shotAt: frame => shotAtFrame(shots, frame) });
2763
- return { position: [placement.x, entry.y ?? 0, placement.z], rot: placement.rot };
2764
- }, [characters, activeChar.id, motion, shots, tlFrame, selectedHierarchyId]);
2607
+ const gizmoView = characterGizmoView({
2608
+ selected: Boolean(charIdFromHierarchyId(selectedHierarchyId)), characters, activeCharacterId,
2609
+ activeCharacter: activeChar, motion, shots, activeId: activeChar.id, frame: tlFrame,
2610
+ });
2765
2611
  // The cast rides the SAME gizmo as scene objects — one movement grammar
2766
2612
  // for everything on stage. The proxy hands ObjectGizmo the object shape
2767
2613
  // it expects; `height` puts the pivot at the hips like a prop's centre.
2768
- const characterGizmoObject = useMemo(() => {
2769
- if (!gizmoView) return null;
2770
- const entry = characters.find((item) => item.id === activeChar.id);
2771
- return {
2772
- id: "__character__",
2773
- x: gizmoView.position[0],
2774
- y: gizmoView.position[1],
2775
- z: gizmoView.position[2],
2776
- height: 1.15,
2777
- footprint: { width: 0.6, depth: 0.6 },
2778
- rotY: gizmoView.rot ?? entry?.rot ?? 0,
2779
- scaleX: entry?.scale ?? 1,
2780
- scaleY: entry?.scale ?? 1,
2781
- scaleZ: entry?.scale ?? 1,
2782
- };
2783
- }, [gizmoView, characters, activeChar.id]);
2614
+ const characterGizmoObject = buildCharacterGizmoObject(gizmoView, characters.find((item) => item.id === activeChar.id));
2784
2615
 
2785
2616
  /* --------------------- per-character layer buffers ---------------------
2786
2617
  * waypoints / motion above are the EDITING BUFFER of the
@@ -3263,16 +3094,6 @@ export default function App() {
3263
3094
  );
3264
3095
  }
3265
3096
 
3266
- // Observe the existing export UI boundary, independently of telemetry.
3267
- // The first transition covers ordinary menu exports and fast failures too;
3268
- // progress updates do not write storage on every encoded frame.
3269
- const hasExportAttempt = exportStatus !== null;
3270
- useEffect(() => {
3271
- if (!hasExportAttempt) return;
3272
- rememberCameraTutorialTerminal("export_started");
3273
- setCameraTutorialHandoff(null);
3274
- }, [hasExportAttempt]);
3275
-
3276
3097
  function downloadOtioCutList() {
3277
3098
  if (!shots.length) {
3278
3099
  setToast(ko("Add at least one Shot before exporting OTIO", "OTIO를 내보내려면 샷을 하나 이상 추가하세요"));
@@ -3772,22 +3593,6 @@ export default function App() {
3772
3593
  // eslint-disable-next-line react-hooks/exhaustive-deps
3773
3594
  }, []);
3774
3595
 
3775
- // The camera tutorial's seed (#209). startCameraTutorial arms
3776
- // tutorialSeedPending once the starter scene is applied, and a compiled walk
3777
- // (take v3) goes on as soon as that scene's character has a parsed rig: a
3778
- // state condition, not a timer. The compile runs in the worker; an edit that lands
3779
- // before it answers makes the job's fence refuse the install (set dressing, no error).
3780
- useEffect(() => {
3781
- if (!tutorialSeedPending || !activeRig) return;
3782
- const seedEpoch = tutorialSeedEpochRef.current;
3783
- tutorialSeedEpochRef.current = null;
3784
- setTutorialSeedPending(false);
3785
- if (seedEpoch === null || seedEpoch !== tutorialProjectEpochRef.current) return;
3786
- const spec = tutorialWalkSpec(activeChar); // the take is set dressing, never an error to act on
3787
- if (spec && runStudioAction("motion.compile", { characterId: activeChar.id, spec })) { setTlFrame(0); setTlPlaying(false); }
3788
- // eslint-disable-next-line react-hooks/exhaustive-deps
3789
- }, [tutorialSeedPending, activeRig]);
3790
-
3791
3596
  // Drive every cast member from ITS OWN clip on the shared playhead. The
3792
3597
  // active character's buffer motion and the stored session motions of the
3793
3598
  // others all advance together; characters without a clip keep their pose.
@@ -3868,7 +3673,7 @@ export default function App() {
3868
3673
  // (test/motion3/qa-motion3-*-browser.mjs). Harmless in normal use.
3869
3674
  useEffect(() => {
3870
3675
  window.__cozyclay = {
3871
- runStudioAction, setFrame: setTlFrame,
3676
+ runStudioAction, setFrame: setTlFrame, setPlaying: setTlPlaying,
3872
3677
  rigA: activeRig, motion, tlFrame, frameCount: tlFrameCount, playing: tlPlaying, ikMode, ikChains, ikFocus, contactRadii: ikChains?.values().next().value?.contactRadii ?? null, ik: ikStateRef.current,
3873
3678
  rangePins, rangePinResiduals,
3874
3679
  rangePinApplySpec: motionDomain.rangePinApplySpec,
@@ -4050,11 +3855,7 @@ export default function App() {
4050
3855
  return { x: at.x, z: at.z };
4051
3856
  };
4052
3857
  }, [charA.parent, charA.y, sceneObjects, tlFrameCount, tlFps]);
4053
- const motionPos = useMemo(() => {
4054
- if (!motion && !subjectCarry) return null;
4055
- const subject = sampleAt(playbackSceneBase, null, tlFrame).subject;
4056
- return subjectCarry ? subjectCarry(subject, tlFrame) : subject;
4057
- }, [motion, subjectCarry, playbackSceneBase, tlFrame]);
3858
+ const motionPos = usePlaybackSubjectPosition({ motion, subjectCarry, scene: playbackSceneBase, frame: tlFrame });
4058
3859
 
4059
3860
  // The subject's full per-frame scene trajectory — what the follow camera
4060
3861
  // is derived from. Without a loaded motion, and riding nothing, the subject
@@ -4075,11 +3876,7 @@ export default function App() {
4075
3876
  // A rail shot with an aim holds its cast (or a world point) instead of the subject.
4076
3877
  const followTrack = useShotCameraTracks({ shots, subjectTrack, subjectId: charA.id, fps: tlFps, yawDeg: charA.rot, filmback, characters, objects: sceneObjects, frameCount: tlFrameCount, activeId: activeChar.id, motion });
4077
3878
 
4078
- const playbackScene = useMemo(() => ({
4079
- ...playbackSceneBase,
4080
- subjectTrack,
4081
- cameraTrack: followTrack,
4082
- }), [playbackSceneBase, subjectTrack, followTrack]);
3879
+ const playbackScene = useMemo(() => playbackSceneWithTrack(playbackSceneBase, subjectTrack, followTrack), [playbackSceneBase, subjectTrack, followTrack]);
4083
3880
 
4084
3881
  // Browser acceptance seam: it invokes the exact production exporter but
4085
3882
  // suppresses the download, returning only serializable evidence.
@@ -4120,8 +3917,7 @@ export default function App() {
4120
3917
 
4121
3918
  // The follow camera owns the shot camera in the same situations key
4122
3919
  // following would: never while an authoring mode holds the viewport.
4123
- const followCamActive =
4124
- activeCamera.mode !== "keys" && !!followTrack?.[tlFrame] && (preview || (!ikMode && !waypointMode && !posing));
3920
+ const followCamActive = followCameraActive(activeCamera.mode, followTrack, tlPlaying ? playbackFrame() : tlFrame, preview, ikMode, waypointMode, posing);
4125
3921
 
4126
3922
  // Implied locomotion speed per authored segment, on the timeline clock
4127
3923
  // (m/s is physical, so the judge always uses tlFps). Shown in the
@@ -4824,7 +4620,7 @@ export default function App() {
4824
4620
  return (
4825
4621
  <AppContext.Provider value={appContext}>
4826
4622
  <StudioShellContext.Provider value={shellContext}>
4827
- <StudioShell className={"app" + (renderActive ? "" : " render-idle")} style={workspaceStyle} data-workflow-mode={workflowMode} data-previs-mode={scenesDomain.previsMode} data-embed-mode={embedMode ? "playview" : playgroundMode ? "playground" : undefined} data-playground-hint={playgroundMode ? playgroundHint ?? undefined : undefined} data-tutorial-step={cameraTutorial ? cameraTutorialStep ?? undefined : undefined} data-rail-draw={railDraw ? 1 : undefined} data-shot-look={lookThroughShot && !embedMode ? 1 : undefined} data-plan-draw={railDraw || pathDraw || waypointMode ? 1 : undefined}
4623
+ <StudioShell className={"app" + (renderActive ? "" : " render-idle")} style={workspaceStyle} data-workflow-mode={workflowMode} data-previs-mode={scenesDomain.previsMode} data-embed-mode={embedMode ? "playview" : playgroundMode ? "playground" : undefined} data-rail-draw={railDraw ? 1 : undefined} data-shot-look={lookThroughShot && !embedMode ? 1 : undefined} data-plan-draw={railDraw || pathDraw || waypointMode ? 1 : undefined}
4828
4624
  viewport={
4829
4625
  <div className="viewport" data-drop={viewportDrop.over ? "over" : undefined} {...viewportDrop.handlers}
4830
4626
  onWheel={(event) => {
@@ -4843,26 +4639,6 @@ export default function App() {
4843
4639
  </div>
4844
4640
  )}
4845
4641
 
4846
- {/* Sits under the mode tabs and left of the Top-View inset, over the
4847
- stage it is teaching. The overlay itself never takes the pointer
4848
- (styles.css) — every step is completed in the studio underneath. */}
4849
- {cameraTutorial && !embedMode && (
4850
- <CameraTutorial
4851
- key={cameraTutorialAttempt}
4852
- analytics={cameraTutorialAnalytics.current}
4853
- previewing={lookThroughShot}
4854
- previsMode={scenesDomain.previsMode}
4855
- shotCount={shots.length}
4856
- railReady={shots.some((shot) => Array.isArray(shot.camera?.cameraRail) && shot.camera.cameraRail.length >= 2)}
4857
- onStepChange={setCameraTutorialStep}
4858
- onComplete={() => { cameraTutorialCompletedRef.current = true; cameraTutorialHandoff?.complete(); }}
4859
- onClose={() => closeCameraTutorial()}
4860
- handoff={cameraTutorialHandoff}
4861
- shotId={activeShot?.id ?? null}
4862
- onOpenExport={openExportMenuForShot}
4863
- onContinue={() => closeCameraTutorial()}
4864
- />
4865
- )}
4866
4642
  <div className="stage" id="stage" ref={stageRef} data-render-loop={renderActive ? "always" : "demand"}>
4867
4643
  {/* Shadows were off, so every castShadow in props.jsx was inert and
4868
4644
  nothing on the open stage ever touched the floor. A contact
@@ -4998,7 +4774,10 @@ export default function App() {
4998
4774
  take={{ frameCount: tlFrameCount, fps: tlFps, objectTracks }}
4999
4775
  registryRef={carrierRegistryRef}
5000
4776
  >
5001
- <Character
4777
+ <PlaybackCharacter
4778
+ entry={view.entry}
4779
+ clip={view.clip}
4780
+ shots={shots}
5002
4781
  url={view.url}
5003
4782
  {...{ model: view.model, posture: view.posture }}
5004
4783
  position={view.position}
@@ -5055,19 +4834,10 @@ export default function App() {
5055
4834
  charA={charA}
5056
4835
  charB={charB}
5057
4836
  showB={showB}
5058
- probeX={motionPos ? motionPos.x : charA.x}
5059
- probeZ={motionPos ? motionPos.z : charA.z}
4837
+ probe={{ scene: playbackSceneBase, frameRef: propFrameRef, carry: subjectCarry }}
5060
4838
  camRef={shotCamRef}
5061
4839
  look={look}
5062
- onMetrics={(p, visible) => {
5063
- shotCameraPosRef.current = { x: p.x, y: p.y, z: p.z };
5064
- setCameraPos((prev) =>
5065
- Math.abs(prev.x - p.x) + Math.abs(prev.y - p.y) + Math.abs(prev.z - p.z) > 1e-4
5066
- ? { x: p.x, y: p.y, z: p.z }
5067
- : prev,
5068
- );
5069
- setSubjectVisible((prev) => (prev === visible ? prev : visible));
5070
- }}
4840
+ onMetrics={(p, visible) => updateShotMetrics({ position: p, visible, playing: tlPlaying, setCameraPos, setSubjectVisible, cameraRef: shotCameraPosRef, visibleRef: subjectVisibleRef })}
5071
4841
  />
5072
4842
  <MoveRig
5073
4843
  playing={movePlaying}
@@ -5148,11 +4918,10 @@ export default function App() {
5148
4918
  target={rangePinPreview?.target}
5149
4919
  enabled={ikMode && ikEditTool === "pin" && Boolean(rangePinPreview?.target)}
5150
4920
  />
5151
- {/* The tutorial's top view is the landing playground's: camera, cast
5152
- and the rail only, so the line the Rail step asks for is drawn on
5153
- a clean floor instead of over 34 footprints. */}
4921
+ {/* The landing playground's top view: camera, cast and the rail
4922
+ only, so a rail is drawn on a clean floor. */}
5154
4923
  <PlanBoard
5155
- minimal={playgroundMode || cameraTutorial}
4924
+ minimal={playgroundMode}
5156
4925
  hostRef={planHostRef}
5157
4926
  planCamRef={planCamRef}
5158
4927
  shotCamRef={shotCamRef}
@@ -5447,7 +5216,7 @@ export default function App() {
5447
5216
 
5448
5217
  <div
5449
5218
  ref={shotPreviewRef}
5450
- hidden={playMode || ikMode || lookThroughShot || workflowMode === "motion"}
5219
+ hidden={playMode || lookThroughShot}
5451
5220
  className="vp-pane vp-shot-preview"
5452
5221
  style={{ "--shot-aspect": shotOutput.aspect }}
5453
5222
  >