@dailies-studio/shot-editor 0.1.0 → 0.2.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 (432) hide show
  1. package/dist/.vite/manifest.json +382 -0
  2. package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
  3. package/dist/assets/AgentPanel-DJEphah0.css +1 -0
  4. package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
  5. package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
  6. package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
  7. package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
  8. package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
  9. package/dist/assets/CurvePane-BjFVabKX.css +1 -0
  10. package/dist/assets/CurvePane-R22WzllJ.js +1 -0
  11. package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
  12. package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
  13. package/dist/assets/FontPicker-v0IOIArM.css +1 -0
  14. package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
  15. package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
  16. package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
  17. package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
  18. package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
  19. package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
  20. package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
  21. package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
  22. package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
  23. package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
  24. package/dist/assets/SoundMode-BmcZru3i.js +3 -0
  25. package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
  26. package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
  27. package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
  28. package/dist/assets/board-B88EPuLO.css +1 -0
  29. package/dist/assets/board-CL-he7SL.js +3 -0
  30. package/dist/assets/components-0oOSPN0l.js +1 -0
  31. package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
  32. package/dist/assets/googleFonts-BNon6M7X.js +1 -0
  33. package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
  34. package/dist/assets/index-4F5NQmI8.css +1 -0
  35. package/dist/assets/index-BGHQGV5z.js +117 -0
  36. package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  37. package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  38. package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  39. package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  40. package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  41. package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  42. package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  43. package/dist/assets/libraryModel-CpIwfFha.js +1 -0
  44. package/dist/assets/lucide-6iK64tTB.js +9319 -0
  45. package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
  46. package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
  47. package/dist/assets/webmcp-BsB9gxWC.js +2 -0
  48. package/dist/index.html +34 -3
  49. package/package.json +12 -9
  50. package/src/addFamilies.ts +66 -0
  51. package/src/api.ts +1631 -55
  52. package/src/app/App.tsx +977 -198
  53. package/src/app/lazyPanel.tsx +137 -0
  54. package/src/app/lazySurfaces.tsx +203 -0
  55. package/src/audio/index.tsx +41 -10
  56. package/src/audio/meter.ts +485 -0
  57. package/src/audio/meterMath.ts +273 -0
  58. package/src/audio/meterStore.ts +155 -0
  59. package/src/audio/offlineMix.ts +289 -0
  60. package/src/audio/preview.ts +928 -118
  61. package/src/bridge.ts +15 -5
  62. package/src/bridgeTypes.ts +1611 -36
  63. package/src/channels.ts +17 -32
  64. package/src/fonts/googleFonts.ts +4 -0
  65. package/src/hostApi.ts +174 -0
  66. package/src/icons.tsx +770 -0
  67. package/src/layerWords.ts +61 -0
  68. package/src/main.tsx +56 -4
  69. package/src/panels/ResizeHandle.tsx +33 -43
  70. package/src/panels/agent/AgentDots.tsx +36 -0
  71. package/src/panels/agent/AgentPanel.tsx +119 -43
  72. package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
  73. package/src/panels/agent/agent.css +113 -97
  74. package/src/panels/agent/agentGate.ts +35 -0
  75. package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
  76. package/src/panels/code/CodeStudio.tsx +373 -0
  77. package/src/panels/code/FileTree.tsx +265 -0
  78. package/src/panels/code/codeBufferGuard.ts +52 -0
  79. package/src/panels/code/codeParts.tsx +321 -0
  80. package/src/panels/comments/CommentComposer.tsx +239 -0
  81. package/src/panels/comments/CommentThread.tsx +346 -0
  82. package/src/panels/comments/CommentsPanel.tsx +336 -0
  83. package/src/panels/comments/InboxButton.tsx +131 -0
  84. package/src/panels/comments/MentionMenu.tsx +98 -0
  85. package/src/panels/comments/parts.tsx +171 -0
  86. package/src/panels/export/ExportDialog.tsx +919 -96
  87. package/src/panels/export/ExportScrim.tsx +57 -0
  88. package/src/panels/files/AssetsTab.tsx +656 -0
  89. package/src/panels/files/FilePanel.tsx +1466 -216
  90. package/src/panels/files/LayerMarks.tsx +40 -0
  91. package/src/panels/files/LeftColumn.tsx +37 -0
  92. package/src/panels/files/McpSection.tsx +690 -47
  93. package/src/panels/files/MissingAssetsNotice.tsx +60 -0
  94. package/src/panels/files/assetDrag.ts +79 -0
  95. package/src/panels/files/resourceParts.tsx +255 -0
  96. package/src/panels/fonts/FontPicker.tsx +762 -0
  97. package/src/panels/fonts/faceCache.ts +207 -0
  98. package/src/panels/fonts/fontPicker.css +271 -0
  99. package/src/panels/fonts/pickerModel.ts +265 -0
  100. package/src/panels/fonts/pickerRequest.ts +46 -0
  101. package/src/panels/header/AccountAvatar.tsx +76 -0
  102. package/src/panels/header/AgentChip.tsx +77 -0
  103. package/src/panels/header/AppMenu.tsx +213 -0
  104. package/src/panels/header/Header.tsx +564 -84
  105. package/src/panels/header/HistoryControls.tsx +31 -5
  106. package/src/panels/header/PresenceStack.tsx +329 -0
  107. package/src/panels/header/ProjectTitle.tsx +232 -0
  108. package/src/panels/header/ShareButton.tsx +59 -0
  109. package/src/panels/header/StatusBadges.tsx +358 -27
  110. package/src/panels/header/SyncDeletedDialog.tsx +88 -0
  111. package/src/panels/header/ZoomControls.tsx +46 -15
  112. package/src/panels/header/agentHandoff.ts +52 -0
  113. package/src/panels/header/leave.ts +26 -0
  114. package/src/panels/help/HelpSection.tsx +189 -163
  115. package/src/panels/insert/InsertDrawer.tsx +723 -0
  116. package/src/panels/insert/iconSet.ts +109 -0
  117. package/src/panels/inspector/ExposeDot.tsx +117 -0
  118. package/src/panels/inspector/FramingRows.tsx +314 -44
  119. package/src/panels/inspector/InspectorFooter.tsx +91 -0
  120. package/src/panels/inspector/InspectorHead.tsx +425 -0
  121. package/src/panels/inspector/InspectorPanel.tsx +408 -124
  122. package/src/panels/inspector/KeyframeDot.tsx +373 -0
  123. package/src/panels/inspector/KeyframeSection.tsx +257 -0
  124. package/src/panels/inspector/MarkerSection.tsx +55 -11
  125. package/src/panels/inspector/ParamRow.tsx +84 -53
  126. package/src/panels/inspector/RightColumn.tsx +152 -0
  127. package/src/panels/inspector/TimeSection.tsx +866 -0
  128. package/src/panels/inspector/animate/EaseControl.tsx +244 -0
  129. package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
  130. package/src/panels/inspector/animate/ShotSection.tsx +562 -48
  131. package/src/panels/inspector/design/AlignRow.tsx +95 -31
  132. package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
  133. package/src/panels/inspector/design/ClonerSection.tsx +323 -62
  134. package/src/panels/inspector/design/ComponentSection.tsx +50 -43
  135. package/src/panels/inspector/design/CornerSection.tsx +45 -14
  136. package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
  137. package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
  138. package/src/panels/inspector/design/DeviceSection.tsx +210 -0
  139. package/src/panels/inspector/design/FillSection.tsx +83 -51
  140. package/src/panels/inspector/design/FontField.tsx +196 -0
  141. package/src/panels/inspector/design/HtmlSection.tsx +23 -12
  142. package/src/panels/inspector/design/InstanceSection.tsx +373 -154
  143. package/src/panels/inspector/design/MaskSection.tsx +84 -0
  144. package/src/panels/inspector/design/MaterialSection.tsx +177 -0
  145. package/src/panels/inspector/design/MediaSection.tsx +273 -120
  146. package/src/panels/inspector/design/MeshSection.tsx +910 -82
  147. package/src/panels/inspector/design/PaintRows.tsx +550 -0
  148. package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
  149. package/src/panels/inspector/design/ShadowSection.tsx +221 -48
  150. package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
  151. package/src/panels/inspector/design/StrokeSection.tsx +228 -43
  152. package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
  153. package/src/panels/inspector/design/TransformSection.tsx +244 -50
  154. package/src/panels/inspector/design/TypographySection.tsx +679 -110
  155. package/src/panels/inspector/layout.ts +295 -0
  156. package/src/panels/inspector/propValue.ts +199 -0
  157. package/src/panels/inspector/scene/CameraSection.tsx +232 -22
  158. package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
  159. package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
  160. package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
  161. package/src/panels/inspector/scene/SceneSection.tsx +219 -44
  162. package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
  163. package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
  164. package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
  165. package/src/panels/inspector/timeModel.ts +190 -0
  166. package/src/panels/inspector/useDocWrite.ts +438 -146
  167. package/src/panels/library/LibraryBrowser.tsx +435 -0
  168. package/src/panels/library/LibraryGrid.tsx +190 -0
  169. package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
  170. package/src/panels/library/LibraryRail.tsx +84 -0
  171. package/src/panels/library/LibrarySoundPanel.tsx +293 -0
  172. package/src/panels/library/SoundHistory.tsx +245 -0
  173. package/src/panels/library/StockSounds.tsx +390 -0
  174. package/src/panels/library/libraryModel.ts +147 -0
  175. package/src/panels/library/stockSearch.ts +77 -0
  176. package/src/panels/maskMarks.tsx +117 -0
  177. package/src/panels/material/MaterialCommentNode.tsx +54 -0
  178. package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
  179. package/src/panels/material/MaterialGraphMenu.tsx +45 -0
  180. package/src/panels/material/MaterialGroupNode.tsx +56 -0
  181. package/src/panels/material/MaterialLibrary.tsx +253 -0
  182. package/src/panels/material/MaterialNode.tsx +453 -0
  183. package/src/panels/material/MaterialNodePalette.tsx +349 -0
  184. package/src/panels/material/MaterialParameterControls.tsx +525 -0
  185. package/src/panels/material/MaterialRerouteNode.tsx +42 -0
  186. package/src/panels/material/MaterialWorkspace.tsx +501 -0
  187. package/src/panels/material/NodePreview.tsx +70 -0
  188. package/src/panels/material/flowModel.ts +441 -0
  189. package/src/panels/material/parameterValues.ts +231 -0
  190. package/src/panels/material/previewScheduler.ts +235 -0
  191. package/src/panels/material/reactFlow.css +6 -0
  192. package/src/panels/material/socketGlyph.ts +85 -0
  193. package/src/panels/material/viewportMemory.ts +58 -0
  194. package/src/panels/preferences/PreferencesDialog.tsx +215 -0
  195. package/src/panels/share/ShareComponentDialog.tsx +247 -0
  196. package/src/panels/stage/StageMenu.tsx +35 -5
  197. package/src/panels/stage/StagePanel.tsx +338 -23
  198. package/src/panels/stage/cellSlots.tsx +70 -0
  199. package/src/panels/storyboard/BoardCanvas.tsx +739 -0
  200. package/src/panels/storyboard/BoardCursors.tsx +158 -0
  201. package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
  202. package/src/panels/storyboard/BoardPlan.tsx +42 -0
  203. package/src/panels/storyboard/ComponentSheet.tsx +187 -0
  204. package/src/panels/storyboard/ComponentStage.tsx +235 -0
  205. package/src/panels/storyboard/MiniSketch.tsx +173 -0
  206. package/src/panels/storyboard/PartPicker.tsx +64 -0
  207. package/src/panels/storyboard/PlanPanel.tsx +617 -0
  208. package/src/panels/storyboard/ReferencePreview.tsx +217 -0
  209. package/src/panels/storyboard/ShotBrief.tsx +360 -0
  210. package/src/panels/storyboard/ShotCard.tsx +251 -0
  211. package/src/panels/storyboard/StagePreview.tsx +348 -0
  212. package/src/panels/storyboard/StillThumbs.tsx +162 -0
  213. package/src/panels/storyboard/StoryboardView.tsx +79 -0
  214. package/src/panels/storyboard/board.css +1065 -0
  215. package/src/panels/storyboard/board.ts +12 -0
  216. package/src/panels/storyboard/boardClipboard.ts +111 -0
  217. package/src/panels/storyboard/boardOrder.ts +68 -0
  218. package/src/panels/storyboard/boardRows.tsx +425 -0
  219. package/src/panels/storyboard/liveCap.ts +90 -0
  220. package/src/panels/storyboard/plan.css +295 -0
  221. package/src/panels/storyboard/posterCache.ts +133 -0
  222. package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
  223. package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
  224. package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
  225. package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
  226. package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
  227. package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
  228. package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
  229. package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
  230. package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
  231. package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
  232. package/src/panels/storyboard/shotboard/sketch.css +1392 -0
  233. package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
  234. package/src/panels/storyboard/storyboardDrop.ts +164 -0
  235. package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
  236. package/src/panels/studio/ComponentSidePanel.tsx +355 -0
  237. package/src/panels/studio/ComponentStudio.tsx +243 -0
  238. package/src/panels/studio/ComponentsHome.tsx +318 -0
  239. package/src/panels/studio/ComponentsPreview.tsx +179 -0
  240. package/src/panels/studio/EveryUse.tsx +244 -0
  241. package/src/panels/studio/ThisUsePanel.tsx +679 -0
  242. package/src/panels/studio/componentParts.tsx +341 -0
  243. package/src/panels/studio/components.ts +6 -0
  244. package/src/panels/studio/newCodeComponent.ts +66 -0
  245. package/src/panels/timeline/AudioFxButton.tsx +290 -0
  246. package/src/panels/timeline/AudioLane.tsx +674 -85
  247. package/src/panels/timeline/BlockRow.tsx +154 -85
  248. package/src/panels/timeline/ChannelRow.tsx +378 -129
  249. package/src/panels/timeline/CurvePane.tsx +508 -0
  250. package/src/panels/timeline/Filmstrip.tsx +361 -0
  251. package/src/panels/timeline/GenerateDrawer.tsx +407 -0
  252. package/src/panels/timeline/KeyStretch.tsx +115 -0
  253. package/src/panels/timeline/LifespanRow.tsx +730 -47
  254. package/src/panels/timeline/MarkerLane.tsx +46 -8
  255. package/src/panels/timeline/PlayButton.tsx +91 -0
  256. package/src/panels/timeline/PropertyPicker.tsx +254 -0
  257. package/src/panels/timeline/ShotRow.tsx +400 -0
  258. package/src/panels/timeline/SoundMode.tsx +1406 -0
  259. package/src/panels/timeline/TimelineDock.tsx +2338 -272
  260. package/src/panels/timeline/TransportVolume.tsx +163 -0
  261. package/src/panels/timeline/beatRuler.ts +74 -0
  262. package/src/panels/timeline/curveModel.ts +500 -0
  263. package/src/panels/timeline/duckUnderVoice.ts +68 -0
  264. package/src/panels/timeline/keyframeMenu.ts +138 -0
  265. package/src/panels/timeline/labelWidth.ts +49 -0
  266. package/src/panels/timeline/marquee.ts +155 -0
  267. package/src/panels/timeline/model.ts +1860 -199
  268. package/src/panels/timeline/snap.ts +125 -43
  269. package/src/panels/timeline/staggerPreview.tsx +92 -0
  270. package/src/panels/timeline/takeMeasure.ts +72 -0
  271. package/src/panels/timeline/useLive.ts +48 -0
  272. package/src/panels/timeline/viewport.ts +234 -0
  273. package/src/panels/toolbar/BackChip.tsx +116 -0
  274. package/src/panels/toolbar/CameraViewBar.tsx +118 -49
  275. package/src/panels/toolbar/CanvasBar.tsx +117 -0
  276. package/src/panels/toolbar/InsertButton.tsx +38 -0
  277. package/src/panels/toolbar/IsolationTrail.tsx +156 -0
  278. package/src/panels/toolbar/PanelToggles.tsx +86 -0
  279. package/src/panels/toolbar/ShadingBar.tsx +119 -26
  280. package/src/panels/toolbar/ToolDockBar.tsx +329 -80
  281. package/src/panels/toolbar/ToolFooter.tsx +102 -0
  282. package/src/panels/toolbar/ViewModeBar.tsx +247 -38
  283. package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
  284. package/src/panels/toolbar/useAddRows.tsx +326 -0
  285. package/src/panels/toolbar/useDismiss.ts +122 -0
  286. package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
  287. package/src/panels/versions/versionModel.ts +103 -0
  288. package/src/panels/virtualWindow.ts +271 -0
  289. package/src/shotTokens.css +151 -60
  290. package/src/stage/FrustumGizmo.tsx +504 -84
  291. package/src/stage/GridPlane.tsx +225 -16
  292. package/src/stage/NodePreviews.tsx +628 -0
  293. package/src/stage/PerfHud.tsx +437 -47
  294. package/src/stage/SplitRenderer.tsx +816 -169
  295. package/src/stage/StageApp.tsx +4274 -486
  296. package/src/stage/StageRootBoundary.tsx +114 -0
  297. package/src/stage/StageTools.tsx +651 -151
  298. package/src/stage/TransformGizmo.tsx +696 -0
  299. package/src/stage/adaptivePresentation.ts +145 -0
  300. package/src/stage/bridgeClient.ts +2522 -41
  301. package/src/stage/exportWorker.ts +9 -0
  302. package/src/stage/feed.ts +305 -14
  303. package/src/stage/fontPreview.ts +176 -0
  304. package/src/stage/frustumGeometry.ts +319 -1
  305. package/src/stage/furnitureTokens.ts +33 -0
  306. package/src/stage/gizmoMath.ts +508 -16
  307. package/src/stage/gpuTimer.ts +276 -0
  308. package/src/stage/instanceModules.ts +194 -33
  309. package/src/stage/interactions/move.ts +24 -5
  310. package/src/stage/isolation.ts +291 -33
  311. package/src/stage/jsSelfProfiler.ts +325 -0
  312. package/src/stage/main.tsx +116 -16
  313. package/src/stage/modalState.ts +222 -23
  314. package/src/stage/moduleEpoch.ts +60 -0
  315. package/src/stage/motionPathStage.ts +279 -0
  316. package/src/stage/motionPreview.ts +74 -0
  317. package/src/stage/overlays/Overlays.tsx +1112 -159
  318. package/src/stage/overlays/Rulers.tsx +495 -0
  319. package/src/stage/panes.ts +159 -10
  320. package/src/stage/project.ts +68 -3
  321. package/src/stage/renderScaleController.ts +730 -0
  322. package/src/stage/rulers.ts +129 -0
  323. package/src/stage/sceneExtent.ts +75 -0
  324. package/src/stage/shading.ts +20 -0
  325. package/src/stage/splineStage.ts +632 -0
  326. package/src/stage/stagePerformanceTelemetry.ts +717 -0
  327. package/src/stage/stageTheme.ts +36 -0
  328. package/src/stage/stageWarmup.ts +11 -0
  329. package/src/stage/textAutoSize.ts +124 -0
  330. package/src/stage/tier1Export.ts +641 -0
  331. package/src/stage/tokenColor.ts +14 -9
  332. package/src/stage/view.ts +56 -9
  333. package/src/stage/workingView.ts +277 -33
  334. package/src/state/accountPrefs.ts +261 -0
  335. package/src/state/agentActions.ts +173 -15
  336. package/src/state/agentActivity.ts +198 -0
  337. package/src/state/agentQuestion.ts +239 -0
  338. package/src/state/bootOpen.ts +42 -0
  339. package/src/state/cameraVerbs.ts +139 -0
  340. package/src/state/clientId.ts +6 -1
  341. package/src/state/comments.ts +1099 -0
  342. package/src/state/context.tsx +19 -1
  343. package/src/state/editorPrefs.ts +220 -0
  344. package/src/state/errorReports.ts +121 -0
  345. package/src/state/fieldWrite.ts +161 -0
  346. package/src/state/fontRecents.ts +70 -0
  347. package/src/state/fonts.ts +656 -0
  348. package/src/state/graphClipboard.ts +414 -0
  349. package/src/state/imageThumbs.ts +216 -0
  350. package/src/state/insert.ts +444 -67
  351. package/src/state/isolationTrail.ts +92 -0
  352. package/src/state/keyMoves.ts +134 -0
  353. package/src/state/keyframeClipboard.ts +177 -0
  354. package/src/state/keymap.ts +1153 -117
  355. package/src/state/layoutPrefs.ts +272 -0
  356. package/src/state/layouts.ts +205 -0
  357. package/src/state/measureMedia.ts +167 -0
  358. package/src/state/menu.ts +2341 -28
  359. package/src/state/motionStamp.ts +7 -0
  360. package/src/state/panelSize.ts +375 -5
  361. package/src/state/pendingChanges.ts +119 -0
  362. package/src/state/playHold.ts +78 -0
  363. package/src/state/playbackPreparation.ts +562 -0
  364. package/src/state/posterCapture.ts +242 -0
  365. package/src/state/presence.ts +254 -0
  366. package/src/state/projectComponents.ts +160 -0
  367. package/src/state/renderEvents.ts +298 -18
  368. package/src/state/runtimeErrors.ts +46 -0
  369. package/src/state/selection.ts +12 -0
  370. package/src/state/shortcuts.ts +1606 -0
  371. package/src/state/sketchGuide.ts +51 -0
  372. package/src/state/splineRefs.ts +87 -0
  373. package/src/state/stageHealth.ts +259 -0
  374. package/src/state/stageWatchdog.ts +47 -0
  375. package/src/state/stillStage.ts +330 -0
  376. package/src/state/stillThumbs.ts +474 -0
  377. package/src/state/store.ts +23591 -2982
  378. package/src/state/storyboardModel.ts +528 -0
  379. package/src/state/sync.ts +1257 -0
  380. package/src/state/syncCache.ts +159 -0
  381. package/src/state/triangles.ts +13 -0
  382. package/src/state/types.ts +1698 -55
  383. package/src/state/unsavedGuard.ts +49 -0
  384. package/src/state/viewMode.ts +200 -0
  385. package/src/state/webmcp.ts +1128 -0
  386. package/src/state/workspace.ts +1147 -0
  387. package/src/styles/assets-tab.css +304 -0
  388. package/src/styles/canvas.css +545 -0
  389. package/src/styles/comments.css +1435 -0
  390. package/src/styles/controls.css +162 -0
  391. package/src/styles/dialogs.css +390 -0
  392. package/src/styles/insert-drawer.css +586 -0
  393. package/src/styles/inspector-board.css +207 -0
  394. package/src/styles/inspector-object.css +486 -0
  395. package/src/styles/inspector-shot.css +252 -0
  396. package/src/styles/inspector.css +360 -0
  397. package/src/styles/left-column.css +1628 -0
  398. package/src/styles/library-preview.css +575 -0
  399. package/src/styles/library.css +512 -0
  400. package/src/styles/modes/code.css +569 -0
  401. package/src/styles/modes/component.css +1561 -0
  402. package/src/styles/modes/material.css +1201 -0
  403. package/src/styles/modes/storyboard.css +518 -0
  404. package/src/styles/share-component.css +49 -0
  405. package/src/styles/shell.css +653 -0
  406. package/src/styles/shot-brief.css +360 -0
  407. package/src/styles/states.css +76 -0
  408. package/src/styles/sync.css +380 -0
  409. package/src/styles/timeline-graph.css +384 -0
  410. package/src/styles/timeline-meters.css +89 -0
  411. package/src/styles/timeline-rows.css +1959 -0
  412. package/src/styles/timeline-sound.css +1003 -0
  413. package/src/styles/timeline.css +754 -0
  414. package/src/styles/tool-footer.css +525 -0
  415. package/src/styles/topbar.css +886 -0
  416. package/src/styles/versions.css +262 -0
  417. package/src/styles.css +72 -2921
  418. package/src/tools.ts +108 -23
  419. package/dist/assets/index-B7notge7.css +0 -1
  420. package/dist/assets/index-BBE6Ta9L.js +0 -52
  421. package/src/panels/files/icons.tsx +0 -118
  422. package/src/panels/header/DocSwitcher.tsx +0 -85
  423. package/src/panels/header/TransportBar.tsx +0 -86
  424. package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
  425. package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
  426. package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
  427. package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
  428. package/src/panels/inspector/animate/useBlocks.ts +0 -43
  429. package/src/panels/material/MaterialPicker.tsx +0 -172
  430. package/src/panels/timeline/SceneStrip.tsx +0 -145
  431. package/src/panels/toolbar/AddMenu.tsx +0 -230
  432. package/src/stage/MoveGizmo.tsx +0 -411
