@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
@@ -8,54 +8,92 @@
8
8
  // Both halves are load-bearing and neither is optional:
9
9
  //
10
10
  // Reading BASE would tell the author 0 while the layer sat at 150, because
11
- // what is on screen is `base ⊕ blocks(frame)`. That is the exact defect the
12
- // 2D lane's read-effective rule was written to close, and it is worse here
13
- // because a shot's blocks are the normal case rather than the exception.
11
+ // what is on screen is the tracks resolved at the frame. That is the exact
12
+ // defect the 2D lane's read-effective rule was written to close, and it is
13
+ // worse here because a shot's keyframed fields are the normal case rather
14
+ // than the exception.
14
15
  //
15
16
  // Writing the typed value into the BASE would make the field disagree with
16
17
  // itself: type 100, watch it read back 250. So a write goes through
17
18
  // `setEffectiveField`, which is the same arithmetic a canvas drag uses —
18
19
  // the inspector and a drag are one operation with different input devices.
19
20
  //
21
+ // ── AND THE THIRD HALF: A DRAG ─────────────────────────────────────────
22
+ //
23
+ // A widget that is DRAGGED (the colour picker's three bars, a slider) never
24
+ // calls `commit` while the pointer is down. It calls `scrub.move` per pointer
25
+ // event and lets the release commit. So a section that wires only the commit
26
+ // half has a control that moves its thumb and writes nothing. `useScrub` and
27
+ // `useSelectionScrub` below are that half, over the store's scrub trio.
28
+ //
20
29
  // ── WHAT IS GONE FROM THE 2D LANE'S VERSION ────────────────────────────
21
30
  //
22
- // `useNodeTrackField` and everything around it. There are no keyframe tracks in
23
- // this lane, so there are no diamonds, no per-property sequenced/static fork,
24
- // and — the real simplification — no `selectionRefusal`: the 2D lane's one row
25
- // was "this prop is sequenced, select it alone to key it", which cannot arise
26
- // here. `planSelection` collapses to dedupe plus document order.
27
- import { useCallback, useEffect, useState } from "react";
28
- import { animateFieldEdit, effectiveTransform, setEffectiveField } from "@dailies-studio/shot/doc";
29
- import type { Doc3D, Layer3D, TransformField, Transform3D } from "@dailies-studio/shot/doc";
31
+ // `useNodeTrackField` and everything around it. This lane HAS keyframe tracks
32
+ // (schema v12) and does have diamonds — `KeyframeDot` — but not a per-property
33
+ // sequenced/static fork and not a `selectionRefusal`. Both were consequences of
34
+ // the 2D lane's per-field write path; here every section keeps building a plain
35
+ // `layer -> layer` and `routeLayerEdit` at `writeSelection` below turns the diff
36
+ // into a keyframe on any path the layer animates. So `planSelection` collapses
37
+ // to dedupe plus document order, and no section knows tracks exist.
38
+ import { useCallback, useMemo, useRef, useSyncExternalStore } from "react";
39
+ import type { ShotEditorStore } from "../../state/store.js";
40
+ import {
41
+ containerFrame,
42
+ effectiveTransform,
43
+ homeOf,
44
+ keyframeMinter,
45
+ lockRefusal,
46
+ lockedLayerEdited,
47
+ resolveSceneTracksAt,
48
+ resolveTracksAt,
49
+ routeSceneEdit,
50
+ sceneAsShown,
51
+ } from "@dailies-studio/shot/doc";
52
+ import type { Doc3D, Layer3D, Scene3D, TransformField, Transform3D } from "@dailies-studio/shot/doc";
53
+ import type { ScrubHandlers } from "@dailies-studio/ui";
30
54
  import { useShotEditor, useStore } from "../../state/context.js";
55
+ import { authoringFrame } from "../../state/viewMode.js";
56
+ import { commitTransformField, writeSelection } from "../../state/fieldWrite.js";
31
57
 
32
- /** A colour swatch takes no drag, so it has no scrub — but `ColorPicker`'s
33
- * contract requires the trio, because the same widget IS scrubbed when it sits
34
- * on a keyframable property elsewhere. Three no-ops, in one place. */
35
- export const NO_SCRUB = { start: () => {}, move: () => {}, end: () => {} };
58
+ // A `NO_SCRUB` triple of no-ops used to live here, passed by every section that
59
+ // believed "a colour swatch takes no drag". The claim was false: `ColorPicker`
60
+ // routes its SV square, its hue bar and its alpha bar ENTIRELY through
61
+ // `scrub.move`, and `PropControl`'s sliders take the same path, so every
62
+ // section that passed it had a colour reachable only through the hex and R/G/B
63
+ // boxes. `useScrub` and `useSelectionScrub` below are what replaced it, and it
64
+ // is deleted rather than deprecated so a new section cannot reintroduce the bug
65
+ // by reaching for the convenient-looking constant.
36
66
 
37
67
  /** THE one document-edit seam. Every inspector commit is a transaction, so it
38
- * is one undo entry however many layers it touched. */
68
+ * is one undo entry however many layers it touched. A write that edits a
69
+ * locked layer lands nothing and names that layer, the same rule
70
+ * `writeSelection` applies per layer. */
39
71
  export function useDocWrite(): (write: (doc: Doc3D) => Doc3D) => void {
40
72
  const store = useStore();
41
- return useCallback((write) => store.edit(write), [store]);
73
+ return useCallback(
74
+ (write) => {
75
+ let refusal: string | null = null;
76
+ const ran = store.edit((base) => {
77
+ const next = write(base);
78
+ const locked = lockedLayerEdited(base, next);
79
+ if (locked === null) return next;
80
+ refusal = lockRefusal(locked);
81
+ return base;
82
+ });
83
+ if (ran && refusal !== null) store.reportError(refusal);
84
+ },
85
+ [store],
86
+ );
42
87
  }
43
88
 
44
- /** MASTER frame -> the scene-local frame the effective read wants. Derived
45
- * rather than stored: the scene's start is a prefix sum, so a trim upstream
46
- * moves it and a cached offset would go stale silently. Pure and EXPORTED,
47
- * because it has callers with different frames: the display hook below
48
- * (coalesced) and every commit path (live at the click or keystroke —
49
- * useTransformWrite here, ShotSection's "Set from current framing"). */
50
- export function sceneLocalFrame(doc: Doc3D, sceneId: string, frame: number): number {
51
- let start = 0;
52
- for (const id of doc.scenes) {
53
- const duration = Math.max(1, Math.trunc(doc.scenesById[id]?.durationFrames ?? 0));
54
- if (id === sceneId) break;
55
- start += duration;
56
- }
57
- return Math.max(0, frame - start);
58
- }
89
+ // `sceneLocalFrame` and a private `containerFrame` used to live here, and the
90
+ // store carried a third open-coding of the same arithmetic. Three prefix sums
91
+ // over one mapping, one of which clamped a scene duration at 1 where the other
92
+ // two clamped at 0 — they agreed only because `validate` refuses a duration
93
+ // below 1, which is a gate holding up an invariant nobody asked it to hold.
94
+ // There is one now: `containerFrame` in @dailies-studio/shot, imported above.
95
+ // Every site here calls it, including the ones that used to open-code its
96
+ // definition-vs-shot fork inline.
59
97
 
60
98
  /** How stale the inspector's DISPLAYED frame may run while the playhead is
61
99
  * moving fast. ~10Hz keeps a scrub's numbers live enough to read against the
@@ -83,69 +121,233 @@ const INSPECTOR_FRAME_MS = 100;
83
121
  * that subscribed the raw frame would run its resolver fold per tick. */