@@ -15,21 +15,477 @@
15
15
  // So: one document, flat fields, and roughly 1700 lines of board machinery
16
16
  // simply not written. Autosave debounce, the sha256 rev and the generation
17
17
  // fence all still apply — they were never board-shaped, only board-keyed.
18
- import { DOCK_HEIGHT, RAIL_WIDTH } from "./panelSize.js";
19
- import type { Doc3D } from "@dailies-studio/shot/doc";
20
- import type { AgentCredentialsInfo, AgentTranscriptItem } from "@dailies-studio/agent-client";
18
+ import {
19
+ CODE_PREVIEW_FRAMES,
20
+ defDurationFrames,
21
+ docDurationFrames,
22
+ sceneToMaster,
23
+ } from "@dailies-studio/shot/doc";
24
+ import {
25
+ CODE_LIVE_WIDTH,
26
+ DOCK_HEIGHT,
27
+ DOCK_PPF,
28
+ INSPECTOR_WIDTH,
29
+ MATERIAL_COLUMN,
30
+ RAIL_WIDTH,
31
+ } from "./panelSize.js";
32
+ import type {
33
+ Doc3D,
34
+ MaterialGraphSocketType,
35
+ MeshMaterialSlotInfo,
36
+ } from "@dailies-studio/shot/doc";
37
+ import type { Ease } from "@dailies-studio/engine";
38
+ import type {
39
+ AgentCredentialsInfo,
40
+ AgentTranscriptItem,
41
+ } from "@dailies-studio/agent-client";
21
42
  import type { Contract } from "@dailies-studio/contract";
22
- import type { ComponentRow, RenderJob, ShotListing } from "../api.js";
23
- import type { ShadingMode, ShotStageInfo, ShotView, StageLayout, StagePane, WorkingView } from "../bridgeTypes.js";
43
+ import type {
44
+ CodeDiagnostic,
45
+ ComponentDefaultsMeta,
46
+ ComponentFileHome,
47
+ ComponentListingUpdate,
48
+ ComponentRow,
49
+ LibraryItem,
50
+ LibraryItemDetail,
51
+ LibraryItemKind,
52
+ ProjectAssetUsage,
53
+ ProjectFile,
54
+ RenderJob,
55
+ ShotVersion,
56
+ ShotVersionList,
57
+ WorkspaceComponent,
58
+ WorkspaceComponentVersion,
59
+ WorkspaceComponentVersionInfo,
60
+ WorkspaceFontList,
61
+ } from "../api.js";
62
+ import type { GoogleFontEntry } from "../fonts/googleFonts.js";
63
+ import type { FontApplyProgress } from "./fonts.js";
64
+ import type { Value } from "@dailies-studio/engine";
65
+ import type {
66
+ PlaybackResolution,
67
+ ShadingMode,
68
+ ShotComponentBackground,
69
+ ShotStageInfo,
70
+ StagePerformanceReport,
71
+ StagePieceFailure,
72
+ ShotView,
73
+ SplineBasis,
74
+ SplineEditContext,
75
+ StageInset,
76
+ StageLayout,
77
+ StagePane,
78
+ WorkingView,
79
+ WorkPlaneTarget,
80
+ } from "../bridgeTypes.js";
24
81
  import { INITIAL_WORKING_VIEW } from "../stage/workingView.js";
25
82
  import type { DrawPoint } from "./insert.js";
26
83
  import type { ShotTool } from "../tools.js";
84
+ import type { SyncStatus } from "./sync.js";
27
85
 
28
86
  export type SaveState = "idle" | "saving" | "saved" | "error";
29
87
 
88
+ /** The dialog a delete under live sync shows (`syncDeleted`). */
89
+ export interface SyncDeleted {
90
+ /** The Project's store name and title when it was trashed. */
91
+ name: string;
92
+ title: string;
93
+ /** Who trashed it, by name; null when it was this user or is unknown. */
94
+ by: string | null;
95
+ color: string | null;
96
+ /** The Trash handle; null when the session did not say (no Restore). */
97
+ trashId: string | null;
98
+ /** `describePending` of what waits. */
99
+ lines: string[];
100
+ count: number;
101
+ /** A Restore or Save as copy is under way. */
102
+ busy: boolean;
103
+ error: string | null;
104
+ }
105
+
106
+ /** The Components workspace's two tabs: the component alone on the stage, or
107
+ * the studio that edits it (Component Studio or Code Studio). */
108
+ export type ComponentsTab = "preview" | "edit";
109
+
110
+ /** The shot board's tools (its footer): Select V, Text T, Box R, Arrow A,
111
+ * Image I and Component C. Image and Component open their pickers. */
112
+ export type SketchTool = "select" | "text" | "box" | "arrow" | "image" | "component";
113
+
114
+ /** Which component the Components workspace shows: a Studio-made definition
115
+ * of this document, or a code component by registry id. */
116
+ /** One use whose changes have no prop in a newer version: where it is, and
117
+ * the keys. The changes stay on the use, kept aside, not applied. */
118
+ export interface ComponentUpdateRemoved {
119
+ where: string;
120
+ keys: string[];
121
+ }
122
+
123
+ /** An Update waiting for a yes, with the props it drops that uses changed. */
124
+ export interface ComponentUpdatePending {
125
+ defId: string;
126
+ version: number;
127
+ /** True for a Community copy: the server writes it, so the yes is always
128
+ * asked and says how to take it back. */
129
+ community: boolean;
130
+ /** One entry per use that loses a changed prop. Null when the Community
131
+ * listing's definition could not be read first: its list then comes after
132
+ * the update, in `ComponentUpdateApplied`. */
133
+ removed: ComponentUpdateRemoved[] | null;
134
+ }
135
+
136
+ /** A Community update the server wrote into this document, held so the
137
+ * author sees what it kept aside and can put the document back. */
138
+ export interface ComponentUpdateApplied {
139
+ defId: string;
140
+ version: number;
141
+ removed: ComponentUpdateRemoved[];
142
+ /** The document the server wrote against, and the one it wrote. Revert
143
+ * update puts `before` back only while the open document is `after`. */
144
+ before: Doc3D;
145
+ after: Doc3D;
146
+ }
147
+
148
+ export type ComponentsSelection = { kind: "studio"; def: string } | { kind: "code"; module: string };
149
+
150
+ /** The Shot a door into Components left (C7): the entered Shot, the playhead,
151
+ * the selection, and the use the door opened (`layerUseKey`). `shotFrame` is
152
+ * the frame inside that Shot, for the crumb. */
153
+ export type ComponentsOrigin = {
154
+ sel: ComponentsSelection;
155
+ useKey: string;
156
+ enteredScene: string | null;
157
+ sceneId: string | null;
158
+ frame: number;
159
+ shotFrame: number;
160
+ selection: readonly string[];
161
+ };
162
+
163
+ /** The Components workspace's preview look (bridge v45). Null fields keep the
164
+ * stage's own ground and the component's own length. */
165
+ /** One build check of a code component. */
166
+ export interface ComponentBuild {
167
+ status: "building" | "built" | "failed";
168
+ /** Why it failed. */
169
+ message?: string;
170
+ /** The workspace version the passing build recorded. */
171
+ version?: number;
172
+ }
173
+
174
+ export interface ComponentPreviewLook {
175
+ background: ShotComponentBackground | null;
176
+ loopFrames: number | null;
177
+ }
178
+
179
+ /** The Insert drawer's sections, top to bottom, and its jump chips. */
180
+ export const INSERT_SECTIONS = ["Basics", "Icons", "Shaders", "Backgrounds"] as const;
181
+ export type InsertSection = (typeof INSERT_SECTIONS)[number];
182
+
183
+ /**
184
+ * What the open source buffer is doing, when it is doing something other than
185
+ * sitting there.
186
+ *
187
+ * DIRTY IS NOT IN HERE. It is `content !== savedContent`, a comparison that
188
+ * cannot be stale, and a status value that also claimed to know would be a
189
+ * second answer to the same question — the one that ends up disagreeing after
190
+ * an undo lands the buffer back on the saved text.
191
+ *
192
+ * `"conflict"` is STICKY: the file changed on disk under the buffer (a 409, or
193
+ * an agent's edit arriving over SSE) and stays that way until the author picks
194
+ * reload or overwrite, because silently choosing either one loses work.
195
+ */
196
+ export type CodeStatus = "idle" | "saving" | "conflict" | "error";
197
+
198
+ /**
199
+ * The source file behind the open code component.
200
+ *
201
+ * SAVES EXPLICITLY (Ctrl+S), never on keystroke: every save re-runs contract
202
+ * extraction and reloads the stage, so autosaving half-typed code would thrash
203
+ * both and show the author a stream of broken renders they did not ask for.
204
+ */
205
+ export interface CodeFileState {
206
+ /** Posix path relative to the project root, which is `ComponentRow.rel`. */
207
+ rel: string;
208
+ /** Live editor text, LF-normalised (CodeMirror is LF-only internally). */
209
+ content: string;
210
+ /** Text at last load or save, LF. Dirty is `content !== savedContent`. */
211
+ savedContent: string;
212
+ /** Server hash at last load or save, sent as `baseHash` so a save can 409. */
213
+ savedHash: string | null;
214
+ /** Restored on save, so a CRLF file round-trips as CRLF. */
215
+ lineEnding: "lf" | "crlf";
216
+ status: CodeStatus;
217
+ /** What went wrong, when `status` is "error" or "conflict". */
218
+ error: string | null;
219
+ /** The HTTP status behind `error`. 404 means the file was deleted on disk. */
220
+ errorStatus: number | null;
221
+ diagnostics: readonly CodeDiagnostic[];
222
+ /**
223
+ * This file is a BUILD PRODUCT the studio itself emits, so the server will
224
+ * refuse every write to it with a 403 and no force.
225
+ *
226
+ * Carried on the buffer so the editor can say so BEFORE the author types a
227
+ * page of changes into a file that cannot accept them. Read from the file's
228
+ * own bytes on the way in (`SourceFile.generated`), not guessed from the
229
+ * path: the marker rule catches an emit that moved, and the directory rule
230
+ * alone would miss it. `docName` is the owning Project's store name when
231
+ * the server resolved it, so the strip can open it.
232
+ */
233
+ generated: { docName: string | null } | null;
234
+ }
235
+
236
+ /** The viewport/editor state borrowed while one mesh's material graph is open.
237
+ * None of it is document data: leaving the graph hands the author back the
238
+ * exact stage, camera and tool posture they were using before they went in. */
239
+ export interface MaterialWorkspaceRestore {
240
+ selection: readonly string[];
241
+ stageLayout: StageLayout;
242
+ workingView: WorkingView;
243
+ activePane: StagePane;
244
+ shadingMode: ShadingMode;
245
+ previousShading: ShadingMode;
246
+ tool: ShotTool;
247
+ pilot: boolean;
248
+ isolation: string | null;
249
+ isolationOrbit: boolean;
250
+ splineEdit: SplineEditContext | null;
251
+ animateMode: boolean;
252
+ }
253
+
254
+ /** A persistent material-authoring context over the live stage. The target is
255
+ * addressed by its container because component definitions and shots can both
256
+ * contain mesh ids, while the graph itself is resolved through the layer's
257
+ * current material binding on every document update (undo may replace it). */
258
+ export interface MaterialWorkspaceState {
259
+ containerId: string;
260
+ layerId: string;
261
+ /**
262
+ * WHICH SURFACE of the target the graph is editing (schema v17).
263
+ *
264
+ * A glTF material index, or null for the layer-wide binding — which is the
265
+ * only possibility on a primitive, and the default on a model until the
266
+ * author picks a slot. Editor state: the DOCUMENT records the binding, and
267
+ * this records which one is on the canvas.
268
+ */
269
+ slot: number | null;
270
+ restore: MaterialWorkspaceRestore;
271
+ }
272
+
273
+ /** What the graph canvas has selected. Nodes and edges together because one
274
+ * rubber band picks up both, and Delete acts on whatever it caught. */
275
+ export interface MaterialGraphSelection {
276
+ nodeIds: readonly string[];
277
+ edgeIds: readonly string[];
278
+ }
279
+
280
+ /**
281
+ * The socket a palette was opened FROM, when the author dragged a wire into
282
+ * empty space rather than pressing Tab.
283
+ *
284
+ * `side` says which end of the wire is in hand: `"source"` means the author is
285
+ * holding an OUTPUT and wants something to feed, `"target"` means they are
286
+ * holding an input and want something to feed it. The type rides along so the
287
+ * palette can put the kinds that would actually connect at the top, rather than
288
+ * letting the author find out by dropping one and being refused.
289
+ */
290
+ export interface MaterialPaletteSource {
291
+ nodeId: string;
292
+ socket: string;
293
+ side: "source" | "target";
294
+ type: MaterialGraphSocketType;
295
+ }
296
+
297
+ /**
298
+ * "Open the node palette here". A MAILBOX, like `stageMenu` and `appMenu`, with
299
+ * the same fresh-identity rule: pressing Tab twice at the same point must
300
+ * re-open the palette rather than be swallowed by value equality.
301
+ *
302
+ * `at` is a point in GRAPH coordinates, or null when the palette should place
303
+ * what it adds wherever the canvas considers the middle — the keyboard route,
304
+ * which has no pointer to ask.
305
+ */
306
+ export interface MaterialPaletteRequest {
307
+ at: { x: number; y: number } | null;
308
+ from?: MaterialPaletteSource;
309
+ }
310
+
311
+ /** Stage-originated shader status. Kept separate from the document because a
312
+ * compiler result says what this browser rendered, never what the author saved. */
313
+ export type MaterialCompileState =
314
+ | { status: "idle" }
315
+ | {
316
+ status: "compiling";
317
+ materialId: string;
318
+ layerId: string;
319
+ slot: number | null;
320
+ }
321
+ | {
322
+ status: "ready";
323
+ materialId: string;
324
+ layerId: string;
325
+ slot: number | null;
326
+ }
327
+ | {
328
+ status: "error";
329
+ materialId: string;
330
+ layerId: string;
331
+ slot: number | null;
332
+ message: string;
333
+ };
334
+
335
+ /**
336
+ * ONE KEYFRAME, AS A CURVE GESTURE ADDRESSES IT.
337
+ *
338
+ * ── AND WHY THIS ONE IS BY ID WHEN `selectedKeyframe` IS BY POSITION ──────
339
+ *
340
+ * `selectedKeyframe`'s comment argues that a PERSISTED subject must be a
341
+ * position: a key can be replaced by a drag landing on its frame, and an id
342
+ * kept across that would address something that is gone.
343
+ *
344
+ * A curve gesture is the opposite case. It runs inside ONE scrub, every capture
345
+ * re-derives from the same base document, and the whole point of the drag is
346
+ * that the frame is CHANGING. A position captured at the press would be right
347
+ * for one pointer event and wrong after it; the id is stable for exactly as
348
+ * long as the gesture is, and `captureCurveOps` resolves it against the base —
349
+ * never against the live document.
350
+ */
351
+ export interface CurveKeyRef {
352
+ containerId: string;
353
+ /** Null = the SHOT itself, the address every keyframe verb already takes. */
354
+ layerId: string | null;
355
+ path: string;
356
+ /** The keyframe's own `kf_…` id. */
357
+ keyframeId: string;
358
+ /** Which component of a VECTOR track the value edit writes. Absent on a
359
+ * scalar track; a vector's keyframe frames are shared across components
360
+ * because they are one track. */
361
+ component?: number;
362
+ }
363
+
364
+ /** A curve gesture's edits, applied in order inside one scrub capture. A batch
365
+ * and not one call each because a keyframe drag MOVES and SETS THE VALUE
366
+ * atomically, and two captures would be two undo entries and one visible
367
+ * intermediate document. */
368
+ export type CurveOp =
369
+ | {
370
+ kind: "move";
371
+ /** `keys[0]` ANCHORS: every key moves by one delta, and a snapped move
372
+ * snaps the first one's landing and applies what it got back to the rest,
373
+ * so the shape of a multi-key selection survives the pull. */
374
+ keys: readonly CurveKeyRef[];
375
+ deltaFrames: number;
376
+ /** Pull the anchor onto a nearby edge — THE ROWS' snap, same targets and
377
+ * same threshold. Off (absent) is the raw rounded delta. */
378
+ snap?: boolean;
379
+ }
380
+ | { kind: "setValue"; key: CurveKeyRef; value: number }
381
+ | { kind: "setEase"; keys: readonly CurveKeyRef[]; ease: Ease };
382
+
383
+ /**
384
+ * ONE KEYFRAME'S ADDRESS, as the store holds it and every keyframe verb takes
385
+ * it: which container, which layer (null = the shot itself), which path, which
386
+ * frame. See `selectedKeyframes` for why it is a position and not an id.
387
+ */
388
+ export interface KeyframeSel {
389
+ containerId: string;
390
+ layerId: string | null;
391
+ path: string;
392
+ frame: number;
393
+ }
394
+
395
+ /** Do these two addresses name the same keyframe? All four fields, because
396
+ * that is the whole address — a path match in the wrong container is a
397
+ * different key on a different layer. */
398
+ export function sameKeyframeSel(a: KeyframeSel, b: KeyframeSel): boolean {
399
+ return (
400
+ a.containerId === b.containerId &&
401
+ a.layerId === b.layerId &&
402
+ a.path === b.path &&
403
+ a.frame === b.frame
404
+ );
405
+ }
406
+
407
+ /** The same identity as a Map key. A PIPE, `curveRowId`'s reason: every part is
408
+ * an id slug or a dotted path and none of them can contain one. */
409
+ export function keyframeSelKey(a: KeyframeSel): string {
410
+ return `${a.containerId}|${a.layerId ?? ""}|${a.path}|${a.frame}`;
411
+ }
412
+
413
+ /**
414
+ * The selection's PRIMARY keyframe, or null.
415
+ *
416
+ * Every consumer that describes ONE keyframe — the inspector's heading, the
417
+ * curve pane's subject ladder, "which track am I looking at" — reads this
418
+ * rather than indexing the array, so widening the field stayed a rename at
419
+ * each of those sites instead of a decision at each of them.
420
+ */
421
+ export function primaryKeyframe(s: {
422
+ selectedKeyframes: readonly KeyframeSel[];
423
+ }): KeyframeSel | null {
424
+ return s.selectedKeyframes[0] ?? null;
425
+ }
426
+
427
+ /**
428
+ * EVERY SPEED THE SHUTTLE HAS, in ladder order.
429
+ *
430
+ * A closed list rather than a number, so "what does a third press of J do" has
431
+ * one answer and no caller can invent 3.5x. `1` is the resting rate and the one
432
+ * every start returns to; the negatives run the picture backwards.
433
+ *
434
+ * SIX AND NOT MORE. Each rung is a doubling, which is the only spacing where a
435
+ * press feels like a step rather than a nudge, and 4x at 30fps is already 120
436
+ * distinct frames a second — past that the display, not the ladder, is the
437
+ * limit.
438
+ */
439
+ export const SHUTTLE_RATES = [-4, -2, -1, 1, 2, 4] as const;
440
+
441
+ export type PlayRate = (typeof SHUTTLE_RATES)[number];
442
+
443
+ /**
444
+ * A window of MASTER frames, half open: `start` plays, `end` does not.
445
+ *
446
+ * The shape `sceneWindows` already uses, deliberately — the playback clock
447
+ * wraps `[start, end)` for an entered shot and for a work area with the same
448
+ * arithmetic, and a closed range here would need one of the two to subtract a
449
+ * frame that the other does not.
450
+ */
451
+ export interface WorkArea {
452
+ start: number;
453
+ end: number;
454
+ }
455
+
456
+ /**
457
+ * One ask for the dock to show some layers' rows (`dockReveal`).
458
+ *
459
+ * THREE SEPARATE ANSWERS, because the two askers want different amounts. The
460
+ * board's "Fine-tune in Studio" selects keys, and a key on a closed row is a
461
+ * selection nobody can see, so it opens the layer's own lanes. A pick on the stage or in Layers only wants the row in view: opening
462
+ * its lanes would grow the dock under the author on every click.
463
+ */
464
+ export interface DockReveal {
465
+ layerIds: readonly string[];
466
+ /** Open each layer's own channel lanes (the shot context's disclosure). */
467
+ lanes: boolean;
468
+ /** Open the row's shot group and scroll the first layer's row into view. */
469
+ scroll: boolean;
470
+ /** Flash the first layer's row once (`is-revealed`). */
471
+ flash: boolean;
472
+ /** Audio clip ids to show (gap 8). Sound mode scrolls to the first clip's
473
+ * track row and flashes it; the other modes have no clip rows to show. */
474
+ audioIds?: readonly string[];
475
+ }
476
+
477
+ /** The Preferences page fields `setEditorPref` writes. The grid, the badges
478
+ * and the playhead rule keep their older setters. */
479
+ export type EditorPrefField =
480
+ | "motionPaths"
481
+ | "rulers"
482
+ | "smartGuides"
483
+ | "blockLabels"
484
+ | "fieldLabels"
485
+ | "toolbarPosition"
486
+ | "playbackResolution";
487
+
30
488
  export interface ShotEditorState {
31
- /** Everything in `.studio/shots/`, for the switcher. */
32
- shots: readonly ShotListing[];
33
489
  /** The open document's store name, or null at the empty state. */
34
490
  name: string | null;
35
491
  doc: Doc3D | null;
@@ -40,47 +496,203 @@ export interface ShotEditorState {
40
496
  /** MASTER frame. Integer, always. */
41
497
  frame: number;
42
498
  playing: boolean;
499
+ /**
500
+ * HOW FAST, AND WHICH WAY, the transport runs. Only read while `playing`.
501
+ *
502
+ * A WORKSPACE PREF and never the document: it is the author holding a key to
503
+ * find a moment, in the same family as `previewVolume` and for the same
504
+ * reason — an export that came out at 4x because somebody shuttled would be
505
+ * the worst kind of silent corruption. `setPlaying(true)` puts it back to 1,
506
+ * so a shuttle is never a mode the author has to remember to leave.
507
+ *
508
+ * THE PICTURE ONLY. See `transportAudible`: sound is silent at every rate but
509
+ * 1, because reverse needs reversed buffers and 2x needs a time stretcher,
510
+ * and either one is a second clock that can disagree with the picture's.
511
+ */
512
+ playRate: PlayRate;
513
+ /**
514
+ * WHETHER THE PROJECT TRANSPORT LOOPS its window (the work area, or the whole
515
+ * film). Off, playback stops on the window's last frame. The dock's Loop
516
+ * button, in the Project context only: a shot or a definition always loops,
517
+ * because those are the views an author watches a beat in over and over
518
+ * (`playbackLoops` is the effective answer). A workspace pref like
519
+ * `playRate`, carried across a document switch by `workspacePrefs`.
520
+ */
521
+ loopPlayback: boolean;
522
+ /**
523
+ * A QUEUED PLAY, or null: Play is on and the transport has not started.
524
+ *
525
+ * `"armed"` waits for the stage to be ready (`stageReady`), `"preparing"`
526
+ * runs a Play pass that has drawn `done` of `total` frames (each frame
527
+ * playback would pay a first-use cost on, see `playbackPreparation.ts`), and
528
+ * `"starting"` waits for the stage to hand over on the start frame. The
529
+ * transport then starts by itself, in one write that clears this and sets
530
+ * `playing` and `playOrigin`.
531
+ *
532
+ * `playing` stays the transport's truth and is false while this is set.
533
+ * Every toggle reads `playOn`, so a second press cancels a queued Play. The
534
+ * same pass run while the author sits paused (the background pass) sets no
535
+ * intent: it is no start, and nothing on screen shows it.
536
+ */
537
+ playIntent: { phase: "armed" | "preparing" | "starting"; done: number; total: number } | null;
538
+ /**
539
+ * WHERE AND WHEN THE TRANSPORT STARTED, or null while it is stopped: the
540
+ * frame (`defFrame` inside a definition) and the `performance.now()` of the
541
+ * start. Written in the same write that turns `playing` true, and cleared
542
+ * on every stop. The picture clock and the audio session both anchor on
543
+ * it, so picture and sound start from one instant.
544
+ */
545
+ playOrigin: { frame: number; at: number } | null;
546
+ /**
547
+ * THE WORK AREA: the window playback loops inside, or null for the whole
548
+ * document.
549
+ *
550
+ * MASTER frames, half open — `[start, end)`, the shape `sceneWindows` uses,
551
+ * so "the last frame" needs no off-by-one anywhere.
552
+ *
553
+ * SESSION STATE AND DELIBERATELY NOT A DOCUMENT FIELD. It is a monitoring
554
+ * preference, like the preview volume: a document field would cost a schema
555
+ * bump and a migration for something that is not part of the film, and the
556
+ * render side already owns an internal frame range a CLI flag can reach
557
+ * without touching the document at all.
558
+ *
559
+ * IT IS ALSO NOT A WORKSPACE PREF, which is the one place it parts company
560
+ * with `playRate`: these are frames of THIS document, and carrying them to
561
+ * the next one would loop the transport over a window that means nothing
562
+ * there. `workspacePrefs` leaves it out on purpose, so a document switch
563
+ * clears it.
564
+ *
565
+ * It applies only in the scene context. A shot already loops inside its own
566
+ * window and a component definition runs its own clock, so both of those
567
+ * have an answer already and a second window would be two answers.
568
+ */
569
+ workArea: WorkArea | null;
43
570
 
44
- /** Which scene the micro timeline and the inspector are looking at. This is
45
- * the SINGLE bit that decides macro vs micro, and it is the same bit the URL
46
- * mirrors — one two-level model, not two. */
571
+ /**
572
+ * Which shot the inspector and the isolation scope are looking at: the one
573
+ * the PLAYHEAD is in, or the one a row/strip click has pinned.
574
+ *
575
+ * DERIVED, NOT A MODE BIT. `sceneAt` ends `?? doc.scenes[0] ?? null`, so it is
576
+ * non-null for any open document, and `seek`, `onHistoryChange` and
577
+ * `loadInto` all rewrite it. It used to be described here as "the single
578
+ * bit that decides macro vs micro" — that was true of the two-level dock this
579
+ * lane deleted, and stayed on the field for a release after the thing it
580
+ * described was gone. The macro/micro bit is `enteredScene`, below.
581
+ */
47
582
  sceneId: string | null;
583
+ /**
584
+ * The shot the author has ENTERED, or null for the scene context.
585
+ *
586
+ * THE ONE CONTEXT BIT, and the reason it is a field of its own rather than a
587
+ * reading of `sceneId`: `sceneId` follows the playhead, so "no shot entered"
588
+ * has no spelling in it at all.
589
+ *
590
+ * null → SCENE context. Arrange shots, aim the camera, grade.
591
+ * set → SHOT context. The elements of that shot and their
592
+ * property lanes.
593
+ * `editingDef` → COMPONENT context, nested inside the shot context.
594
+ *
595
+ * Written ONLY by `enterScene`/`leaveScene` (and the document-swap clear),
596
+ * never derived. While it is set the master playhead is clamped inside that
597
+ * shot's window by `clampToEntered`, which is what keeps every one of the
598
+ * ~83 existing `sceneId` readers correct with no edit: `sceneAt` can only
599
+ * ever answer with the entered shot.
600
+ */
601
+ enteredScene: string | null;
48
602
  /** Layer ids, in document order, deduped. */
49
603
  selection: readonly string[];
50
604
 
51
605
  /** Which tool is armed. Decides what a canvas press does, and which plane a
52
606
  * move unprojects onto. */
53
607
  tool: ShotTool;
608
+ /** Per-shot construction planes. Editor state only and reset on project load. */
609
+ workPlanes: Readonly<Record<string, WorkPlaneTarget>>;
610
+ /** Interpolation used by the next Shift+P draft. A workspace preference. */
611
+ splineBasis: SplineBasis;
612
+ /** The spline whose anchors are being edited, or null for layer mode. */
613
+ splineEdit: SplineEditContext | null;
54
614
 
55
615
  /**
56
- * TWO MODE BITS, and collapsing them would be a data-corrupting surprise.
616
+ * ⇧A, Animate: edits write keyframes at the playhead.
57
617
  *
58
- * `inspectorMode` is a VIEW choice, set by clicking the inspector's tabs.
59
- * `animateMode` is a BEHAVIOUR choice, set ONLY by ⇧A: with it on, a canvas
60
- * drag authors a BLOCK ending at the playhead instead of writing the base.
618
+ * With it on, every author edit (a drag, a typed value, a nudge) still lands
619
+ * as a value, and the store then keys each static field it moved: the old
620
+ * value at the layer's start and the new one at the playhead, in the same
621
+ * undo step (`noteChange`, store.ts). A field a track already writes is
622
+ * keyed at the playhead whether this is on or not. An agent apply never
623
+ * records.
61
624
  *
62
- * One bit would mean that clicking "Animate" to LOOK at the blocks silently
63
- * arms block authoring on the next drag.
625
+ * A BEHAVIOUR bit: nothing but ⇧A and its toolbar button may write this, so
626
+ * looking at a panel never arms recording on the next drag.
64
627
  */
65
- inspectorMode: "design" | "animate";
66
628
  animateMode: boolean;
67
629
 
68
630
  /**
69
- * The block the dock has focused, or null.
631
+ * The camera move the dock has focused, or null.
70
632
  *
71
- * Addressed by (scene, layer, block) even though a block id is unique across
72
- * the document. The layer is what tells a row which lane it is in without a
73
- * lookup; the SCENE is what stopped being derivable when the dock became one
74
- * master strip. `state.sceneId` follows the playhead, so a block clicked in
75
- * scene 3 while the playhead sits in scene 1 would have sent Delete and Split
76
- * at scene 1 — where the id does not exist, so both would have silently done
77
- * nothing.
633
+ * Addressed by (scene, shot) even though a shot id is unique across the
634
+ * document: the SCENE is what stopped being derivable when the dock became
635
+ * one master strip. `state.sceneId` follows the playhead, so a move clicked
636
+ * in scene 3 while the playhead sits in scene 1 would have sent Delete at
637
+ * scene 1, where the id does not exist, and it would silently do nothing.
638
+ */
639
+ selectedMove: { sceneId: string; shotId: string } | null;
640
+ /**
641
+ * The track keyframes the dock has focused. Empty when none is.
642
+ *
643
+ * ── ADDRESSED BY POSITION, NOT BY ID ────────────────────────────────────
644
+ *
645
+ * A `kf_…` id exists and is unique, and this deliberately does not use it. The
646
+ * three edits an author makes to a keyframe are "move it", "ease it" and
647
+ * "delete it", and every one of them is `(track, frame)` — `setKeyframeAt`
648
+ * REPLACES by frame, and a drag onto an occupied frame replaces the key that
649
+ * was there. Keying selection on an id would leave it pointing at a keyframe
650
+ * a drag had just replaced, which is a stale subject with no symptom until
651
+ * Delete removes the wrong thing.
652
+ *
653
+ * The container is carried for `selectedMove`'s reason, one level up: a
654
+ * definition's interior tracks live under a `cd_…` id and a shot's under a
655
+ * `sc_…` one, and `state.sceneId` follows the playhead, so a diamond clicked
656
+ * in another shot would have sent Delete somewhere it does not exist.
657
+ *
658
+ * `layerId: null` means the SHOT ITSELF is the subject — its background and
659
+ * its post stack (P5). Null rather than a second field, because every verb
660
+ * that acts on a keyframe already carries the whole address and a parallel
661
+ * `sceneKeyframe` would double each of them.
662
+ *
663
+ * ── A LIST, WITH `[0]` AS THE PRIMARY ─────────────────────────────────
664
+ *
665
+ * Box-select in the graph, dragging four keys at once, applying one ease to
666
+ * a run and copy/paste are all the same missing capability, and this single
667
+ * field was the whole of what was missing: `CurveGraph` and `TimelineModel`
668
+ * were already written for a set (`selectedAddrs`, `selectKeyframes(refs,
669
+ * mode)`, `move.refs[]`) and only this store answered with one.
670
+ *
671
+ * ORDERED, and `[0]` is the PRIMARY — what the inspector describes, what the
672
+ * curve pane focuses, what a "which track" question resolves against. The
673
+ * most recently added entry becomes primary, which is what makes shift-click
674
+ * feel like extending a selection rather than shuffling it.
675
+ *
676
+ * Still POSITIONS, for the reason above: ids stay a gesture-scope concept,
677
+ * resolved against a drag's own base.
678
+ */
679
+ selectedKeyframes: readonly KeyframeSel[];
680
+ /**
681
+ * ROWS or GRAPH: which face the dock's track area shows.
682
+ *
683
+ * The AE / Rive posture, and a MODE rather than a second panel because the two
684
+ * answer the same question at different resolutions — "when does this happen"
685
+ * and "how does it move" — and a graph beside the rows would take the height
686
+ * the rows need to be readable at all.
687
+ *
688
+ * In the store rather than in the dock because the keymap toggles it and the
689
+ * dock's own `open`/`openShots` sets do not have that problem.
78
690
  */
79
- selectedBlock: { sceneId: string; layerId: string; blockId: string } | null;
691
+ dockMode: "rows" | "graph" | "sound";
80
692
  /**
81
693
  * The marker the strip has selected, or null.
82
694
  *
83
- * A BARE ID, unlike `selectedBlock`: a marker belongs to the document rather
695
+ * A BARE ID, unlike `selectedMove`: a marker belongs to the document rather
84
696
  * than to a scene (`MarkerRef.frame` is a master frame), so there is nothing
85
697
  * to address it relative to and nothing that can go stale under a retime.
86
698
  *
@@ -89,6 +701,25 @@ export interface ShotEditorState {
89
701
  * Delete" can never remove a marker the author stopped looking at.
90
702
  */
91
703
  selectedMarker: string | null;
704
+ /**
705
+ * The audio clips the Sound dock has selected (gap 8): clip ids in either
706
+ * home. The next subject after the marker for Delete, ⌘D, ⌘B and Q / W.
707
+ *
708
+ * It KEEPS the layer selection, because the inspector keeps showing the
709
+ * layer and a clip has no inspector of its own. Any other focus (a camera move, a
710
+ * key, a marker, a layer pick) clears it.
711
+ */
712
+ selectedAudio: readonly string[];
713
+ /**
714
+ * Per-tab SOLO: the track ids this tab hears alone. Monitoring, never the
715
+ * document: not saved, not synced, never in an export. Empty is the full mix.
716
+ */
717
+ soloTracks: readonly string[];
718
+ /** Snap drags to the beat grid when the document has a tempo. Per tab. */
719
+ snapBeats: boolean;
720
+ /** A note the dock shows in its head, such as "3 clips split". Not an
721
+ * error; the dock drops it after a moment. */
722
+ dockNotice: { text: string } | null;
92
723
  /**
93
724
  * The box a drawn IMAGE layer is waiting to land in, or null.
94
725
  *
@@ -111,13 +742,279 @@ export interface ShotEditorState {
111
742
  * inside the iframe is a different document's event and never reaches the
112
743
  * chrome window's outside-click listener.
113
744
  */
114
- stageMenu: { at: { x: number; y: number }; hits: readonly string[]; pane: StagePane } | null;
745
+ stageMenu: {
746
+ at: { x: number; y: number };
747
+ hits: readonly string[];
748
+ pane: StagePane;
749
+ /** Set when the press landed on a motion path dot (gap 3): the menu is
750
+ * that key's, not the layer's. */
751
+ pathKey?: { layerId: string; keyId: string };
752
+ } | null;
753
+ /**
754
+ * ⌘K asked for the app menu. A MAILBOX, like `stageMenu` above, and for the
755
+ * same reason and with the same identity rule: fresh object per press, so a
756
+ * second ⌘K re-opens rather than being swallowed by value equality.
757
+ *
758
+ * It carries NOTHING — the menu opens under its own trigger, whose rect only
759
+ * the header knows. The object exists so that an effect has an identity to key
760
+ * on; a boolean would need a second verb to put it back.
761
+ *
762
+ * Never persisted, never a document edit, and never cleared by the store: the
763
+ * menu owns its own dismissal (Escape, an outside press, a row firing), none
764
+ * of which the store can see.
765
+ */
766
+ appMenu: Record<string, never> | null;
767
+ /** "Focus the column's search", asked by `/`. A MAILBOX on `appMenu`'s
768
+ * model: the store cannot reach the input, so it parks a fresh identity and
769
+ * the panel's effect moves the caret. Never cleared, never persisted. */
770
+ searchFocus: Record<string, never> | null;
115
771
  /**
116
- * The mesh layers a material pick would be applied to, or null when the
117
- * picker is closed. Editor state: nothing exists in the document until a
118
- * card is chosen.
772
+ * A request that Code Studio take ONE STEP BACK, the way its own Escape and
773
+ * back chevron do: leaving asks first when a buffer is dirty. A fresh object per press, on
774
+ * `appMenu`'s model, because the key can arrive where the panel cannot hear
775
+ * it (the stage iframe forwards its keys straight to `handleKey`), and the
776
+ * store cannot open the confirm that leaving needs.
119
777
  */
120
- materialPicker: { layerIds: readonly string[] } | null;
778
+ codeBackRequest: Record<string, never> | null;
779
+ /** The node editor over one real mesh, or null for the ordinary shot shell. */
780
+ materialWorkspace: MaterialWorkspaceState | null;
781
+ /** Fraction of the viewport occupied by the graph pane. Workspace-only. */
782
+ materialColumnWidth: number;
783
+ /** A code component's live preview width in px, clamped to `CODE_LIVE_WIDTH`. */
784
+ codeLiveWidth: number;
785
+ /**
786
+ * What the graph canvas has selected.
787
+ *
788
+ * Its own field rather than a second use of `selection`, which holds LAYER
789
+ * ids: the two are live at the same time (the workspace keeps the mesh
790
+ * selected on the stage while the author works on its nodes), and one field
791
+ * serving both would make Delete in the graph reach for the mesh.
792
+ *
793
+ * Revalidated on every document write beside `isolation`: undo, an agent
794
+ * write or a delete can take a node away, and a selection pointing at nothing
795
+ * makes the next Delete a silent no-op.
796
+ */
797
+ materialGraphSelection: MaterialGraphSelection;
798
+ /**
799
+ * WHICH SCOPE the canvas is showing: the trail of function ids the author has
800
+ * entered, innermost last. Empty is the main graph.
801
+ *
802
+ * A TRAIL and not one id, because the crumb bar has to name every level to
803
+ * get back out of a function entered from inside another one. Editor state:
804
+ * the document records which nodes belong to which function (`node.scope`),
805
+ * and this records which of those the author is looking at.
806
+ */
807
+ materialGraphPath: readonly string[];
808
+ /** The node palette's mailbox, or null. Fresh identity per request — see
809
+ * `MaterialPaletteRequest`. */
810
+ materialPalette: MaterialPaletteRequest | null;
811
+ /** Do graph drags land on the grid? A workspace preference, never document
812
+ * data: node positions are layout and an author's snapping habit is not. */
813
+ materialSnapToGrid: boolean;
814
+ /** "Frame the graph" asked for. A MAILBOX for `appMenu`'s reason: only the
815
+ * canvas knows its own viewport, so the store parks an identity and the
816
+ * canvas's effect does the fit. */
817
+ materialFrameRequest: Record<string, never> | null;
818
+ /** Latest compile status for the graph currently mounted on the stage. */
819
+ materialCompile: MaterialCompileState;
820
+ // ── the storyboard ───────────────────────────────────────────────────────
821
+ /**
822
+ * The fourth mode: the Project's planning board. A FLAG over the shell,
823
+ * not a place that remembers what it replaced: it clears nothing on
824
+ * entry and restores nothing on exit. Mirrored as `?storyboard=1`.
825
+ */
826
+ storyboardOpen: boolean;
827
+ /**
828
+ * THE SHOT BOARD: the planned shot ("part_…") whose sketches the board
829
+ * shows, or null for the whole board. The workspace stays `storyboard`.
830
+ * Closing the board KEEPS it, so the switch's Storyboard segment comes back
831
+ * to the view the author left; a write that removes the part clears it.
832
+ * Mirrored as `?plan=<partId>`. Session state, never saved.
833
+ */
834
+ storyboardPlan: string | null;
835
+ /** The sketch the shot board shows ("skt_…"), one of the plan's, or null
836
+ * while the plan has none yet. Mirrored as `?sketch=<id>`. */
837
+ storyboardSketch: string | null;
838
+ /** The items selected on the open sketch ("ski_…"), revalidated on every
839
+ * write like `storyboardSelection`. */
840
+ sketchSelection: readonly string[];
841
+ /** The shot board's armed tool. A view pref, never saved. */
842
+ sketchTool: SketchTool;
843
+ /** Play on the shot board: the sketch live in one stage. A preview, so
844
+ * nothing is saved, and it stops when the shot board closes. */
845
+ sketchPlaying: boolean;
846
+ /** "Guide: Sketch N" in the Studio: the sketch the stage draws as dashed
847
+ * outlines, or null. A view, never saved, and it goes with its sketch. */
848
+ sketchGuide: { partId: string; sketchId: string } | null;
849
+ /**
850
+ * Where leaving a mode (the Components workspace or the material nodes)
851
+ * goes back to. `"storyboard"` when the mode was entered over the board, so
852
+ * a trip from the board comes back to the board; `"stage"` otherwise.
853
+ * Written on the first entry only, so switching subjects inside a mode keeps
854
+ * it. Entering a Shot resets it.
855
+ */
856
+ returnTarget: "stage" | "storyboard";
857
+ /**
858
+ * The Components workspace is up with NO component open: its empty state,
859
+ * or the top bar's door before a pick. Once a subject opens, `editingDef`
860
+ * or `codeComponent` holds the workspace and this goes false.
861
+ */
862
+ componentsOpen: boolean;
863
+ /** Which tab of the Components workspace shows. Every edit door lands on
864
+ * `"edit"`; the top bar's door keeps what the author last picked. */
865
+ componentsTab: ComponentsTab;
866
+ /** The component the Components workspace last showed. Written by every
867
+ * door, kept on leave, so the top bar reopens it. Cleared on a document
868
+ * swap: a definition id names one document's component. */
869
+ componentsSelection: ComponentsSelection | null;
870
+ /** Where a Shot door into Components came from (C7), or null. Back to Shot
871
+ * and Esc restore it. Leaving Components any other way clears it. */
872
+ componentsOrigin: ComponentsOrigin | null;
873
+ /** The preview's ground and loop (bridge v45), pushed with the component
874
+ * context. */
875
+ componentPreview: ComponentPreviewLook;
876
+ /** The workspace component store's rows (`GET /api/workspace-components`),
877
+ * or null before the first read. The `cpt_` id of a component of this
878
+ * Project is looked up here by its home. */
879
+ workspaceComponents: WorkspaceComponent[] | null;
880
+ /** Which Project each code file belongs to, from the same read (`homes`):
881
+ * the module paths every live Project lists as made for it or uses. Null
882
+ * before the first read, and Your library's "made for no Project" waits on
883
+ * it. Kept across a Project switch, like the rows. */
884
+ componentHomes: ComponentFileHome[] | null;
885
+ /** The font apply in flight for the Font field (`applyFont` and the verbs
886
+ * that share its lane): its download and upload progress, and its failure
887
+ * with Retry (`retryFontApply`). Null when none runs or the last one
888
+ * landed; cleared by a newer apply and by a Project switch. */
889
+ fontApply: FontApplyProgress | null;
890
+ /** The workspace font list (`GET /api/fonts`, `loadWorkspaceFonts`), or
891
+ * null before the first read that succeeded. */
892
+ workspaceFonts: WorkspaceFontList | null;
893
+ /** The Google catalog once loaded (`loadFontCatalog`), for the font
894
+ * helpers that take it (`state/fonts.ts`); null before. */
895
+ fontCatalog: readonly GoogleFontEntry[] | null;
896
+ /** The versions read per workspace component, newest first. */
897
+ componentVersions: Readonly<Record<string, WorkspaceComponentVersionInfo[]>>;
898
+ /** The last build check per code component (registry id). A failing one
899
+ * keeps its last good build on screen and refuses an export. */
900
+ componentBuilds: Readonly<Record<string, ComponentBuild>>;
901
+ /** The Share dialog for the component Components shows (lane P draws it). */
902
+ shareComponentOpen: boolean;
903
+ /** The History view of the component Components shows, or null when it is
904
+ * closed. `viewing` is the version read for the read-only view. */
905
+ componentHistory: { viewing: WorkspaceComponentVersion | null } | null;
906
+ /** Every use (C4): the centre lists the uses of `subject` in place of the
907
+ * preview, and the right panel shows THIS USE for `use` (a key from
908
+ * `cardUseKey`/`layerUseKey` in workspace.ts). It names its subject, so a
909
+ * switch to another component shows that one's preview again without a
910
+ * reset at every door (`usesViewOf`). Null when closed. */
911
+ componentUses: { subject: ComponentsSelection; use: string | null } | null;
912
+ /** The latest version of each Community listing this document's copies
913
+ * came from, by listing id. Read by `refreshListingUpdates`. */
914
+ componentListingUpdates: Readonly<Record<string, ComponentListingUpdate>>;
915
+ /** An Update held for a yes: applying it drops props this document's uses
916
+ * changed, so the author sees them first. Null when none is pending. */
917
+ componentUpdate: ComponentUpdatePending | null;
918
+ /** The Community update just written, until Done, Revert update or a doc
919
+ * swap. Null when none. */
920
+ componentUpdated: ComponentUpdateApplied | null;
921
+ /**
922
+ * The library browser: the whole shipped catalog, full screen, where an
923
+ * author picks what this Project wants.
924
+ *
925
+ * A DIALOG AND NOT A FIFTH MODE, which is the one decision in this field
926
+ * worth writing down. `Dialog` portals to `document.body` at `--z-dialog`
927
+ * 1100, above the whole shell (whose top tier is the overlay, 10), and it
928
+ * brings its own focus trap and its own Escape. So this flag costs no
929
+ * workspace in `workspaceOf`, no shell class, no cell slots, no keymap lane
930
+ * and no `edgesOf` key.
931
+ *
932
+ * It also has to be a dialog to be usable. A fifth mode would join the
933
+ * modes' one-at-a-time rule (`workspaceOf`: opening one closes the
934
+ * others), so it could not share the screen with the board, which is
935
+ * exactly where an author chooses what to build with. This opens over
936
+ * anything, including the board.
937
+ */
938
+ libraryBrowserOpen: boolean;
939
+ /**
940
+ * The Insert drawer over the left column: closed (null), or open and asked
941
+ * to show `section`. A FRESH OBJECT per open, so asking again for the
942
+ * section already open scrolls back to it. Closed by any change of
943
+ * workspace (opening a mode), and never open for a viewer.
944
+ */
945
+ insertDrawer: { section: InsertSection } | null;
946
+ /** The author has met the drawer (closed it, or added anything). Remembered
947
+ * in `editorPrefs`; while false, an empty Project opens the drawer once. */
948
+ insertDrawerSeen: boolean;
949
+ /** A layer the drawer placed WITHOUT selecting it ("Use as background"):
950
+ * its outline row flashes and says where it went. The next selection
951
+ * clears it. */
952
+ justPlaced: string | null;
953
+ /**
954
+ * The planned shot currently being renamed, or null. Its name field shows in
955
+ * the Plan panel's head and on the shot board's bar (`PartName`).
956
+ *
957
+ * ONE SPELLING FOR EVERY SURFACE, so a rename started with F2 and one started
958
+ * from a double-click on the name are the same rename.
959
+ */
960
+ storyboardRenaming: string | null;
961
+ /**
962
+ * What the board has selected: reference ids ("ref_…"), still ids
963
+ * ("stl_…"), part ids ("part_…"), and the derived gap and cut ids. Its own field
964
+ * for `materialGraphSelection`'s reason, and revalidated on every document
965
+ * write beside it: a delete, an undo or an agent write can take a card away.
966
+ */
967
+ storyboardSelection: readonly string[];
968
+ /**
969
+ * The `animateMode` the board parked on open, or null while it is closed.
970
+ * The board is planning, so an armed key-on-change bit would turn every
971
+ * placement into keys. Close restores it, as Material does.
972
+ */
973
+ storyboardRestoreAnimate: boolean | null;
974
+ /**
975
+ * Whether a reference's "More" fold (every control of a component card) is
976
+ * open. A component card's "All controls" sets it, so the fold the
977
+ * inspector shows is the one the card asked for. A view pref.
978
+ */
979
+ referenceMoreOpen: boolean;
980
+ /**
981
+ * Whether the board's Project assets panel is open in the right column
982
+ * (`boardRightPanel` "assets"), from the board's + menu. Where "Keep in
983
+ * Assets" lands and a file is uploaded, removed or dragged onto a card.
984
+ * False whenever the board is closed. Session state, never saved.
985
+ */
986
+ boardAssetsOpen: boolean;
987
+ /**
988
+ * The category the library browser opens on, or null to keep its own. A
989
+ * mailbox: `setLibraryBrowserOpen(true, category)` parks it and the browser
990
+ * applies it once on open.
991
+ */
992
+ libraryBrowserCategory: "all" | LibraryItemKind | null;
993
+ /**
994
+ * The planned shots "Build planned shots" handed to the agent (S1). A card
995
+ * reads Building while its part is here and it is not marked built. All
996
+ * leave when the agent run that followed ends. Session state, never saved;
997
+ * a document switch drops it.
998
+ */
999
+ boardBuilding: { part: string }[];
1000
+ /** True once an agent run started after the last "Build planned shots",
1001
+ * so the run's end clears `boardBuilding`. */
1002
+ boardBuildingRan: boolean;
1003
+ /** Files dropped or pasted on the board, waiting for the drop dialog's
1004
+ * scope (S2). Null while the dialog is closed. */
1005
+ boardDrop: { files: File[] } | null;
1006
+ /**
1007
+ * The last "Add motion" request: the planned shot and the Shot it asked the
1008
+ * agent to change. The review card warns when a proposal redraws any other
1009
+ * Shot, and copies this part's prompt. Transient: a document switch drops it.
1010
+ */
1011
+ motionRequest: { partId: string; sceneId: string | null } | null;
1012
+ /** Text the agent composer adopts on its next render. A fresh identity per
1013
+ * ask, consumed once by the mounted composer. */
1014
+ agentPrefill: { text: string } | null;
1015
+ /** Layers the timeline dock shows on its next render. A fresh identity per
1016
+ * ask, consumed once by the mounted dock, `agentPrefill`'s shape. */
1017
+ dockReveal: DockReveal | null;
121
1018
  /**
122
1019
  * Triangle counts by mesh layer id, pushed up from the stage as models load.
123
1020
  *
@@ -127,6 +1024,16 @@ export interface ShotEditorState {
127
1024
  * layer whose model has not arrived yet reports nothing at all.
128
1025
  */
129
1026
  meshTriangles: Readonly<Record<string, number>>;
1027
+ /**
1028
+ * The glTF materials each loaded model ships, pushed up from the stage as
1029
+ * models parse.
1030
+ *
1031
+ * The same push-not-poll argument the triangle count makes: a material index,
1032
+ * name and map set live in the FILE, so no amount of reading the document
1033
+ * tells the slot rail that slot 3 is called "Screen". Absent for a layer
1034
+ * whose model has not loaded, and for every primitive.
1035
+ */
1036
+ meshMaterialSlots: Readonly<Record<string, readonly MeshMaterialSlotInfo[]>>;
130
1037
  /**
131
1038
  * The subtree being edited in isolation, or null for the whole scene.
132
1039
  *
@@ -161,6 +1068,26 @@ export interface ShotEditorState {
161
1068
  /** The shading the working pane was in before the current one, so ⇧Z can
162
1069
  * toggle wireframe and come BACK rather than stranding the author in it. */
163
1070
  previousShading: ShadingMode;
1071
+ /**
1072
+ * The shading to put back on leaving the open definition, or null if there is
1073
+ * nothing to put back.
1074
+ *
1075
+ * ── WHY A DEFINITION OPENS IN MATERIAL ──────────────────────────────────
1076
+ *
1077
+ * Clay is a MODELLING aid: it strips colour and texture so silhouette and
1078
+ * proportion read. A component definition is made of exactly what clay
1079
+ * strips — painted layers, stacked, drawn with `depthTest` off — so the
1080
+ * default solid shading rendered an opened definition as ONE featureless grey
1081
+ * rectangle. It looked like the editor had hung.
1082
+ *
1083
+ * The same argument the material picker already makes on close: a look you
1084
+ * cannot see is a choice you cannot make.
1085
+ *
1086
+ * SEPARATE FROM `previousShading`, which belongs to ⇧Z's wireframe toggle.
1087
+ * One field serving both would make leaving a definition and pressing ⇧Z
1088
+ * fight over the same slot.
1089
+ */
1090
+ shadingBeforeDef: ShadingMode | null;
164
1091
  /**
165
1092
  * The component definition the OUTLINE is scoped to, or null for the scene.
166
1093
  *
@@ -175,6 +1102,136 @@ export interface ShotEditorState {
175
1102
  * every write beside `isolation` — a definition can be deleted too.
176
1103
  */
177
1104
  editingDef: string | null;
1105
+ /**
1106
+ * The DEF-LOCAL playhead while a definition is open (unify P2). A SECOND
1107
+ * field beside `frame` on purpose: `frame` is master time and every
1108
+ * playhead-paired verb (addShotAtPlayhead, Animate's keys, audio preview, the
1109
+ * scene render) reads it — repurposing it def-locally would silently corrupt
1110
+ * all of them. While `editingDef` is null this is inert. Clamped against
1111
+ * `defDurationFrames` at the same seam `editingDef` is validated.
1112
+ */
1113
+ defFrame: number;
1114
+ /**
1115
+ * The chrome's zoom over the Studio's head-on render. 1 = the whole canvas
1116
+ * fitted, 2 = twice as large.
1117
+ *
1118
+ * EDITOR STATE, pushed to the stage with the component context: the stage
1119
+ * frames the definition from `def.size`, so the zoom is a scale on that
1120
+ * frame's half-extents rather than anything the document records.
1121
+ *
1122
+ * NOT a workspace pref, and deliberately not: it is scoped to ONE definition's
1123
+ * canvas, so carrying it to the next definition would open a component the
1124
+ * author has never seen at a magnification they chose for another one. It
1125
+ * resets to 1 on every entry, with the rest of the cleared-on-leave set.
1126
+ */
1127
+ studioZoom: number;
1128
+ /**
1129
+ * THE OTHER COMPONENT FAMILY'S OPEN SUBJECT: a registry id
1130
+ * ("<module>#<Export>") whose source the code studio is showing, or null.
1131
+ *
1132
+ * A SECOND FIELD BESIDE `editingDef` rather than a widened one, for the
1133
+ * reason `defFrame` is a second field beside `frame`: the two subjects are
1134
+ * different KINDS. A definition is a document object with an interior, an
1135
+ * exposed surface and named actions, and every one of those is a panel that
1136
+ * reads `editingDef`. A code component is a .tsx file the author owns; it has
1137
+ * a source buffer and a contract, and nothing else in this chrome. One field
1138
+ * answering both would have every reader ask "which sort is it" first.
1139
+ *
1140
+ * MUTUALLY EXCLUSIVE with `editingDef`: both mean "the stage is rendering one
1141
+ * component standalone", and the stage holds one context. Each entry clears
1142
+ * the other, and `pushComponentContext` is where the two meet.
1143
+ */
1144
+ codeComponent: string | null;
1145
+ /**
1146
+ * The defaults rail's LIVE overrides, contract key -> engine Value.
1147
+ *
1148
+ * A scrub writes here and pushes the component context; nothing lands in any
1149
+ * document. A committed key is HELD here until the fresh contract arrives
1150
+ * (`loadContract` clears it), so the preview never flashes the old default
1151
+ * between the write landing in source and the re-extraction being adopted.
1152
+ */
1153
+ codePreviewProps: Readonly<Record<string, Value>>;
1154
+ /**
1155
+ * FOLLOW MODE: the project file Code Studio is following an MCP agent in,
1156
+ * or null. While the agent keeps writing it, its buffer is read-only and
1157
+ * reloads on each write (`adoptSourceChange`), and the preview takes the
1158
+ * props and frame of each `preview_component` the agent asks for. "Take
1159
+ * over" (`takeOverCodeFile`) ends it.
1160
+ */
1161
+ codeFollow: string | null;
1162
+ /** A file the author asked to follow that the registry does not list yet:
1163
+ * it opens in follow mode as soon as it does (`refreshLibrary`). */
1164
+ codeFollowPending: string | null;
1165
+ /**
1166
+ * The Components props panel's TRIED values, prop key -> engine Value, for
1167
+ * the open subject (a code component's contract keys, or a definition's
1168
+ * exposed keys).
1169
+ *
1170
+ * While `componentApplyAll` is off, every props-panel edit lands here and
1171
+ * nowhere else: the preview draws it (pushed on the component context) and
1172
+ * no document, file or instance changes. Turning `componentApplyAll` on
1173
+ * saves these to the component and clears them. Dropped whenever the
1174
+ * subject changes (the `set` seam), so a tried value never follows the
1175
+ * author to another component.
1176
+ */
1177
+ componentPlay: Readonly<Record<string, Value>>;
1178
+ /**
1179
+ * The props panel's "Apply to all instances" checkbox. Off (the default) =
1180
+ * try values in the preview only (`componentPlay`). On = every edit saves to
1181
+ * the component itself, which every instance without its own value follows.
1182
+ * Resets to off with the subject.
1183
+ */
1184
+ componentApplyAll: boolean;
1185
+ /** The key under an active scrub gesture, or null. Its override survives a
1186
+ * contract landing mid-drag (another key's commit must not yank this one). */
1187
+ codeScrubKey: string | null;
1188
+ /** Keys whose defaults commit is IN FLIGHT. The held-override release in
1189
+ * `loadContract` keeps these too: a contract landing for key A's commit
1190
+ * must not yank key B's still-travelling one — that would flash B's old
1191
+ * default on stage for the exact window the hold exists to cover. */
1192
+ codePendingDefaults: readonly string[];
1193
+ /** Each default the author changed while this component is open, as it was
1194
+ * before the first change: what the props head's Reset puts back. Kept by
1195
+ * the store, so it survives the panel remounting, and dropped with the
1196
+ * defaults history (another component, or leaving Code Studio). */
1197
+ codeDefaultsBaseline: Readonly<Record<string, Value>>;
1198
+ /** The last defaults-commit refusal or failure, shown at the top of the
1199
+ * rail. Cleared by the next gesture. */
1200
+ codeDefaultsError: string | null;
1201
+ /** Per-component defaults metadata (target file, writability, per-key
1202
+ * editability), cached beside `contracts` and evicted at the same seams. */
1203
+ contractDefaults: Readonly<Record<string, ComponentDefaultsMeta>>;
1204
+ /**
1205
+ * Every open buffer, keyed by project-relative posix path.
1206
+ *
1207
+ * A MAP AND NOT ONE FILE, because the subject and the buffer stopped being
1208
+ * the same question. A component is one export in one module, but the work of
1209
+ * changing it reaches the helper beside it, the types it imports and the
1210
+ * config that includes it — and an editor that can only hold the file it
1211
+ * previewed makes the author leave to read any of them.
1212
+ *
1213
+ * Keyed by rel rather than an array of tabs: rel IS the identity (`/api/files`
1214
+ * takes one, the diagnostics route takes one, `CodeEditor`'s state cache keys
1215
+ * on one), and a list would need a lookup on every one of those.
1216
+ */
1217
+ codeFiles: Readonly<Record<string, CodeFileState>>;
1218
+ /** Which buffer the editor is showing, or null when none is open. Always a
1219
+ * key of `codeFiles` or null — nothing may point at a buffer that is not
1220
+ * there, and `closeCodeFile` picks the neighbour rather than leaving this
1221
+ * dangling. */
1222
+ codeActive: string | null;
1223
+ /** Tab order, oldest first. SEPARATE from `codeFiles` because an object's key
1224
+ * order is a language detail and a tab strip's order is a promise to the
1225
+ * author: tabs must not reshuffle when a buffer is replaced. */
1226
+ codeOrder: readonly string[];
1227
+ /** Every editable file in the project, flat, as `GET /api/files` answers.
1228
+ * The tree's shape is drawn from these paths, never stored. */
1229
+ projectFiles: readonly ProjectFile[];
1230
+ projectFilesLoaded: boolean;
1231
+ /** The server's walk hit its cap, so this listing is INCOMPLETE. Said in the
1232
+ * tree, because a file browser that quietly omits files is worse than none. */
1233
+ projectFilesTruncated: boolean;
1234
+ projectFilesError: string | null;
178
1235
  /** The export dialog is open. A store bit rather than local state, because
179
1236
  * the keymap opens it (⌘E) and the header's button toggles it, and two
180
1237
  * places holding "is it open" is one place for them to disagree. */
@@ -187,11 +1244,46 @@ export interface ShotEditorState {
187
1244
  * away from the modal without cancelling anything.
188
1245
  */
189
1246
  renderJob: RenderJob | null;
1247
+ /**
1248
+ * A render of THIS document is running, either way it was started.
1249
+ *
1250
+ * ONE lock both export paths write, because both mean the same thing to the
1251
+ * chrome: the transport is held, Space and the Play menu refuse, and the
1252
+ * progress on screen comes from here. It is NOT the stage's own
1253
+ * `exporting` (bridgeClient) — that one is the Tier-1 deferral flag and says
1254
+ * nothing about a server render.
1255
+ *
1256
+ * `where` is what the chrome does about it. A BROWSER export renders through
1257
+ * the visible stage by design, so the stage is veiled and says so; a SERVER
1258
+ * render leaves the stage alone and only pauses playback, because the machine
1259
+ * is busy but the canvas is still the author's.
1260
+ */
1261
+ exportLock: {
1262
+ where: "browser" | "server";
1263
+ done: number;
1264
+ total: number;
1265
+ /** What a server render is doing before its first frame (F98), such as
1266
+ * the one-time browser download. */
1267
+ note?: string;
1268
+ } | null;
190
1269
  // ── the agent drawer ─────────────────────────────────────────────────────
191
1270
  //
192
1271
  // Held in the STORE and not in the panel, for the reason the render job is:
193
1272
  // closing the drawer must not cancel a running turn or lose its transcript.
194
1273
  agentOpen: boolean;
1274
+ /** The right column's Comments tab is the one picked (plan C5). The agent
1275
+ * wins the column while it is open; the tab comes back when it closes. */
1276
+ commentsOpen: boolean;
1277
+ /** The Studio's Brief tab is the one picked (S4): the planned shot the
1278
+ * Shot under the playhead was built from. Off the board only; the agent and
1279
+ * Comments win the column while open, as over the inspector. */
1280
+ briefOpen: boolean;
1281
+ /** The comment tool (C): off the board, a click on the camera pane drafts
1282
+ * a Studio thread (bridge v45, S5). Null when not armed. */
1283
+ boardTool: "comment" | null;
1284
+ /** The open Project takes comments from this caller
1285
+ * (`LoadedShot.can.comment`). A missing `can` (an older server) is true. */
1286
+ canComment: boolean;
195
1287
  agentItems: readonly AgentTranscriptItem[];
196
1288
  agentRunning: boolean;
197
1289
  agentError: string | null;
@@ -213,18 +1305,97 @@ export interface ShotEditorState {
213
1305
  railCollapsed: boolean;
214
1306
  inspectorCollapsed: boolean;
215
1307
  /**
216
- * What entering the working view auto-collapsed, per panel: the collapse
217
- * value to restore on leaving, or null when there is nothing pending —
218
- * either the mode is not on, or the author toggled that panel themselves
219
- * inside it and took ownership (the collapse verbs null the slot).
1308
+ * What the focus mode auto-collapsed, per panel: the collapse value to
1309
+ * restore on leaving it, or null when there is nothing pending. Either the
1310
+ * mode is off, or the author toggled that panel themselves inside it and
1311
+ * took ownership (the collapse verbs null the slot).
1312
+ *
1313
+ * The DOCK has no slot, and that is the current rule rather than an
1314
+ * oversight: no view collapses the timeline any more. See `focusHides`.
220
1315
  */
221
1316
  focusRestoreInspector: boolean | null;
222
- focusRestoreDock: boolean | null;
1317
+ focusRestoreRail: boolean | null;
1318
+ /**
1319
+ * Whether the focus mode is engaged, or null for off.
1320
+ *
1321
+ * There is exactly ONE now, and it belongs to one place: Video at the SCENE
1322
+ * level, where the subject is the finished cut rather than anything to edit,
1323
+ * so both side rails go and the picture gets the full width. Inside a shot
1324
+ * NEITHER view hides a panel: the author is building there and the panels
1325
+ * are what they build with. See `applyFocus` in store.ts.
1326
+ *
1327
+ * ── WHY THIS IS STORED AND NOT DERIVED ──────────────────────────────────
1328
+ *
1329
+ * It is the "before" of every transition, and the input it would be derived
1330
+ * from cannot supply one: `video` is the same flag everywhere and survives
1331
+ * a trip into a shot, so it means "rails away" at the scene level and
1332
+ * "nothing engaged" one level down, and a CONTEXT change with the flag
1333
+ * unchanged is a real transition in both directions. The restore slots cannot supply it either:
1334
+ * they are null both when the mode was never entered and when the author
1335
+ * took ownership inside it, so a mode with its slots surrendered would look
1336
+ * unengaged and re-collapse the panels the author had just re-opened, on
1337
+ * the next edit, because `syncContextStage` runs on every one of them.
1338
+ *
1339
+ * Stored, the answer is a comparison: same key, nothing happens.
1340
+ */
1341
+ focusHides: "video" | null;
1342
+ /**
1343
+ * THE AUTHOR IS WATCHING, not working.
1344
+ *
1345
+ * Video is the export's picture with the authoring taken off it: no handles,
1346
+ * no hover, no drawing, no drags. The transport runs in Edit and Video alike.
1347
+ * It is a FLAG OVER THE PICTURE and never a pane layout, which is the whole
1348
+ * correction this field carries. It was `sceneVideo` and applied at the scene
1349
+ * level alone, because a shot's Video was spelled as camera mode
1350
+ * (`stageLayout.mode`) instead. That could not survive contact with the
1351
+ * facts: the rect, ellipse, line, text, image and pen tools draw in the
1352
+ * CAMERA pane and nowhere else, so "Video is the camera pane" made drawing
1353
+ * impossible in the view that could not play and possible in the one that
1354
+ * could.
1355
+ *
1356
+ * So `stageLayout.mode` is EDIT'S LAYOUT now (the 3D viewport, the split, the
1357
+ * frame, on keys ⇧1 / ⇧2 / ⇧3) and this is orthogonal to it. Entering Video
1358
+ * never writes the layout; leaving shows it again. Above a shot it ALSO
1359
+ * puts both side rails away, which is the one focus mode left in the product
1360
+ * (`focusKeyFor`).
1361
+ *
1362
+ * THE WAY OUT IS ANY EDIT. Every selection verb, the document seam, both
1363
+ * scrub lanes, the tool arm and the four "go somewhere to work" doors call
1364
+ * `enterEdit()`, so the first touch of anything lands the author in Edit,
1365
+ * paused, on the frame they were looking at. See the seam list in store.ts.
1366
+ *
1367
+ * A WORKSPACE VALUE: it survives a document switch, like `stageLayout` and
1368
+ * the slots that ride with it, and it is deliberately NOT persisted to
1369
+ * storage: a reload opens on the work, not on the screening room.
1370
+ */
1371
+ video: boolean;
223
1372
  /** Dragged sizes, in PIXELS — see state/panelSize.ts for why not `vh`. */
224
1373
  railWidth: number;
225
1374
  dockHeight: number;
1375
+ /** The RIGHT rail's width. It was a CSS literal with no writer at all until
1376
+ * the UX overhaul, which is why the panel had been worked around (labels
1377
+ * shortened to fit) rather than widened. State now, for the reason
1378
+ * `railWidth` is state, and it reaches CSS the same way: `--inspector-w`,
1379
+ * written imperatively from App.tsx. */
1380
+ inspectorWidth: number;
1381
+ /**
1382
+ * How far the chrome reaches in over the stage on each side, measured.
1383
+ *
1384
+ * The stage iframe is pinned `inset: 0` and the panels are docked over it, so
1385
+ * only the shell can know this — it measures the boxes and pushes the four
1386
+ * numbers down the bridge for the overlay's furniture and the derived
1387
+ * framing (see `setStageInset`).
1388
+ *
1389
+ * IT IS STATE AND NOT A CLOSURE because the CHROME needs it too. The split
1390
+ * divider divides the part the author can see, not the window, so the
1391
+ * handle's own arithmetic reads the same four numbers the stage does. A
1392
+ * second measurement in App.tsx would be a second answer to a question with
1393
+ * one true answer, and the two would disagree exactly while a panel moves.
1394
+ */
1395
+ stageInset: StageInset;
226
1396
  /** Pixels per frame in the dock. The unit @dailies-studio/timeline's geometry
227
- * and `useAnchoredZoom` both speak, so there is no second zoom scale. */
1397
+ * and `useAnchoredZoom` both speak, so there is no second zoom scale. Its
1398
+ * bounds and its default are `DOCK_PPF` in state/panelSize.ts. */
228
1399
  dockPpf: number;
229
1400
 
230
1401
  /**
@@ -259,17 +1430,26 @@ export interface ShotEditorState {
259
1430
  * shortcut point at it, so it follows the POINTER and not the selection. */
260
1431
  activePane: StagePane;
261
1432
  /**
262
- * "Camera follows this view": orbiting the working pane writes the shot
263
- * camera's framing continuously.
1433
+ * PILOTING: the Camera pane's navigation (middle-drag orbit, shift-drag
1434
+ * pan, wheel zoom, R roll) writes the shot camera through the framing
1435
+ * gesture lane instead of moving the pane's viewport.
264
1436
  *
265
- * A WORKSPACE PREF like the other two, and deliberately NOT called a lock.
266
- * The word says "nothing can move" when what is true is the opposite — the
267
- * camera is being moved by everything you do. It is armed only by its own
268
- * control, never as a side effect of entering a mode, for the reason
269
- * `animateMode` is: a bit that silently makes drags write the document must
270
- * be turned on on purpose.
1437
+ * It replaced "camera follows this view", which projected every 3D-view
1438
+ * orbit onto a camera that cannot tilt and left the two panes disagreeing
1439
+ * mid-drag. Session-only and deliberately NOT called a lock: nothing is
1440
+ * locked, the camera is being moved by everything you do. It is armed only
1441
+ * by its own control or its key, never as a side effect of entering a mode,
1442
+ * for the reason `animateMode` is: a bit that silently makes drags write the
1443
+ * document must be turned on on purpose.
1444
+ */
1445
+ pilot: boolean;
1446
+ /**
1447
+ * The component that drives the render camera, by name, or null while the
1448
+ * document's rig does. Reported by the stage (bridge v29 `onCameraDriver`):
1449
+ * the inspector's Camera fields and the camera verbs describe a rig the
1450
+ * picture ignores while this is set, and they say so.
271
1451
  */
272
- cameraFollowsView: boolean;
1452
+ cameraDriver: string | null;
273
1453
  /**
274
1454
  * The WORKING pane's shading (solid / wireframe / material / rendered).
275
1455
  *
@@ -279,8 +1459,124 @@ export interface ShotEditorState {
279
1459
  * sentence with no footnotes.
280
1460
  */
281
1461
  shadingMode: ShadingMode;
1462
+ /** The working pane's grid floor: how the author looks at the scene, never
1463
+ * something about the shot. Remembered across reloads in `editorPrefs`. */
1464
+ showGrid: boolean;
1465
+ /**
1466
+ * The two DIAGNOSTIC chips in the canvas bar, shown or hidden.
1467
+ *
1468
+ * Per-viewer prefs on `showGrid`'s rule: how the author is looking at the
1469
+ * chrome, never anything about the shot, and remembered across reloads in
1470
+ * `editorPrefs`. They exist because both chips are for the person
1471
+ * BUILDING the film and neither belongs in a recording of it.
1472
+ *
1473
+ * ONLY these two. The save-state and codegen chips are not on this switch and
1474
+ * must not join it: they are the chrome telling the author something is wrong
1475
+ * or unsaved, and a warning with an off switch is a warning that will be off
1476
+ * on the day it matters.
1477
+ */
1478
+ showFpsBadge: boolean;
1479
+ showBackendBadge: boolean;
1480
+ /**
1481
+ * A single plain pick of a layer moves a paused playhead onto it (gap 4),
1482
+ * when the playhead is outside the layer's lifespan. A per-viewer
1483
+ * pref on `showGrid`'s rule, kept in `editorPrefs`, and the View menu's
1484
+ * "Move playhead to selection" row.
1485
+ */
1486
+ selectMovesPlayhead: boolean;
1487
+ /**
1488
+ * THE REST OF THE PREFERENCES PAGE (gap 5, Paper page 24), each a per-viewer
1489
+ * pref on `showGrid`'s rule: kept in `editorPrefs`, synced to the account
1490
+ * when hosted, never in the document, and carried across a document switch.
1491
+ *
1492
+ * `motionPaths`: the selected layer's motion path on the stage.
1493
+ * `rulers`: the camera pane's rulers (and the guides they create).
1494
+ * `smartGuides`: snapping to the frame and the other layers while dragging.
1495
+ * User guides snap either way; Ctrl drops both for one drag.
1496
+ * `blockLabels`: camera move bar names, when they fit or never.
1497
+ * `fieldLabels`: inspector fields show a glyph in the pill, or the name above.
1498
+ * `toolbarPosition`: the tool footer under the canvas, or above it.
1499
+ */
1500
+ motionPaths: boolean;
1501
+ rulers: boolean;
1502
+ smartGuides: boolean;
1503
+ blockLabels: "fit" | "off";
1504
+ fieldLabels: "compact" | "named";
1505
+ toolbarPosition: "bottom" | "top";
1506
+ /**
1507
+ * What a played frame may render at (bridge v52, `PlaybackResolution`):
1508
+ * Auto lowers it below the display's density only while the GPU cannot keep
1509
+ * up, Full never does, Fast starts at 0.7 of it. Pushed to the stage with
1510
+ * the rulers (`setEditorChrome`). Kept in `editorPrefs` on this browser
1511
+ * only, not on the account: it is a fact about this machine's GPU, which
1512
+ * another device does not share.
1513
+ */
1514
+ playbackResolution: PlaybackResolution;
1515
+ /** The Preferences dialog is open. A store bit for `exportOpen`'s reason:
1516
+ * ⌘, , the app menu row and the agent's `prefs.open` all open it. */
1517
+ prefsOpen: boolean;
282
1518
  /** What the stage reported on ready, or null before it did. */
283
1519
  stageInfo: ShotStageInfo | null;
1520
+ /**
1521
+ * What the stage PRESENTED over the last second, or null when it is not
1522
+ * playing.
1523
+ *
1524
+ * Written once a second during playback and retracted on stop. Like
1525
+ * `sceneId`'s per-cut write it is hot-only while `playing` (store `set`), so
1526
+ * the fps chip reads it hot and no other panel wakes for it. Null is the
1527
+ * resting state and means "no measurement", not "zero frames": the chrome's
1528
+ * own seek cadence is not an answer here, because the stage coalesces
1529
+ * requests and drops publications exactly when the two disagree.
1530
+ */
1531
+ stagePerf: StagePerformanceReport | null;
1532
+ /**
1533
+ * The pieces of the Shot the stage could not draw and isolated (bridge v34
1534
+ * `onPieceFailures`): a layer, a background, a post stack or a transition
1535
+ * that threw while the rest kept drawing. Whole on every change, empty once
1536
+ * everything renders again, and empty with no stage attached. The stage
1537
+ * badge shows it, and the stage refuses an export of any frame it touches.
1538
+ */
1539
+ stagePieceFailures: readonly StagePieceFailure[];
1540
+ /** The stage's render fallbacks (`health().renderErrors`): an html layer
1541
+ * that failed to decode, a font or glyph engine failure, a missing
1542
+ * texture. Mirrored on each health change for the same badge. */
1543
+ stageRenderErrors: readonly string[];
1544
+ /**
1545
+ * The stage still OWES THE OPEN SUBJECT its first whole presentation
1546
+ * (bridge v51 `onStageBusy`): the Project, a component context or a
1547
+ * material preview, not yet drawn whole by this renderer. Once it has been,
1548
+ * later loads (a font, a texture, a model) hold nothing.
1549
+ *
1550
+ * FALSE WHEN NO STAGE IS ATTACHED, and that is the contract rather than an
1551
+ * accident: a bare store, a unit test and SSR are all "ready". False too
1552
+ * while the stage cannot present at all (a crash, a lost context, a
1553
+ * renderer compile failure), so a queued Play never waits on it for ever.
1554
+ * `onReady` is not this signal: it fires when the RENDERER is up, before
1555
+ * anything is drawn. Read through `stageReady`.
1556
+ */
1557
+ stageBusy: boolean;
1558
+ /**
1559
+ * A STAGE IS ON ITS WAY: a connect attempt has begun and none has reported
1560
+ * in yet.
1561
+ *
1562
+ * `stageBusy` cannot answer this. It is FALSE when no stage is attached, by
1563
+ * the contract above, so Play used to start for the whole iframe boot: the
1564
+ * stage document, a renderer adapter request and the first commit, with
1565
+ * nothing on the canvas but the stage's own boot ground. Now a press there
1566
+ * is queued (`playIntent` armed) until the stage reports in and draws.
1567
+ *
1568
+ * Read together with `stageInfo === null`, which is what "has not reported
1569
+ * in" means: `onReady` is the single writer of that field, and it fires when
1570
+ * the renderer is up on the document the chrome pushed.
1571
+ *
1572
+ * Written by `StagePanel` alone: true from its first commit (hosted, through
1573
+ * the code host's boot too), and again for the document a reload remints;
1574
+ * false when the attempt fails or the panel unmounts, which also cancels a
1575
+ * queued Play, so a bridge version mismatch or a poll timeout hands the
1576
+ * button back instead of leaving it waiting. A bare store, a unit test and
1577
+ * SSR never mount `StagePanel`, which is what keeps them ready.
1578
+ */
1579
+ stageExpected: boolean;
284
1580
  /** A stage connect or runtime failure, for the overlay strip. */
285
1581
  stageError: string | null;
286
1582
 
@@ -298,10 +1594,70 @@ export interface ShotEditorState {
298
1594
  /** Lazily fetched contracts by component id — the inspector's row source. */
299
1595
  contracts: Readonly<Record<string, Contract>>;
300
1596
 
1597
+ // ── the shipped library, for the board's Library lane ────────────────────
1598
+ /** Every `GET /api/library/catalog` row: built-ins, installed shared
1599
+ * components and the SHIPPED items. The rail lists the non-builtins; a
1600
+ * `library` card looks its row up here for the thumb and the docs. */
1601
+ libraryCatalog: readonly LibraryItem[];
1602
+ /** `libraryLoaded`'s rule, for the same reason: a failed fetch also ends
1603
+ * with an empty list, and "no items" must never look like "not read". */
1604
+ libraryCatalogLoaded: boolean;
1605
+ libraryCatalogError: string | null;
1606
+ /** The items the registry REFUSED to read: a lying `library.json`, files that
1607
+ * drifted from `library-manifest.json`, a manifest entry with no directory.
1608
+ * They are omitted from `libraryCatalog`, so without these rows an item this
1609
+ * build broke is indistinguishable from one it never shipped. Each error
1610
+ * names its own fix. */
1611
+ libraryCatalogProblems: readonly { id: string; error: string }[];
1612
+ /** Lazily fetched item details by id (`loadLibraryItem`), the inspector's
1613
+ * Docs source. */
1614
+ libraryItems: Readonly<Record<string, LibraryItemDetail>>;
1615
+
1616
+ /** Server-authoritative media usage for the open Project. Null while it has
1617
+ * not been read, or when the current server predates the scoped asset API. */
1618
+ assetUsage: ProjectAssetUsage | null;
1619
+
301
1620
  saveState: SaveState;
302
1621
  /** Unsaved edits exist. Separate from `saveState` because a save that is
303
1622
  * in flight while a new edit lands is BOTH "saving" and dirty. */
304
1623
  dirty: boolean;
1624
+ /** Live sync only (null without it): an edit exists that is only in this
1625
+ * tab's memory, not yet in IndexedDB. Under sync THAT is what a closed tab
1626
+ * loses, so the close guard reads it instead of `dirty`. */
1627
+ syncUnstored: boolean | null;
1628
+ /** Live sync only (null without it): where this device's changes stand,
1629
+ * for the save chip, its popover and the quota banner. A `hold` makes the
1630
+ * Project read only until it clears (`readOnlyReason` says why). */
1631
+ syncStatus: SyncStatus | null;
1632
+ /** The open Project moved on the server under unsaved edits: another tab or
1633
+ * an agent wrote it ("changed"), or it is gone ("deleted"). Autosave stops
1634
+ * while it is set, so the same 409 is not sent on every edit, and the
1635
+ * shell's banner offers the one way on (`reloadLatest`). */
1636
+ saveConflict: "changed" | "deleted" | null;
1637
+ /** Live sync (R10): the open Project was renamed under the session. The
1638
+ * header's short note, "Renamed to X by Maya"; it clears itself. `by` is
1639
+ * the person's name, null when it was this user or nobody is known. */
1640
+ syncRenamed: { to: string; by: string | null; color: string | null } | null;
1641
+ /** Live sync (R10): the open Project went to the Trash under changes of
1642
+ * this tab that never reached the server. The dialog offers `restoreDeleted`
1643
+ * or `saveDeletedAsCopy`; the editor is read only meanwhile. */
1644
+ syncDeleted: SyncDeleted | null;
1645
+ /** The open Project is view only: the server said this caller may not edit
1646
+ * it (`LoadedShot.can.edit === false`, a hosted Viewer), or refused a save
1647
+ * as forbidden. Every document and server write is refused with
1648
+ * `VIEW_ONLY` (`viewOnly` in store.ts) and nothing autosaves. */
1649
+ readOnly: boolean;
1650
+ /** The Version history panel is open in the right column. */
1651
+ versionsOpen: boolean;
1652
+ /** The panel's name field is up and focused (Cmd+S, "Save version"). */
1653
+ versionNameAsk: boolean;
1654
+ /** The open Project's version rows, or null before the first read. */
1655
+ versionList: ShotVersionList | null;
1656
+ /** A version on screen in place of the live document. The chrome shows
1657
+ * `doc` read-only (`readOnly` is true while it is up); the history keeps
1658
+ * the live document, and `roleReadOnly` keeps what the server's role
1659
+ * said, so leaving the preview puts both back. */
1660
+ versionPreview: { version: ShotVersion; roleReadOnly: boolean } | null;
305
1661
  /** The last API failure, in words the author can act on. */
306
1662
  error: string | null;
307
1663
  /** Codegen refused to overwrite a hand-edited generated file. Sticky until
@@ -318,33 +1674,131 @@ export const INITIAL_VIEW: ShotView = { zoom: 1, panX: 0, panY: 0 };
318
1674
  /** Camera only, because that is the pane that IS the export: an editor that
319
1675
  * opened onto a working pane would open onto a view of the shot that is not
320
1676
  * the shot. */
321
- export const INITIAL_STAGE_LAYOUT: StageLayout = { mode: "camera", splitRatio: 0.5 };
1677
+ export const INITIAL_STAGE_LAYOUT: StageLayout = {
1678
+ mode: "camera",
1679
+ splitRatio: 0.5,
1680
+ };
322
1681
 
323
1682
  export const INITIAL_STATE: ShotEditorState = {
324
- shots: [],
325
1683
  name: null,
326
1684
  doc: null,
327
1685
  rev: null,
328
1686
  frame: 0,
329
1687
  playing: false,
1688
+ playRate: 1,
1689
+ loopPlayback: true,
1690
+ playIntent: null,
1691
+ playOrigin: null,
1692
+ workArea: null,
330
1693
  sceneId: null,
1694
+ enteredScene: null,
331
1695
  selection: [],
332
1696
  isolation: null,
333
1697
  isolationOrbit: false,
334
1698
  previousShading: "solid",
1699
+ shadingBeforeDef: null,
335
1700
  editingDef: null,
1701
+ defFrame: 0,
1702
+ studioZoom: 1,
1703
+ codeComponent: null,
1704
+ codePreviewProps: {},
1705
+ codeFollow: null,
1706
+ codeFollowPending: null,
1707
+ componentPlay: {},
1708
+ componentApplyAll: false,
1709
+ codeScrubKey: null,
1710
+ codePendingDefaults: [],
1711
+ codeDefaultsBaseline: {},
1712
+ codeDefaultsError: null,
1713
+ contractDefaults: {},
1714
+ codeFiles: {},
1715
+ codeActive: null,
1716
+ codeOrder: [],
1717
+ projectFiles: [],
1718
+ projectFilesLoaded: false,
1719
+ projectFilesTruncated: false,
1720
+ projectFilesError: null,
336
1721
  tool: "select",
337
- inspectorMode: "design",
1722
+ workPlanes: {},
1723
+ splineBasis: "bezier",
1724
+ splineEdit: null,
338
1725
  animateMode: false,
339
- selectedBlock: null,
1726
+ selectedMove: null,
1727
+ selectedKeyframes: [],
1728
+ dockMode: "rows",
340
1729
  selectedMarker: null,
1730
+ selectedAudio: [],
1731
+ soloTracks: [],
1732
+ snapBeats: true,
1733
+ dockNotice: null,
341
1734
  imagePick: null,
342
1735
  stageMenu: null,
343
- materialPicker: null,
1736
+ appMenu: null,
1737
+ searchFocus: null,
1738
+ codeBackRequest: null,
1739
+ materialWorkspace: null,
1740
+ materialColumnWidth: MATERIAL_COLUMN.default,
1741
+ codeLiveWidth: CODE_LIVE_WIDTH.default,
1742
+ materialGraphSelection: { nodeIds: [], edgeIds: [] },
1743
+ materialGraphPath: [],
1744
+ materialPalette: null,
1745
+ materialSnapToGrid: true,
1746
+ materialFrameRequest: null,
1747
+ materialCompile: { status: "idle" },
1748
+ storyboardOpen: false,
1749
+ storyboardPlan: null,
1750
+ storyboardSketch: null,
1751
+ sketchSelection: [],
1752
+ sketchTool: "select",
1753
+ sketchPlaying: false,
1754
+ sketchGuide: null,
1755
+ returnTarget: "stage",
1756
+ componentsOpen: false,
1757
+ componentsTab: "preview",
1758
+ componentsSelection: null,
1759
+ componentsOrigin: null,
1760
+ // Dark, the desk, from the start: the Backdrop control lights what the
1761
+ // stage draws. Null sent nothing and left the working pane's own grey.
1762
+ componentPreview: { background: "dark", loopFrames: null },
1763
+ workspaceComponents: null,
1764
+ componentHomes: null,
1765
+ fontApply: null,
1766
+ workspaceFonts: null,
1767
+ fontCatalog: null,
1768
+ componentVersions: {},
1769
+ componentBuilds: {},
1770
+ shareComponentOpen: false,
1771
+ componentHistory: null,
1772
+ componentUses: null,
1773
+ componentListingUpdates: {},
1774
+ componentUpdate: null,
1775
+ componentUpdated: null,
1776
+ libraryBrowserOpen: false,
1777
+ insertDrawer: null,
1778
+ insertDrawerSeen: false,
1779
+ justPlaced: null,
1780
+ storyboardRenaming: null,
1781
+ storyboardSelection: [],
1782
+ storyboardRestoreAnimate: null,
1783
+ referenceMoreOpen: false,
1784
+ boardAssetsOpen: false,
1785
+ libraryBrowserCategory: null,
1786
+ boardBuilding: [],
1787
+ boardBuildingRan: false,
1788
+ boardDrop: null,
1789
+ motionRequest: null,
1790
+ agentPrefill: null,
1791
+ dockReveal: null,
344
1792
  meshTriangles: {},
1793
+ meshMaterialSlots: {},
345
1794
  exportOpen: false,
346
1795
  renderJob: null,
1796
+ exportLock: null,
347
1797
  agentOpen: false,
1798
+ commentsOpen: false,
1799
+ briefOpen: false,
1800
+ boardTool: null,
1801
+ canComment: true,
348
1802
  agentItems: [],
349
1803
  agentRunning: false,
350
1804
  agentError: null,
@@ -357,28 +1811,217 @@ export const INITIAL_STATE: ShotEditorState = {
357
1811
  railCollapsed: false,
358
1812
  inspectorCollapsed: false,
359
1813
  focusRestoreInspector: null,
360
- focusRestoreDock: null,
1814
+ focusRestoreRail: null,
1815
+ focusHides: null,
1816
+ video: false,
361
1817
  railWidth: RAIL_WIDTH.default,
362
1818
  dockHeight: DOCK_HEIGHT.default,
363
- dockPpf: 6,
1819
+ inspectorWidth: INSPECTOR_WIDTH.default,
1820
+ stageInset: { top: 0, right: 0, bottom: 0, left: 0 },
1821
+ dockPpf: DOCK_PPF.default,
364
1822
  previewMuted: false,
365
1823
  previewVolume: 1,
366
1824
  view: INITIAL_VIEW,
367
1825
  stageLayout: INITIAL_STAGE_LAYOUT,
368
1826
  workingView: INITIAL_WORKING_VIEW,
369
1827
  activePane: "camera",
370
- cameraFollowsView: false,
1828
+ pilot: false,
1829
+ cameraDriver: null,
371
1830
  shadingMode: "solid",
1831
+ showGrid: true,
1832
+ showFpsBadge: true,
1833
+ showBackendBadge: true,
1834
+ selectMovesPlayhead: true,
1835
+ motionPaths: true,
1836
+ rulers: false,
1837
+ smartGuides: true,
1838
+ blockLabels: "fit",
1839
+ fieldLabels: "compact",
1840
+ toolbarPosition: "bottom",
1841
+ playbackResolution: "auto",
1842
+ prefsOpen: false,
372
1843
  stageInfo: null,
1844
+ stagePerf: null,
1845
+ stagePieceFailures: [],
1846
+ stageRenderErrors: [],
1847
+ stageBusy: false,
1848
+ stageExpected: false,
373
1849
  stageError: null,
374
1850
  library: [],
375
1851
  libraryLoaded: false,
376
1852
  libraryError: null,
377
1853
  contracts: {},
1854
+ libraryCatalog: [],
1855
+ libraryCatalogLoaded: false,
1856
+ libraryCatalogError: null,
1857
+ libraryCatalogProblems: [],
1858
+ libraryItems: {},
1859
+ assetUsage: null,
378
1860
  saveState: "idle",
379
1861
  dirty: false,
1862
+ syncUnstored: null,
1863
+ syncStatus: null,
1864
+ saveConflict: null,
1865
+ syncRenamed: null,
1866
+ syncDeleted: null,
1867
+ readOnly: false,
1868
+ versionsOpen: false,
1869
+ versionNameAsk: false,
1870
+ versionList: null,
1871
+ versionPreview: null,
380
1872
  error: null,
381
1873
  emitWarning: null,
382
1874
  canUndo: false,
383
1875
  canRedo: false,
384
1876
  };
1877
+
1878
+ /**
1879
+ * How long the SUBJECT-LOCAL clock runs, or null when no component is open.
1880
+ *
1881
+ * THE ONE ANSWER FOR THREE FORKS. The component context swaps the whole
1882
+ * playback triple (the total, the seek verb and the starting frame), and that
1883
+ * fork is asked in three places: the header's playback loop reads the total,
1884
+ * the same loop picks `seekDef` over `seek`, and `seekDefTo` clamps against it.
1885
+ * Three copies of "is a component open, and if so how long is it" is three
1886
+ * chances to answer differently, and the symptom of disagreeing is a transport
1887
+ * that wraps at a frame the scrubber cannot reach.
1888
+ *
1889
+ * A CODE COMPONENT HAS NO DURATION of its own — it has a `useStudioClock()` and
1890
+ * whatever it does with it — so it gets the preview's fixed ruler. A definition
1891
+ * has an authored one, or a derived extent.
1892
+ */
1893
+ export function defClockFrames(s: ShotEditorState): number | null {
1894
+ // The preview's loop length (bridge v45) wraps the stage, so the chrome's
1895
+ // transport wraps at the same frame.
1896
+ const loop = s.componentPreview.loopFrames;
1897
+ if (s.codeComponent !== null) return loop ?? CODE_PREVIEW_FRAMES;
1898
+ const def =
1899
+ s.editingDef === null ? undefined : s.doc?.components?.[s.editingDef];
1900
+ return def === undefined ? null : (loop ?? defDurationFrames(def));
1901
+ }
1902
+
1903
+ /**
1904
+ * THE WINDOW THE TRANSPORT LOOPS INSIDE, in whichever clock is driving.
1905
+ *
1906
+ * One function because the two halves of the answer have to agree: a start from
1907
+ * one reading and a length from another is a loop that wraps to a frame outside
1908
+ * itself, and `seek`'s clamp then pins the playhead there and playback looks
1909
+ * frozen. Three subjects, innermost first:
1910
+ *
1911
+ * A COMPONENT CONTEXT owns the clock outright. Its own duration, based at 0,
1912
+ * and `frame` does not move at all while it is open — so neither the entered
1913
+ * shot nor the work area has any say.
1914
+ *
1915
+ * AN ENTERED SHOT is a window of master time, so it is the loop.
1916
+ *
1917
+ * A WORK AREA is the scene context's own window, and only there: the two
1918
+ * above already answer, and a second window over one of them would be two
1919
+ * answers to one question. CLAMPED to the document on every read rather than
1920
+ * repaired on write — a trim, an undo or a deleted shot can shorten the film
1921
+ * under a work area nobody touched, and a stale end would loop past the last
1922
+ * frame the document has.
1923
+ *
1924
+ * It allocates one two-field object per call, and the two selectors below make
1925
+ * that two allocations per store notification — which during playback is twice
1926
+ * a frame. Deliberate, and within the rule this file keeps: what the rule
1927
+ * forbids is the O(scenes) walk `sceneWindows` materialises per call, which is
1928
+ * why the entered-shot arm reaches for `sceneToMaster` (a prefix sum with an
1929
+ * early return) instead. Two fixed-size literals are not that.
1930
+ *
1931
+ * EXPORTED so the window rule can be asked directly, without a fake rAF queue
1932
+ * to reach it — the same reason `playbackTargetFrame` is.
1933
+ */
1934
+ export function playbackWindow(s: ShotEditorState): {
1935
+ start: number;
1936
+ frames: number;
1937
+ } {
1938
+ const def = defClockFrames(s);
1939
+ if (def !== null) return { start: 0, frames: def };
1940
+ const doc = s.doc;
1941
+ if (doc === null) return { start: 0, frames: 0 };
1942
+ const total = docDurationFrames(doc);
1943
+ if (s.enteredScene !== null) {
1944
+ const entered = doc.scenesById[s.enteredScene];
1945
+ // A STALE ENTERED SHOT FALLS BACK TO THE WHOLE FILM AND NOT TO THE WORK
1946
+ // AREA. The shot was deleted out from under the author, which is a state
1947
+ // the chrome repairs on its next context sync; borrowing a bracket they
1948
+ // drew in the scene context for those few frames would be the transport
1949
+ // inventing a window nobody asked for at the one moment it is confused.
1950
+ return entered === undefined
1951
+ ? { start: 0, frames: total }
1952
+ : {
1953
+ start: sceneToMaster(doc, s.enteredScene, 0) ?? 0,
1954
+ frames: Math.max(0, Math.trunc(entered.durationFrames)),
1955
+ };
1956
+ }
1957
+ const area = s.workArea;
1958
+ if (area === null) return { start: 0, frames: total };
1959
+ const end = Math.min(total, Math.trunc(area.end));
1960
+ const start = Math.max(0, Math.trunc(area.start));
1961
+ // A BRACKET THE FILM NO LONGER REACHES IS NO WINDOW AT ALL — fall back to the
1962
+ // whole film rather than squeezing it onto the last frame. A one-frame loop
1963
+ // at the end of a document the author just trimmed reads as a frozen
1964
+ // transport, which is the failure this whole file keeps designing against,
1965
+ // and the honest answer to "your bracket is off the end" is the film. A
1966
+ // bracket that still OVERLAPS keeps its overlap: only the end was clamped.
1967
+ return start < end ? { start, frames: end - start } : { start: 0, frames: total };
1968
+ }
1969
+
1970
+ /** Whether the transport wraps at the end of `playbackWindow`. A definition
1971
+ * and an entered shot always do; the Project context follows the Loop pref. */
1972
+ export function playbackLoops(s: ShotEditorState): boolean {
1973
+ if (defClockFrames(s) !== null) return true;
1974
+ return s.enteredScene !== null || s.loopPlayback;
1975
+ }
1976
+
1977
+ /**
1978
+ * IS THE SOUND THE MASTER SOUNDTRACK, PLAYED STRAIGHT? The one answer behind
1979
+ * `TransportSnapshot.audible`.
1980
+ *
1981
+ * TWO REASONS IT IS FALSE, and they are the same reason twice: a second clock
1982
+ * the picture's clock cannot agree with.
1983
+ *
1984
+ * A COMPONENT CONTEXT drives a definition-local clock and never moves
1985
+ * `frame`, so the document's soundtrack has nothing to anchor to — it would
1986
+ * start from a static master frame and play on over a definition looping on
1987
+ * its own. `defClockFrames` is the same predicate the transport forks on.
1988
+ *
1989
+ * ANY RATE BUT 1 is silent, and that is a refusal rather than a gap. Reverse
1990
+ * needs reversed buffers; 2x and 4x with the pitch kept need a time
1991
+ * stretcher. Both are a second scheduler running on its own arithmetic
1992
+ * beside the picture's, and the moment the two round differently the author
1993
+ * is hearing one frame while watching another. This lane refuses that the
1994
+ * way it refuses every other plausible-but-wrong artifact.
1995
+ *
1996
+ * A pure function of the state, so the bridge that pushes the snapshot has no
1997
+ * second opinion to form — see `audio/index.tsx`.
1998
+ */
1999
+ export function transportAudible(s: ShotEditorState): boolean {
2000
+ return defClockFrames(s) === null && s.playRate === 1;
2001
+ }
2002
+
2003
+ /** The buffer the editor is showing, or null. One reader for `codeActive` so
2004
+ * nothing has to remember that it indexes `codeFiles`. */
2005
+ export function activeCodeFile(s: ShotEditorState): CodeFileState | null {
2006
+ return s.codeActive === null ? null : (s.codeFiles[s.codeActive] ?? null);
2007
+ }
2008
+
2009
+ /** The file the SUBJECT lives in, or null. What lets the URL leave `file=` off
2010
+ * in the ordinary case where the author is looking at the component's own
2011
+ * source: `code=` already says which file that is. */
2012
+ export function codeSubjectRel(s: ShotEditorState): string | null {
2013
+ if (s.codeComponent === null) return null;
2014
+ return s.library.find((r) => r.id === s.codeComponent)?.rel ?? null;
2015
+ }
2016
+
2017
+ /** Is ANY open buffer unsaved? What the leave-confirm asks, and it must ask
2018
+ * across the whole tab set: the buffer on screen being clean says nothing
2019
+ * about the three behind it, and closing the studio drops them all. */
2020
+ export function anyCodeFileDirty(s: Pick<ShotEditorState, "codeFiles">): boolean {
2021
+ return Object.values(s.codeFiles).some((f) => f.content !== f.savedContent);
2022
+ }
2023
+
2024
+ /** Are the code buffers live? Only Code Studio's subject holds them. */
2025
+ export function buffersOpen(s: ShotEditorState): boolean {
2026
+ return s.codeComponent !== null;
2027
+ }