84
122
  export function useCoalescedFrame(): number {
85
123
  const store = useStore();
86
- const [frame, setFrame] = useState(() => store.getState().frame);
87
- useEffect(() => {
88
- let timer: ReturnType<typeof setTimeout> | null = null;
89
- let lastPush = 0;
90
- const push = (f: number): void => {
91
- lastPush = performance.now();
92
- setFrame(f);
93
- };
94
- const onChange = (): void => {
95
- const s = store.getState();
96
- // Held during playback. The pause itself notifies (playing flips), and
97
- // that notification falls through to the flush below.
98
- if (s.playing) return;
99
- if (timer !== null) {
100
- clearTimeout(timer);
101
- timer = null;
102
- }
103
- const since = performance.now() - lastPush;
104
- if (since >= INSPECTOR_FRAME_MS) {
105
- push(s.frame);
106
- return;
124
+ const source = coalescedFrameSource(store, "frame");
125
+ return useSyncExternalStore(source.subscribe, source.get, source.get);
126
+ }
127
+
128
+ /** The definition clock (`defFrame`), coalesced by the same policy. A
129
+ * readout inside a definition that subscribed the hot channel re-rendered
130
+ * per tick of the def-local clock while the transport played. */
131
+ export function useCoalescedDefFrame(): number {
132
+ const store = useStore();
133
+ const source = coalescedFrameSource(store, "defFrame");
134
+ return useSyncExternalStore(source.subscribe, source.get, source.get);
135
+ }
136
+
137
+ /**
138
+ * ONE coalescer per store, shared by every `useCoalescedFrame` caller.
139
+ *
140
+ * Each caller used to own a raw store subscription, a `useState` and a
141
+ * trailing timer. `useSelectionValue` instantiates two per field, so the
142
+ * Typography section alone held eighteen subscriptions and eighteen timers,
143
+ * every one of which woke on every store notification (sixty a second while
144
+ * playing) to read the state and return. This is the same policy, held once:
145
+ * the frame is HELD while the transport plays (the pause itself notifies and
146
+ * falls through to the flush), trails at ~10 Hz during a scrub, and leads on
147
+ * a lone seek. Subscribers see one notification per coalesced change through
148
+ * `useSyncExternalStore`.
149
+ *
150
+ * The store subscription exists only while someone listens. A fresh listener
151
+ * syncs the frame from the store WITHOUT touching `lastPush`: mounting must
152
+ * not consume the leading edge, or the first seek after mount would coalesce
153
+ * instead of landing immediately. With nobody listening, `get` reads the
154
+ * store live rather than a value that went stale in between.
155
+ */
156
+ interface CoalescedFrameSource {
157
+ get(): number;
158
+ subscribe(listener: () => void): () => void;
159
+ }
160
+ type CoalescedClock = "frame" | "defFrame";
161
+ const COALESCED_FRAME_SOURCES = new WeakMap<ShotEditorStore, Partial<Record<CoalescedClock, CoalescedFrameSource>>>();
162
+ function coalescedFrameSource(store: ShotEditorStore, key: CoalescedClock): CoalescedFrameSource {
163
+ let bucket = COALESCED_FRAME_SOURCES.get(store);
164
+ if (bucket === undefined) {
165
+ bucket = {};
166
+ COALESCED_FRAME_SOURCES.set(store, bucket);
167
+ }
168
+ const existing = bucket[key];
169
+ if (existing !== undefined) return existing;
170
+ const listeners = new Set<() => void>();
171
+ let frame = store.getState()[key];
172
+ let lastPush = 0;
173
+ let timer: ReturnType<typeof setTimeout> | null = null;
174
+ let unsubscribe: (() => void) | null = null;
175
+ // Only a frame CHANGE spends the leading edge. Every store notification lands
176
+ // here, so a late non-frame one (an asset-usage fetch after open) would
177
+ // otherwise make the next lone seek trail.
178
+ const push = (next: number): void => {
179
+ if (next === frame) return;
180
+ lastPush = performance.now();
181
+ frame = next;
182
+ for (const listener of [...listeners]) listener();
183
+ };
184
+ const onChange = (): void => {
185
+ const s = store.getState();
186
+ if (s.playing) return;
187
+ if (timer !== null) {
188
+ clearTimeout(timer);
189
+ timer = null;
190
+ }
191
+ const since = performance.now() - lastPush;
192
+ if (since >= INSPECTOR_FRAME_MS) {
193
+ push(s[key]);
194
+ return;
195
+ }
196
+ // The trailing flush re-reads the store when it fires, so the value that
197
+ // lands is the newest one, not the one that armed the timer.
198
+ timer = setTimeout(() => {
199
+ timer = null;
200
+ push(store.getState()[key]);
201
+ }, INSPECTOR_FRAME_MS - since);
202
+ };
203
+ const source: CoalescedFrameSource = {
204
+ get: () => (listeners.size === 0 ? store.getState()[key] : frame),
205
+ subscribe(listener) {
206
+ if (listeners.size === 0) {
207
+ frame = store.getState()[key];
208
+ unsubscribe = store.subscribe(onChange);
107
209
  }
108
- // The trailing flush: re-reads the store when it fires, so the value
109
- // that lands is the newest one, not the one that armed the timer.
110
- timer = setTimeout(() => {
111
- timer = null;
112
- push(store.getState().frame);
113
- }, INSPECTOR_FRAME_MS - since);
114
- };
115
- // The frame may have moved between render and subscription. A SYNC-UP,
116
- // not a rate event: it must not touch `lastPush`, or mounting would
117
- // consume the leading edge and the very first seek after mount would
118
- // coalesce instead of landing immediately.
119
- setFrame(store.getState().frame);
120
- const unsubscribe = store.subscribe(onChange);
121
- return () => {
122
- unsubscribe();
123
- if (timer !== null) clearTimeout(timer);
124
- };
125
- }, [store]);
126
- return frame;
210
+ listeners.add(listener);
211
+ return () => {
212
+ listeners.delete(listener);
213
+ if (listeners.size !== 0) return;
214
+ unsubscribe?.();
215
+ unsubscribe = null;
216
+ if (timer !== null) {
217
+ clearTimeout(timer);
218
+ timer = null;
219
+ }
220
+ };
221
+ },
222
+ };
223
+ bucket[key] = source;
224
+ return source;
127
225
  }
128
226
 
129
227
  /** The scene the inspector is looking at, and the SCENE-LOCAL frame — the two
130
- * things every effective read needs. The frame is the COALESCED one. */
228
+ * things every effective read needs. The frame is the COALESCED one.
229
+ *
230
+ * With a DEFINITION open (unify P2) the container is the definition and the
231
+ * frame is the DEF-LOCAL clock: interior keyframes live on def-local frames, so
232
+ * an effective read at a master-derived frame would show values from a clock
233
+ * the stage is not rendering. */
131
234
  export function useInspectorFrame(): { sceneId: string | null; sceneFrame: number } {
132
235
  const sceneId = useShotEditor((s) => s.sceneId);
236
+ const editingDef = useShotEditor((s) => s.editingDef);
237
+ const defFrame = useCoalescedDefFrame();
133
238
  const frame = useCoalescedFrame();
134
239
  const doc = useShotEditor((s) => s.doc);
135
- if (doc === null || sceneId === null) return { sceneId: null, sceneFrame: 0 };
136
- return { sceneId, sceneFrame: sceneLocalFrame(doc, sceneId, frame) };
240
+ if (doc === null) return { sceneId: null, sceneFrame: 0 };
241
+ if (editingDef !== null) return { sceneId: editingDef, sceneFrame: defFrame };
242
+ if (sceneId === null) return { sceneId: null, sceneFrame: 0 };
243
+ return { sceneId, sceneFrame: containerFrame(doc, sceneId, frame) };
137
244
  }
138
245
 
139
- /** Every selected layer, in document order. The ONE place a panel gets its
140
- * subjects, so no section re-derives the list with a different answer. */
246
+ /** Every selected layer, in document order, AS PAINTED at the playhead. The ONE
247
+ * place a panel gets its subjects, so no section re-derives the list with a
248
+ * different answer. Resolves through the CONTAINER (`editingDef ?? sceneId`,
249
+ * the store's own `layerContainer` rule), so the right rail works inside a
250
+ * definition.
251
+ *
252
+ * ── `resolveTracksAt` IS THE READOUT HALF OF THE ASYMMETRY ──────────────
253
+ *
254
+ * The header above states that readouts come from the layer as painted, and
255
+ * until this call only the TRANSFORM half was implemented (`effectiveTransform`
256
+ * -> `trackedBaseTransform`, which substitutes five channels and nothing else).
257
+ * Every other tracked path was read straight off the authored constant — and
258
+ * that constant is DEAD once a track exists, because `routeTrackedEdit` mints
259
+ * the keyframe and writes the old base BACK (trackOps.ts). Base pinned plus a
260
+ * readout that reads the base is a control that can never move again: the
261
+ * slider does not follow the pointer, an arrow nudge is always base+step, and
262
+ * typing the number the field is showing is diffed against itself and writes
263
+ * nothing at all. That is one bug over ~70 trackable paths, from
264
+ * `instance.props.<key>` through `paint.fill.color`, `mesh.color`,
265
+ * `text.style.sizePx` and the crop boxes.
266
+ *
267
+ * The two halves compose rather than collide: `resolveTracksAt` SKIPS transform
268
+ * paths (tracks.ts), leaving them to `effectiveTransform` below, and it returns
269
+ * its input by reference for an untracked layer, so the common case allocates
270
+ * nothing. `tracks` stays on the result, which is what keeps `KeyframeDot`'s
271
+ * diamond states reading true off a resolved layer.
272
+ *
273
+ * DISPLAY ONLY. Writes never come through here: `writeSelection` re-reads its
274
+ * subjects from the document it is handed, and a scrub capture re-derives from
275
+ * the gesture's base, so a sampled value cannot be baked into a constant.
276
+ */
141
277
  export function useSelectedLayers(): Layer3D[] {
142
278
  const doc = useShotEditor((s) => s.doc);
143
279
  const sceneId = useShotEditor((s) => s.sceneId);
280
+ const editingDef = useShotEditor((s) => s.editingDef);
144
281
  const selection = useShotEditor((s) => s.selection);
145
- if (doc === null || sceneId === null) return [];
282
+ const { sceneFrame } = useInspectorFrame();
283
+ // Memoised on its five inputs: the inspector repaints whole on every
284
+ // document identity change, i.e. once per rAF frame of any drag anywhere,
285
+ // and this ran its map/filter/resolve chain once per `useSelectionValue`
286
+ // site in the open sections (nine in Typography alone) per repaint.
287
+ return useMemo(() => {
288
+ if (doc === null) return EMPTY_LAYERS;
289
+ const container = editingDef ?? sceneId;
290
+ if (container === null) return EMPTY_LAYERS;
291
+ const home = homeOf(doc, container);
292
+ if (home === null) return EMPTY_LAYERS;
293
+ return selection
294
+ .map((id) => home.layersById[id])
295
+ .filter((l): l is Layer3D => l !== undefined)
296
+ .map((l) => resolveTracksAt(l, sceneFrame, doc.fps));
297
+ }, [doc, sceneId, editingDef, selection, sceneFrame]);
298
+ }
299
+ const EMPTY_LAYERS: Layer3D[] = [];
300
+
301
+ /**
302
+ * `effectiveTransform`, replayed per (layer, frame, fps).
303
+ *
304
+ * Every `useSelectionValue` site folds it for every selected layer on every
305
+ * inspector repaint; the layer objects are immutable on edit and the frame is
306
+ * the shared coalesced one, so the same fold was answered up to nine times per
307
+ * section per repaint. One entry per layer object, replaced when the frame or
308
+ * fps moves.
309
+ */
310
+ const EFFECTIVE_TRANSFORMS = new WeakMap<
311
+ Layer3D,
312
+ { sceneFrame: number; fpsNum: number; fpsDen: number; transform: Transform3D }
313
+ >();
314
+ function effectiveTransformCached(layer: Layer3D, sceneFrame: number, fps: Doc3D["fps"]): Transform3D {
315
+ const cached = EFFECTIVE_TRANSFORMS.get(layer);
316
+ if (
317
+ cached !== undefined &&
318
+ cached.sceneFrame === sceneFrame &&
319
+ cached.fpsNum === fps.num &&
320
+ cached.fpsDen === fps.den
321
+ ) {
322
+ return cached.transform;
323
+ }
324
+ const transform = effectiveTransform(layer, sceneFrame, fps);
325
+ EFFECTIVE_TRANSFORMS.set(layer, { sceneFrame, fpsNum: fps.num, fpsDen: fps.den, transform });
326
+ return transform;
327
+ }
328
+
329
+ /**
330
+ * The shot the scene sections describe, AS PAINTED at the playhead.
331
+ *
332
+ * `useSelectedLayers`'s twin one level up, for the same reason and against the
333
+ * same failure: `routeSceneEdit` runs the SAME `routeTrackedEdit`, so a scene
334
+ * constant is pinned the moment a track exists on it, and the two sections that
335
+ * read `doc.scenesById[sceneId]` directly (the background well and the 27
336
+ * keyframable post-effect params) would show it forever.
337
+ *
338
+ * The frame is `containerFrame`'s and NOT `useInspectorFrame`'s: that hook
339
+ * substitutes the DEFINITION and its own clock while one is open, and a shot is
340
+ * not a definition — asking it for a scene would hand back `undefined` and blank
341
+ * the section. Display only, exactly as above.
342
+ */
343
+ export function useInspectorScene(): Scene3D | null {
344
+ const doc = useShotEditor((s) => s.doc);
345
+ const sceneId = useShotEditor((s) => s.sceneId);
346
+ const frame = useCoalescedFrame();
347
+ if (doc === null || sceneId === null) return null;
146
348
  const scene = doc.scenesById[sceneId];
147
- if (scene === undefined) return [];
148
- return selection.map((id) => scene.layersById[id]).filter((l): l is Layer3D => l !== undefined);
349
+ if (scene === undefined) return null;
350
+ return resolveSceneTracksAt(scene, containerFrame(doc, sceneId, frame), doc.fps);
149
351
  }
150
352
 
151
353
  export interface SelectionValue<T> {
@@ -160,17 +362,54 @@ export interface SelectionValue<T> {
160
362
  * Read one property across the selection, from the layer AS PAINTED.
161
363
  *
162
364
  * `read` is handed the EFFECTIVE transform for transform properties and the
163
- * layer itself for paint ones, because paint has no block deltas at all in v1 —
164
- * so paint reads and writes are both base and cannot disagree.
365
+ * layer itself for paint ones.
366
+ *
367
+ * ── `read` MUST RETURN A SCALAR ─────────────────────────────────────────
368
+ *
369
+ * A number, a string, a boolean, null or undefined. Members are compared with
370
+ * `Object.is`, so an OBJECT read is mixed BY IDENTITY: `l => l.paint.shadows[0]`
371
+ * reports `mixed` for every multi-selection, including one where all the shadows
372
+ * are identical, and the row shows "Mixed" over values the author can see agree.
373
+ * Read the scalar the row displays — `l => l.paint.shadows?.[0]?.color`, one
374
+ * hook per field.
375
+ *
376
+ * A deep equal here is NOT the fix, for the reason the 2D lane's twin states:
377
+ * it would need a key-order-independent equality, which is a second and quieter
378
+ * definition of "the same value" living in a module with no business owning one.
379
+ *
380
+ * The rule cannot be stated in the type system over an arbitrary `T`, so a
381
+ * violation is REPORTED instead: a permanently wrong "Mixed" that says nothing
382
+ * is exactly the silent readout this lane keeps removing.
165
383
  */
166
384
  export function useSelectionValue<T>(read: (layer: Layer3D, effective: Transform3D) => T): SelectionValue<T> {
167
385
  const layers = useSelectedLayers();
168
386
  const { sceneFrame } = useInspectorFrame();
169
387
  const doc = useShotEditor((s) => s.doc);
170
388
  if (layers.length === 0 || doc === null) return { value: undefined, mixed: false };
171
- const values = layers.map((l) => read(l, effectiveTransform(l, sceneFrame, doc.fps)));
389
+ const values = layers.map((l) => read(l, effectiveTransformCached(l, sceneFrame, doc.fps)));
172
390
  const first = values[0]!;
173
- return { value: first, mixed: values.some((v) => !Object.is(v, first)) };
391
+ const mixed = values.some((v) => !Object.is(v, first));
392
+ // Only in the mixed branch: a scalar read costs one `typeof` on a value that
393
+ // already disagreed, and an agreeing selection costs nothing at all.
394
+ if (mixed) reportObjectRead(first);
395
+ return { value: first, mixed };
396
+ }
397
+
398
+ /** Shapes already reported, so a wrong read is loud ONCE rather than sixty
399
+ * times a second. This runs inside render, and the inspector re-renders on
400
+ * every coalesced frame while the playhead moves. Keyed on the value's own
401
+ * keys, so two different broken reads each get their own line. */
402
+ const OBJECT_READS = new Set<string>();
403
+
404
+ function reportObjectRead(value: unknown): void {
405
+ if (value === null || typeof value !== "object") return;
406
+ const shape = Object.keys(value).join(",");
407
+ if (OBJECT_READS.has(shape)) return;
408
+ OBJECT_READS.add(shape);
409
+ console.error(
410
+ `[shot inspector] useSelectionValue read an OBJECT ({${shape}}), so "Mixed" is decided by ` +
411
+ `identity and is true for every multi-selection. Read the scalar the row displays instead.`,
412
+ );
174
413
  }
175
414
 
176
415
  /**
@@ -180,44 +419,129 @@ export function useSelectionValue<T>(read: (layer: Layer3D, effective: Transform
180
419
  * that way — so a fan-out cannot depend on the order the author happened to
181
420
  * click in, and an id cannot be visited twice.
182
421
  */
183
- export function useSelectionWrite(): (
184
- build: (layer: Layer3D) => Layer3D,
185
- opts?: { includeLocked?: boolean },
186
- ) => void {
422
+ export function useSelectionWrite(): (build: (layer: Layer3D) => Layer3D) => void {
187
423
  const store = useStore();
188
424
  return useCallback(
189
- (build, opts) => {
425
+ (build) => {
190
426
  // LIVE at the commit, like the frame in useTransformWrite: a
191
427
  // render-captured sceneId paired with a live frame would write into
192
- // last render's scene when the commit lands right on a crossing.
193
- const sceneId = store.getState().sceneId;
194
- if (sceneId === null) return;
195
- const ids = store.getState().selection;
196
- if (ids.length === 0) return;
197
- store.edit((doc) => {
198
- const scene = doc.scenesById[sceneId];
199
- if (scene === undefined) return doc;
200
- const layersById = { ...scene.layersById };
201
- let changed = false;
202
- for (const id of ids) {
203
- const layer = layersById[id];
204
- // A locked layer is skipped — that is what locked MEANS. The one
205
- // exception is the lock toggle itself, or a lock would be one-way.
206
- if (layer === undefined || (layer.locked === true && opts?.includeLocked !== true)) continue;
207
- const next = build(layer);
208
- if (next === layer) continue;
209
- layersById[id] = next;
210
- changed = true;
211
- }
212
- return changed
213
- ? { ...doc, scenesById: { ...doc.scenesById, [sceneId]: { ...scene, layersById } } }
214
- : doc;
428
+ // last render's scene when the commit lands right on a crossing. The
429
+ // CONTAINER rule: a definition while one is open.
430
+ const s = store.getState();
431
+ const container = s.editingDef ?? s.sceneId;
432
+ if (container === null || s.selection.length === 0 || s.doc === null) return;
433
+ const at = containerFrame(s.doc, container, authoringFrame(s));
434
+ let refusal: string | null = null;
435
+ const ran = store.edit((doc) => {
436
+ const out = writeSelection(doc, container, s.selection, build, at);
437
+ refusal = out.refusal;
438
+ return out.doc;
215
439
  });
440
+ if (ran && refusal !== null) store.reportError(refusal);
216
441
  },
217
442
  [store],
218
443
  );
219
444
  }
220
445
 
446
+ /**
447
+ * `writeSelection`'s twin with the SHOT as the subject: build the next scene,
448
+ * and let `routeSceneEdit` turn the diff into a keyframe on any path the shot
449
+ * animates (P5).
450
+ *
451
+ * Exported because the two scene sections each own a pure patch function
452
+ * (`patchScene`, `patchEffect`) that both their typed and their drag path
453
+ * already run, and this wraps exactly those — so the background well and fifteen
454
+ * effect fields become keyframe-aware without either section learning what a
455
+ * track is. Same rule as the layer half, one level up.
456
+ *
457
+ * The FRAME is read live by the caller, never captured at render, for the reason
458
+ * `useSelectionScrub` reads it live: a drag held across a shot boundary would
459
+ * otherwise write keys at a frame the playhead has left.
460
+ *
461
+ * IDENTITY WHEN NOTHING CHANGED, so `edit` records no history entry and a drag
462
+ * that ends where it started commits nothing.
463
+ */
464
+ export function writeScene(
465
+ doc: Doc3D,
466
+ sceneId: string,
467
+ build: (scene: Scene3D) => Scene3D,
468
+ sceneFrame: number,
469
+ ): Doc3D {
470
+ const scene = doc.scenesById[sceneId];
471
+ if (scene === undefined) return doc;
472
+ // Built from the shot as shown, for `writeSelection`'s reason.
473
+ const shown = sceneAsShown(scene, sceneFrame, doc.fps);
474
+ const next = routeSceneEdit(scene, build(shown), sceneFrame, keyframeMinter(doc), shown);
475
+ if (next === scene) return doc;
476
+ return { ...doc, scenesById: { ...doc.scenesById, [sceneId]: next } };
477
+ }
478
+
479
+ /**
480
+ * A PANEL DRAG as ONE undo entry: the store's scrub trio wearing the shape the
481
+ * shared widgets ask for (`ScrubHandlers`, ui/TextField.ts).
482
+ *
483
+ * `write` is PURE, `(doc, raw) => doc`, and it MUST RE-READ its subject from
484
+ * the doc it is HANDED. Every capture re-derives from the gesture's BASE
485
+ * (store.ts's `scrubCapture`), so a move passes the ABSOLUTE value the author is
486
+ * pointing at and never a delta from the previous move. A builder that instead
487
+ * closed over a value read at RENDER time re-applies the whole drag on top of
488
+ * itself sixty times a second, and the only symptom is a control that runs away
489
+ * from the cursor.
490
+ *
491
+ * NO ERROR IS RETURNED, unlike the 2D lane's `DocWrite`. This chrome has one
492
+ * error strip and the store owns it: `beginScrub` puts its own refusal there
493
+ * when another lane holds the history (a stage drag, a wheel dolly under
494
+ * follow), and the two later verbs no-op on a refused scrub, so a widget may
495
+ * drive all three blind, which is exactly what `ColorPicker` does.
496
+ */
497
+ export function useScrub(write: (doc: Doc3D, raw: unknown) => Doc3D): ScrubHandlers {
498
+ const store = useStore();
499
+ // Deliberately not memoized. Every call site passes an inline builder, so a
500
+ // dependency on it would rebuild this object every render anyway, and the
501
+ // widgets read these handlers off their props at POINTER time, which is what
502
+ // keeps the newest builder the one a move runs.
503
+ return {
504
+ start: () => store.beginScrub(),
505
+ move: (raw) => store.scrubCapture((doc) => write(doc, raw)),
506
+ end: (commit) => store.endScrub(commit),
507
+ };
508
+ }
509
+
510
+ /**
511
+ * The same bracket, fanned out over the selection: `useSelectionWrite`'s builder
512
+ * with the widget's raw value beside it, through the same `writeSelection`.
513
+ *
514
+ * The subjects are read LIVE per capture rather than at render, for the reason
515
+ * the commit path reads them live: a fan-out over the selection a panel last
516
+ * RENDERED against would write into layers the author has since left.
517
+ */
518
+ export function useSelectionScrub(build: (layer: Layer3D, raw: unknown) => Layer3D): ScrubHandlers {
519
+ const store = useStore();
520
+ // The last capture's lock refusal, named once at the release rather than per
521
+ // pointer event.
522
+ const refusal = useRef<string | null>(null);
523
+ const scrub = useScrub((doc, raw) => {
524
+ const s = store.getState();
525
+ const container = s.editingDef ?? s.sceneId;
526
+ if (container === null) return doc;
527
+ // The frame LIVE at the capture, and against the document being written
528
+ // rather than the store's — a scrub re-derives from its base, so the shot
529
+ // starts here are the base's.
530
+ const at = containerFrame(doc, container, authoringFrame(s));
531
+ const out = writeSelection(doc, container, s.selection, (layer) => build(layer, raw), at);
532
+ refusal.current = out.refusal;
533
+ return out.doc;
534
+ });
535
+ return {
536
+ ...scrub,
537
+ end: (commit) => {
538
+ scrub.end(commit);
539
+ if (commit && refusal.current !== null) store.reportError(refusal.current);
540
+ refusal.current = null;
541
+ },
542
+ };
543
+ }
544
+
221
545
  /**
222
546
  * Type a transform field across the selection.
223
547
  *
@@ -227,42 +551,10 @@ export function useSelectionWrite(): (
227
551
  */
228
552
  export function useTransformWrite(): (field: TransformField, value: number) => void {
229
553
  const store = useStore();
230
- const write = useSelectionWrite();
231
554
  return useCallback(
232
555
  (field, value) => {
233
- // LIVE state at the keystroke, never the display hook's coalesced frame:
234
- // the compensation arithmetic must target the frame actually painted, or
235
- // the typed value lands up to 100ms of playhead away from where it reads
236
- // back.
237
- const s = store.getState();
238
- const doc = s.doc;
239
- if (doc === null || s.sceneId === null) return;
240
- const sceneId = s.sceneId;
241
- const sceneFrame = sceneLocalFrame(doc, sceneId, s.frame);
242
- let refusal: string | null = null;
243
- // ANIMATE ON: the typed value becomes a During block ending at the
244
- // playhead — the same fork a stage drag takes (store.ts) and the camera
245
- // takes (writeFraming), because the inspector and a drag are one
246
- // operation with different input devices. OFF: the base write below.
247
- if (s.animateMode) {
248
- store.edit((base) => {
249
- const out = animateFieldEdit(base, sceneId, s.selection, field, value, sceneFrame, base.fps);
250
- refusal = out.refusals[0] ?? null;
251
- return out.doc;
252
- });
253
- store.reportError(refusal);
254
- return;
255
- }
256
- write((layer) => {
257
- const out = setEffectiveField(layer, field, value, sceneFrame, doc.fps);
258
- if (out.refusal !== null) {
259
- refusal ??= out.refusal;
260
- return layer;
261
- }
262
- return out.transform === layer.transform ? layer : { ...layer, transform: out.transform };
263
- });
264
- store.reportError(refusal);
556
+ store.reportError(commitTransformField(store, field, value, store.getState().selection));
265
557
  },
266
- [store, write],
558
+ [store],
267
559
  );
268
560
  }