@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
@@ -0,0 +1,866 @@
1
+ // WHEN A LAYER IS ON SCREEN, AND — FOR A RECORDING — WHICH PART OF IT PLAYS.
2
+ //
3
+ // ── WHY THIS SECTION HAD TO EXIST ───────────────────────────────────────
4
+ //
5
+ // A lifetime was reachable only by dragging a bar in the dock. That is the
6
+ // right gesture and it is the only one, so "start this at frame 90" was a drag
7
+ // against a ruler, unrepeatable across two layers and impossible at a zoom
8
+ // where a frame is a third of a pixel. The clip fields were worse: `sourceIn`,
9
+ // `sourceOut` and the natural length of a recording had no field ANYWHERE in
10
+ // the chrome, and the three things that can end a clip were indistinguishable
11
+ // by looking at it.
12
+ //
13
+ // ── ITS ASYMMETRY IS `useDocWrite`'s, AND BOTH HALVES ARE LIVE HERE ─────
14
+ //
15
+ // Read that file's header first. Readouts come from the layer AS PAINTED at the
16
+ // playhead (`useSelectedLayers`, which runs `resolveTracksAt`), and writes are
17
+ // compensated so the rendered value lands on what was typed. This section is
18
+ // where the two halves are visibly different work rather than the same read
19
+ // twice, because it has one field of each kind:
20
+ //
21
+ // THE GAIN IS TRACKED. `video.sound.gain` is a keyframable path (tracks.ts),
22
+ // so its base is DEAD the moment a track exists — `routeTrackedEdit` writes
23
+ // the old base back. A row reading the base would show a number the mixer
24
+ // stopped using, and typing the number the field displayed would diff
25
+ // against itself and write nothing at all. So it reads through the resolved
26
+ // layer and writes through `useSelectionWrite`, which turns the diff into a
27
+ // keyframe at the playhead.
28
+ // EVERY TIME FIELD IS NOT. A lifetime has no track, so there is
29
+ // nothing to compensate — but the value still must not be read at RENDER
30
+ // and written back later. Each write below re-reads its subject from the
31
+ // document `useDocWrite` hands it and computes the delta THERE, so a commit
32
+ // that lands after an undo, a scene crossing or another panel's edit is
33
+ // measured against the document it is actually writing into.
34
+ //
35
+ // ── AND EVERY TIME EDIT GOES THROUGH AN OP THAT ALREADY EXISTS ──────────
36
+ //
37
+ // `writeLifespan` (panels/timeline/model.ts) is what the dock's bars drag
38
+ // through. It forks a VIDEO layer in a scene onto `trimLayer`/`moveLayers`,
39
+ // which bound an edge by the FOOTAGE as well as by the scene, and leaves a
40
+ // plain layer on the clamped lifetime write. Typing a number and dragging a bar
41
+ // are one operation with different input devices — the rule `useDocWrite`'s
42
+ // header states for the canvas — so this section spends a subtraction to reach
43
+ // the delta API rather than owning a second set of clamps that would disagree
44
+ // with the bars on the first edge case.
45
+ //
46
+ // ── ONE LAYER, DECLARED ─────────────────────────────────────────────────
47
+ //
48
+ // A lifetime is a property of one layer and every clip op takes one `layerId`.
49
+ // A fan-out would need `moveLayers`' all-or-nothing posture for the edges and
50
+ // has no meaning at all for "source in", so this section renders for a single
51
+ // selection and withholds itself otherwise — the rule `component`, `instance`,
52
+ // `triggers` and `material` already follow (layout.ts).
53
+ //
54
+ // CEILING, stated: a NEGATIVE source in is readable here and not typeable. It
55
+ // means the window opens before the recording does, which is what `splitScene`
56
+ // leaves behind for a clip already playing at a cut; `setVideoSource` clamps a
57
+ // requested source frame to the recording, so the row shows the lead and
58
+ // refuses to invent one. Slipping the clip in the dock is what moves it.
59
+ //
60
+ // ── EVERY FIELD DRAGS LIVE ──────────────────────────────────────────────
61
+ //
62
+ // Each number's drag runs the SAME op its typed value does, as a scrub: one
63
+ // undo entry, every capture re-derived from the gesture's base (so an edge's
64
+ // delta is measured against the document the drag started from), and the
65
+ // store's lock rule applied to each capture. A capture an op refuses (a retime
66
+ // the sound forbids, a fade longer than the sound) holds the last value the
67
+ // same gesture landed, and the refusal is said once, at the release.
68
+ import { useRef, useState } from "react";
69
+ import {
70
+ InspectorRow,
71
+ InspectorSection,
72
+ NumberField,
73
+ PropertyGrid,
74
+ SegmentedControl,
75
+ Toggle,
76
+ useLocked,
77
+ wordOptions,
78
+ } from "@dailies-studio/ui";
79
+ import type { ScrubHandlers } from "@dailies-studio/ui";
80
+ import {
81
+ MAX_AUDIO_GAIN,
82
+ MEDIA_LOOPS,
83
+ containerDurationOf,
84
+ defDurationFrames,
85
+ homeOf,
86
+ retimeVideo,
87
+ retimeVideoRefusal,
88
+ setVideoSound,
89
+ setVideoSoundRefusal,
90
+ setVideoSource,
91
+ videoClipOf,
92
+ } from "@dailies-studio/shot/doc";
93
+ import type {
94
+ ComponentLayer3D,
95
+ Doc3D,
96
+ InstanceLayer3D,
97
+ Layer3D,
98
+ MediaLoop,
99
+ Scene3D,
100
+ VideoLayer3D,
101
+ VideoSound,
102
+ } from "@dailies-studio/shot/doc";
103
+ import { useShotEditor, useStore } from "../../state/context.js";
104
+ import { framesToSeconds } from "@dailies-studio/engine";
105
+ import { formatDuration, lifespanOf, timecode, writeLifespan } from "../timeline/model.js";
106
+ import { KeyframeDot } from "./KeyframeDot.js";
107
+ import {
108
+ MAX_GAIN_DB,
109
+ MIN_GAIN_DB,
110
+ closingCaption,
111
+ closingEdge,
112
+ dbToGain,
113
+ gainAsDb,
114
+ naturalOutFrame,
115
+ } from "./timeModel.js";
116
+ import { useDocWrite, useScrub, useSelectedLayers, useSelectionScrub, useSelectionWrite } from "./useDocWrite.js";
117
+
118
+ /** The loops, as data, so a word renamed in the schema is a compile error here
119
+ * rather than a segment that writes a value the runtime ignores. */
120
+ const LOOP_OPTIONS = wordOptions(MEDIA_LOOPS);
121
+
122
+ /** A placement's loop (schema v47): the same two words, plus Off, which is the
123
+ * field's absence and what every older placement means. */
124
+ const PLACEMENT_LOOP_OFF = "off";
125
+ const PLACEMENT_LOOP_OPTIONS = [{ value: PLACEMENT_LOOP_OFF, label: "Off" }, ...LOOP_OPTIONS];
126
+
127
+ export function TimeSection() {
128
+ const store = useStore();
129
+ const write = useDocWrite();
130
+ const layers = useSelectedLayers();
131
+ const doc = useShotEditor((s) => s.doc);
132
+ const sceneId = useShotEditor((s) => s.sceneId);
133
+ const editingDef = useShotEditor((s) => s.editingDef);
134
+
135
+ // ONE layer, by the rule in this file's header. `layers` is already the
136
+ // selection AS PAINTED and in document order, so the single case is the
137
+ // whole of it.
138
+ const layer = layers.length === 1 ? layers[0] : undefined;
139
+ if (doc === null || layer === undefined) return null;
140
+ // The CONTAINER, the store's own `layerContainer` rule: a definition while
141
+ // one is open, and the scene otherwise. `writeLifespan` takes either.
142
+ const container = editingDef ?? sceneId;
143
+ if (container === null) return null;
144
+ // The SCENE, and null inside a definition. Every clip op resolves its layer
145
+ // through `doc.scenesById`, so a `cmp_` container would have them refuse
146
+ // silently — the same fork `writeLifespan` takes for exactly this reason.
147
+ const scene: Scene3D | undefined = editingDef === null && sceneId !== null ? doc.scenesById[sceneId] : undefined;
148
+ const life = lifespanOf(layer, containerDurationOf(doc, container));
149
+
150
+ return (
151
+ <InspectorSection className="insp-object" title="Time" persistKey="shot:time">
152
+ {/* A plain pair, two boxes across the row. A lifetime has no track, so
153
+ neither box carries a diamond, and neither is narrower for it. */}
154
+ <PropertyGrid>
155
+ <LifetimeField
156
+ side="in"
157
+ container={container}
158
+ layerId={layer.id}
159
+ value={life.in}
160
+ title={`Scene frame this layer appears on: ${timecode(life.in, doc.fps)}`}
161
+ onWrite={write}
162
+ />
163
+ <LifetimeField
164
+ side="out"
165
+ container={container}
166
+ layerId={layer.id}
167
+ value={life.out}
168
+ title={`Scene frame this layer leaves on: ${timecode(life.out, doc.fps)}`}
169
+ onWrite={write}
170
+ />
171
+ </PropertyGrid>
172
+
173
+ {/* THE TIMECODE, as a readout and not a second pair of fields. Frames are
174
+ what a lifetime is stored in and what a keyframe is written in, so the
175
+ fields stay in frames; the clock is what an author reads a cut
176
+ against, and having both means never converting by hand. Drawn as a
177
+ read-only BOX in the numeral face, the field's shape with nothing to
178
+ type into, so it reads as a value of this layer rather than a note:
179
+ the house readout (`insp-readout`, widgets.css). */}
180
+ <InspectorRow label="Timecode" className="insp-readout">
181
+ <span className="insp-dim" data-testid="time-timecode">
182
+ {timecode(life.in, doc.fps)} → {timecode(life.out, doc.fps)}
183
+ </span>
184
+ </InspectorRow>
185
+
186
+ {/* READ-ONLY, because it is not a third fact. Duration is `out - in`, and
187
+ a field for it would be a second absolute writer for one of the two
188
+ edges: typing into it would have to pick an edge to move, silently. */}
189
+ <InspectorRow label="Duration" className="insp-readout">
190
+ <span className="insp-dim" data-testid="time-duration">
191
+ {formatDuration(framesToSeconds(life.out - life.in, doc.fps))}
192
+ </span>
193
+ </InspectorRow>
194
+
195
+ {layer.kind === "video" && scene !== undefined && sceneId !== null ? (
196
+ <ClipRows doc={doc} scene={scene} sceneId={sceneId} layer={layer} />
197
+ ) : null}
198
+
199
+ {layer.kind === "instance" || layer.kind === "component" ? <PlacementLoopRows doc={doc} layer={layer} /> : null}
200
+
201
+ {layer.kind === "video" && scene !== undefined && sceneId !== null && layer.video.sound !== undefined ? (
202
+ <SoundRows
203
+ doc={doc}
204
+ sceneId={sceneId}
205
+ layer={layer}
206
+ sound={layer.video.sound}
207
+ onRefusal={(message) => store.reportError(message)}
208
+ />
209
+ ) : null}
210
+ </InspectorSection>
211
+ );
212
+ }
213
+
214
+ /**
215
+ * One lifetime edge, typed or dragged.
216
+ *
217
+ * Both halves re-read their subject from the document they are handed and
218
+ * measure the delta there. See this file's header: the typed value is
219
+ * absolute, `writeLifespan` is a gesture API, and the subtraction must not be
220
+ * taken against a render-time reading. A drag's every capture starts from the
221
+ * gesture's base, so its delta never compounds.
222
+ */
223
+ function LifetimeField(props: {
224
+ side: "in" | "out";
225
+ container: string;
226
+ layerId: string;
227
+ value: number;
228
+ title: string;
229
+ onWrite: (write: (doc: Doc3D) => Doc3D) => void;
230
+ }) {
231
+ const { side, container, layerId } = props;
232
+ const edge = (base: Doc3D, to: number): Doc3D => {
233
+ const subject = homeOf(base, container)?.layersById[layerId];
234
+ if (subject === undefined) return base;
235
+ const now = lifespanOf(subject, containerDurationOf(base, container));
236
+ const delta = Math.round(to) - now[side];
237
+ return delta === 0 ? base : writeLifespan(base, container, layerId, side, delta);
238
+ };
239
+ const scrub = useScrub((doc, raw) => edge(doc, Number(raw)));
240
+ return (
241
+ <InspectorRow>
242
+ <NumberField
243
+ value={props.value}
244
+ label={side === "in" ? "In" : "Out"}
245
+ ariaLabel={side === "in" ? "Lifetime in" : "Lifetime out"}
246
+ unit="f"
247
+ min={0}
248
+ step={1}
249
+ title={props.title}
250
+ onCommit={(next) => props.onWrite((base) => edge(base, next))}
251
+ onScrubStart={scrub.start}
252
+ onScrub={scrub.move}
253
+ onScrubEnd={scrub.end}
254
+ />
255
+ </InspectorRow>
256
+ );
257
+ }
258
+
259
+ /**
260
+ * A drag over one of this section's REFUSING ops: `useScrub`'s bracket, with
261
+ * the op's refusal kept from the last capture and said ONCE, at the release,
262
+ * the way `useSelectionScrub` says a lock.
263
+ *
264
+ * A REFUSED CAPTURE HOLDS THE LAST VALUE THIS GESTURE LANDED. Every capture
265
+ * re-derives from the gesture's base, so a refused one that wrote the base
266
+ * threw the whole drag away: a fade dragged to 10 and on past what the sound
267
+ * allows went back to 0 at the release. It re-runs the op with the last value
268
+ * it accepted instead (the press value when it accepted none), so the field
269
+ * stops where the op stopped and the release lands that.
270
+ *
271
+ * `refused` hears the refusal and the value it refused, for a row that holds
272
+ * it on screen; `landed` hears a release whose last capture was accepted, so
273
+ * that row can let go of a refusal it was still holding.
274
+ */
275
+ function useOpScrub(
276
+ write: (doc: Doc3D, value: number) => { doc: Doc3D; refusal: string | null },
277
+ on: { refused?: (why: string, value: number) => void; landed?: () => void } = {},
278
+ ): ScrubHandlers {
279
+ const store = useStore();
280
+ const gesture = useRef<{ refusal: string | null; value: number; landed: number | null } | null>(null);
281
+ const scrub = useScrub((doc, raw) => {
282
+ const value = Number(raw);
283
+ const out = write(doc, value);
284
+ const landed = out.refusal === null ? value : (gesture.current?.landed ?? null);
285
+ gesture.current = { refusal: out.refusal, value, landed };
286
+ return out.refusal === null || landed === null ? out.doc : write(doc, landed).doc;
287
+ });
288
+ return {
289
+ start: () => {
290
+ gesture.current = null;
291
+ scrub.start();
292
+ },
293
+ move: scrub.move,
294
+ end: (commit) => {
295
+ scrub.end(commit);
296
+ const last = gesture.current;
297
+ gesture.current = null;
298
+ if (!commit || last === null) return;
299
+ if (last.refusal === null) {
300
+ on.landed?.();
301
+ return;
302
+ }
303
+ store.reportError(last.refusal);
304
+ on.refused?.(last.refusal, last.value);
305
+ },
306
+ };
307
+ }
308
+
309
+ /**
310
+ * The clip half: which part of the recording this window selects, how fast it
311
+ * runs, what happens past its end, and which edge actually closes it.
312
+ *
313
+ * A child component rather than inline JSX because the video case needs its own
314
+ * hooks (the speed field keeps a draft while a refusal is on screen) and a hook
315
+ * cannot live behind the `kind === "video"` test above.
316
+ */
317
+ function ClipRows(props: { doc: Doc3D; scene: Scene3D; sceneId: string; layer: VideoLayer3D }) {
318
+ const { doc, scene, sceneId, layer } = props;
319
+ const store = useStore();
320
+ const write = useDocWrite();
321
+ // Natural length is a plain button, so it reads a viewer's lock here.
322
+ const locked = useLocked();
323
+ const rate = layer.video.timeScale ?? 1;
324
+ const clip = videoClipOf(layer, scene.durationFrames, doc.fps, doc.assets[layer.video.asset]);
325
+ const close = closingEdge(clip, scene.durationFrames, rate);
326
+ const natural = naturalOutFrame(clip, layer.video.offsetFrames ?? 0, rate);
327
+
328
+ /** Both source edges through ONE op. `setVideoSource` is the only thing in
329
+ * the repo that converts between the two rulers, and its header says why:
330
+ * expressing a source statement through the gesture ops means the caller
331
+ * doing the `(f - offsetFrames) * timeScale` arithmetic, which is exactly
332
+ * the drift `videoClipOf` refuses. */
333
+ const source = (base: Doc3D, which: "inFrames" | "outFrames", to: number): Doc3D =>
334
+ setVideoSource(base, sceneId, layer.id, { [which]: Math.round(to) });
335
+ // The drag halves: `setVideoSource` clamps to the recording itself, so a
336
+ // capture past either end lands on it rather than refusing.
337
+ const inScrub = useScrub((doc, raw) => source(doc, "inFrames", Number(raw)));
338
+ const outScrub = useScrub((doc, raw) => source(doc, "outFrames", Number(raw)));
339
+
340
+ return (
341
+ <>
342
+ {/* A plain pair: the source edges in the recording's own frames. */}
343
+ <PropertyGrid>
344
+ <InspectorRow>
345
+ <NumberField
346
+ value={clip.sourceInFrames}
347
+ label="Src in"
348
+ ariaLabel="Source in frame"
349
+ unit="f"
350
+ step={1}
351
+ // NO `min`. A negative value is a real state — the window opens
352
+ // before the recording does — and `NumberField` clamps on every
353
+ // commit path, so a floor here would erase it on the first touch.
354
+ // The WRITE clamps instead, in `setVideoSource`, which is the
355
+ // ceiling this file's header states.
356
+ title="Which source frame plays under the window's in. Moves the footage, never the window"
357
+ onCommit={(next) => write((base) => source(base, "inFrames", next))}
358
+ onScrubStart={inScrub.start}
359
+ onScrub={inScrub.move}
360
+ onScrubEnd={inScrub.end}
361
+ />
362
+ </InspectorRow>
363
+ <InspectorRow>
364
+ <NumberField
365
+ value={clip.sourceOutFrames}
366
+ label="Src out"
367
+ ariaLabel="Source out frame"
368
+ unit="f"
369
+ step={1}
370
+ title="Which source frame plays under the window's out. Moves the out, never the in"
371
+ onCommit={(next) => write((base) => source(base, "outFrames", next))}
372
+ onScrubStart={outScrub.start}
373
+ onScrub={outScrub.move}
374
+ onScrubEnd={outScrub.end}
375
+ />
376
+ </InspectorRow>
377
+ </PropertyGrid>
378
+
379
+ <SpeedRow
380
+ doc={doc}
381
+ sceneId={sceneId}
382
+ layer={layer}
383
+ rate={rate}
384
+ onWrite={(next) => write(next)}
385
+ onRefusal={(message) => store.reportError(message)}
386
+ />
387
+
388
+ {/* Past the recording's END. `hold` is the default — a screen capture
389
+ under a longer shot ends on its last frame rather than restarting
390
+ mid-shot — and every document written before schema v22 carries an
391
+ explicit `loop` put there by the migration, so an old film keeps the
392
+ wrap it was authored with. */}
393
+ <InspectorRow label="Past end">
394
+ <SegmentedControl
395
+ ariaLabel="Video loop"
396
+ value={layer.video.loop ?? "hold"}
397
+ options={LOOP_OPTIONS}
398
+ onChange={(next) =>
399
+ write((base) => patchVideo(base, sceneId, layer.id, (v) => ({ ...v, loop: next as MediaLoop })))
400
+ }
401
+ />
402
+ </InspectorRow>
403
+
404
+ {/* THE NATURAL LENGTH. Through `writeLifespan` like every other edge, so
405
+ the footage clamp and the scene clamp are `trimLayer`'s and not a
406
+ second pair here. DISABLED, never hidden, when nobody has probed the
407
+ asset: the affordance for a capability stays visible at rest (the
408
+ design system's rule), and the tooltip says what is missing. */}
409
+ <InspectorRow label="Length">
410
+ <button
411
+ type="button"
412
+ className="btn"
413
+ data-testid="time-natural-length"
414
+ disabled={locked || natural === null}
415
+ title={
416
+ natural === null
417
+ ? "This recording has not been probed, so its length is unknown"
418
+ : `Set the out so the whole recording plays once, at frame ${natural}`
419
+ }
420
+ onClick={() =>
421
+ write((base) => {
422
+ if (natural === null) return base;
423
+ const now = lifespanOf(base.scenesById[sceneId]?.layersById[layer.id] ?? layer, scene.durationFrames);
424
+ return natural === now.out ? base : writeLifespan(base, sceneId, layer.id, "out", natural - now.out);
425
+ })
426
+ }
427
+ >
428
+ Natural length
429
+ </button>
430
+ </InspectorRow>
431
+
432
+ <InspectorRow>
433
+ <span className="insp-notice" data-testid="time-closing-edge">
434
+ {closingCaption(close, doc.fps, rate)}
435
+ </span>
436
+ </InspectorRow>
437
+ </>
438
+ );
439
+ }
440
+
441
+ /**
442
+ * The rate, with KEEP-WINDOW semantics and its refusal on screen.
443
+ *
444
+ * `retimeVideo` pins the source frame under the window's `in` across the
445
+ * change, so the bar does not move and the footage does not scrub sideways
446
+ * under a cut that is already timed. It REFUSES a rate away from 1 on a layer
447
+ * whose inline sound is in the mix, because neither mixer can resample — and a
448
+ * refusal that only returned the document unchanged would read as a field that
449
+ * snaps back for no reason.
450
+ *
451
+ * So the typed value is held in a DRAFT while the refusal is up. Without it the
452
+ * field re-renders from the document, shows the old rate, and the author's only
453
+ * evidence that anything happened is one line in the error strip.
454
+ *
455
+ * ── AND THE DRAFT IS RE-ASKED EVERY RENDER, NOT LATCHED ─────────────────
456
+ *
457
+ * The refusal names its own cure ("mute or remove the sound first"), so the
458
+ * document that cures it arrives while the draft is still on screen. A latched
459
+ * draft would then keep showing 2 over a layer that is now free to BE 2 — and
460
+ * because the field's value already reads 2, retyping it commits nothing at
461
+ * all: the author performs the cure the message asked for and the control is
462
+ * dead. So the stored draft is only honoured while the refusal still applies,
463
+ * and the field falls back to the document's own rate the moment it lifts.
464
+ */
465
+ function SpeedRow(props: {
466
+ doc: Doc3D;
467
+ sceneId: string;
468
+ layer: VideoLayer3D;
469
+ rate: number;
470
+ onWrite: (write: (doc: Doc3D) => Doc3D) => void;
471
+ onRefusal: (message: string | null) => void;
472
+ }) {
473
+ const { doc, sceneId, layer, rate } = props;
474
+ const [refused, setRefused] = useState<{ value: number; why: string } | null>(null);
475
+ // THE DRAG HALF, through the same op and the same refusal as a typed rate:
476
+ // a capture the sound forbids lands nothing new, and the release holds the
477
+ // refused rate on screen exactly as a typed one is held. A drag that lands
478
+ // lets go of a held one, as a typed rate that lands does: otherwise the field
479
+ // kept showing the refused 2 and its notice over a document now at 1.
480
+ const scrub = useOpScrub(
481
+ (base, raw) => {
482
+ const to = speedOf(raw);
483
+ const why = retimeVideoRefusal(base, sceneId, layer.id, to);
484
+ return why !== null ? { doc: base, refusal: why } : { doc: retimeVideo(base, sceneId, layer.id, to), refusal: null };
485
+ },
486
+ {
487
+ refused: (why, raw) => setRefused({ value: speedOf(raw), why }),
488
+ landed: () => setRefused(null),
489
+ },
490
+ );
491
+ // Re-asked, never latched — see the header. `retimeVideoRefusal` is a pure
492
+ // read over the live document, so the draft survives exactly as long as the
493
+ // reason for it does.
494
+ const standing =
495
+ refused !== null && retimeVideoRefusal(doc, sceneId, layer.id, refused.value) !== null ? refused : null;
496
+ return (
497
+ <>
498
+ <InspectorRow>
499
+ <NumberField
500
+ value={standing?.value ?? rate}
501
+ label="Speed"
502
+ ariaLabel="Video speed"
503
+ unit="x"
504
+ step={0.1}
505
+ // NO `min`, for the reason the source-in row has none: a negative
506
+ // rate is a legal authored value (backwards playback from a late
507
+ // start) and `NumberField` clamps on every commit path.
508
+ title="Playback rate, keeping the window and the frame it opens on. 2 runs the recording twice as fast"
509
+ onCommit={(next) => {
510
+ const to = speedOf(next);
511
+ const why = retimeVideoRefusal(doc, sceneId, layer.id, to);
512
+ if (why !== null) {
513
+ setRefused({ value: to, why });
514
+ props.onRefusal(why);
515
+ return;
516
+ }
517
+ setRefused(null);
518
+ props.onWrite((base) => retimeVideo(base, sceneId, layer.id, to));
519
+ }}
520
+ onScrubStart={scrub.start}
521
+ onScrub={scrub.move}
522
+ onScrubEnd={scrub.end}
523
+ />
524
+ </InspectorRow>
525
+ {standing === null ? null : (
526
+ <InspectorRow>
527
+ <span className="insp-notice is-warn" data-testid="time-speed-refusal">
528
+ {standing.why}
529
+ </span>
530
+ </InspectorRow>
531
+ )}
532
+ </>
533
+ );
534
+ }
535
+
536
+ /** A rate as the document may hold it: three decimals, and 0 (or anything
537
+ * not a number) snapped to 1. Zero is REFUSED at the save gate rather than
538
+ * written, so it is snapped here instead of being handed to the validator as
539
+ * an error the author cannot see the cause of. */
540
+ function speedOf(next: number): number {
541
+ const rounded = Number(next.toFixed(3));
542
+ return rounded === 0 || !Number.isFinite(rounded) ? 1 : rounded;
543
+ }
544
+
545
+ /**
546
+ * THE RECORDING'S OWN SOUND, on the layer that draws its picture.
547
+ *
548
+ * Only for a video that HAS one: `video.sound` absent means silent, which is
549
+ * every video layer written before the field existed, and a level field over
550
+ * nothing is three controls that write into a record that is not there.
551
+ * Attaching one is `add_footage`'s and the dock's job, not a level row's.
552
+ *
553
+ * ── THE GAIN AND THE REST TAKE DIFFERENT DOORS, ON PURPOSE ──────────────
554
+ *
555
+ * GAIN goes through `useSelectionWrite`, because `video.sound.gain` is a
556
+ * keyframable path: the write has to become a KEYFRAME at the playhead when the
557
+ * layer animates it, and that is precisely what `routeLayerEdit` does inside
558
+ * that hook. `setVideoSound` would write the base instead, which a track then
559
+ * ignores.
560
+ *
561
+ * MUTE AND THE FADES go through `setVideoSound`, because they are not tracked
562
+ * and they ARE gated: a fade pair longer than the frames the recording is
563
+ * audible over, a gain past the mixer's ceiling, a duck naming a clip that is
564
+ * gone — six rules that live in `validateShotDoc` and that the predicate asks
565
+ * rather than restates. Writing those fields raw would produce a document the
566
+ * save gate refuses, with the author holding the only copy.
567
+ *
568
+ * ── LOCK GOVERNS THIS SECTION LIKE EVERY OTHER ─────────────────────────
569
+ *
570
+ * `useDocWrite` and `useSelectionWrite` both run `lockRefuses`, and so does the
571
+ * dock's bar drag, so a locked clip refuses In/Out, the source rows, the sound
572
+ * rows and Gain alike, and names the layer.
573
+ *
574
+ * ── AND THE PATCH IS BUILT FROM THE BASE RECORD, NEVER THE RESOLVED ONE ──
575
+ *
576
+ * `setVideoSound` takes a WHOLE `VideoSound` and writes it verbatim, so the
577
+ * record handed to it is the record the document ends up with — every field,
578
+ * not only the one the author touched. `props.sound` is the layer AS PAINTED
579
+ * (`useSelectedLayers` runs `resolveTracksAt`), so when `video.sound.gain` is
580
+ * KEYFRAMED that record's `gain` is a SAMPLE OF AN ANIMATION at the playhead.
581
+ * Spreading it meant ticking Mute or nudging a fade froze the whole automation
582
+ * to whatever the needle happened to read on that frame, silently and with the
583
+ * author's only copy of the curve gone the moment the save landed.
584
+ *
585
+ * This is exactly the asymmetry `useDocWrite`'s header exists to state: READ
586
+ * resolved, WRITE the base. So `baseSoundOf` re-reads the authored record out
587
+ * of the document being written into, `next` patches the one field the author
588
+ * touched, and the gain the track owns passes through untouched — which is
589
+ * also what keeps `routeTrackedEdit`'s "the base is dead once a track exists"
590
+ * true rather than something this section quietly overwrites.
591
+ */
592
+ function SoundRows(props: {
593
+ doc: Doc3D;
594
+ sceneId: string;
595
+ layer: VideoLayer3D;
596
+ sound: VideoSound;
597
+ onRefusal: (message: string | null) => void;
598
+ }) {
599
+ const { doc, sceneId, layer, sound } = props;
600
+ const write = useDocWrite();
601
+ const selectionWrite = useSelectionWrite();
602
+ // THE DRAG HALVES. The gain through the selection seam its typed value takes
603
+ // (so a drag over an animated gain keys it), the fades through the same gate
604
+ // their typed values ask, refusing a capture the gate refuses.
605
+ const gainScrub = useSelectionScrub((subject, raw) => withGain(subject, Number(raw)));
606
+ const fadeScrub = (key: "fadeInFrames" | "fadeOutFrames") => (base: Doc3D, value: number) => {
607
+ const from = baseSoundOf(base, sceneId, layer.id);
608
+ if (from === undefined) return { doc: base, refusal: null };
609
+ const next = withFade(from, key, value);
610
+ const why = setVideoSoundRefusal(base, sceneId, layer.id, next);
611
+ return why !== null ? { doc: base, refusal: why } : { doc: setVideoSound(base, sceneId, layer.id, next), refusal: null };
612
+ };
613
+ const fadeInScrub = useOpScrub(fadeScrub("fadeInFrames"));
614
+ const fadeOutScrub = useOpScrub(fadeScrub("fadeOutFrames"));
615
+
616
+ /** One door for the three gated fields, with the refusal reported rather
617
+ * than swallowed: a control that silently did nothing is the failure this
618
+ * whole lane keeps removing.
619
+ *
620
+ * `next` is applied TWICE and to two different records on purpose. The
621
+ * predicate has to judge the document the author is looking at, so it gets
622
+ * the base read out of `doc`; the write re-reads and re-patches inside
623
+ * `useDocWrite`'s own document, which is this file's rule for every
624
+ * untracked field (see the header) and is what makes a commit that lands
625
+ * after an undo or another panel's edit measure against the right subject.
626
+ * `next` is pure — `withFade` and `without` build a new object and read
627
+ * nothing else — so applying it twice cannot compound. */
628
+ const patchSound = (next: (from: VideoSound) => VideoSound): void => {
629
+ const base = baseSoundOf(doc, sceneId, layer.id);
630
+ if (base === undefined) return;
631
+ const why = setVideoSoundRefusal(doc, sceneId, layer.id, next(base));
632
+ if (why !== null) {
633
+ props.onRefusal(why);
634
+ return;
635
+ }
636
+ write((into) => {
637
+ const from = baseSoundOf(into, sceneId, layer.id);
638
+ return from === undefined ? into : setVideoSound(into, sceneId, layer.id, next(from));
639
+ });
640
+ };
641
+
642
+ return (
643
+ <>
644
+ {/* A HEADING and not a nested `InspectorSection`: the sound is part of
645
+ this clip's timing, and a second collapsible inside one would give
646
+ the author two disclosure states for one subject. */}
647
+ <div className="insp-heading">Recording sound</div>
648
+
649
+ {/* dB, because that is the unit a level decision is made in, over a
650
+ document that stores the LINEAR multiplier the engine applies. The
651
+ conversion is `timeModel.ts`'s and its round trip is pinned there.
652
+ Both ends are the mixer's: silence (`MIN_GAIN_DB`, where the gain
653
+ reads back as a true 0) and its ceiling, so a drag stops where the
654
+ commit would. */}
655
+ <InspectorRow>
656
+ <NumberField
657
+ value={gainAsDb(sound.gain ?? 1)}
658
+ label="Gain"
659
+ ariaLabel="Recording gain"
660
+ unit="dB"
661
+ step={0.1}
662
+ min={MIN_GAIN_DB}
663
+ max={MAX_GAIN_DB}
664
+ title="Level of the recording's own sound. 0 dB is the file as recorded"
665
+ trailing={<KeyframeDot path="video.sound.gain" label="Gain" />}
666
+ onCommit={(next) => selectionWrite((subject) => withGain(subject, next))}
667
+ onScrubStart={gainScrub.start}
668
+ onScrub={gainScrub.move}
669
+ onScrubEnd={gainScrub.end}
670
+ />
671
+ </InspectorRow>
672
+
673
+ <InspectorRow label="Mute">
674
+ <Toggle
675
+ checked={sound.muted === true}
676
+ ariaLabel="Mute the recording's sound"
677
+ onChange={(checked) =>
678
+ patchSound((from) => (checked ? { ...from, muted: true } : without(from, "muted")))
679
+ }
680
+ />
681
+ </InspectorRow>
682
+
683
+ {/* A plain pair, the two fades. */}
684
+ <PropertyGrid>
685
+ <InspectorRow>
686
+ <NumberField
687
+ value={sound.fadeInFrames ?? 0}
688
+ label="Fade in"
689
+ ariaLabel="Recording fade in"
690
+ unit="f"
691
+ min={0}
692
+ step={1}
693
+ onCommit={(next) => patchSound((from) => withFade(from, "fadeInFrames", next))}
694
+ onScrubStart={fadeInScrub.start}
695
+ onScrub={fadeInScrub.move}
696
+ onScrubEnd={fadeInScrub.end}
697
+ />
698
+ </InspectorRow>
699
+ <InspectorRow>
700
+ <NumberField
701
+ value={sound.fadeOutFrames ?? 0}
702
+ label="Fade out"
703
+ ariaLabel="Recording fade out"
704
+ unit="f"
705
+ min={0}
706
+ step={1}
707
+ onCommit={(next) => patchSound((from) => withFade(from, "fadeOutFrames", next))}
708
+ onScrubStart={fadeOutScrub.start}
709
+ onScrub={fadeOutScrub.move}
710
+ onScrubEnd={fadeOutScrub.end}
711
+ />
712
+ </InspectorRow>
713
+ </PropertyGrid>
714
+ </>
715
+ );
716
+ }
717
+
718
+ /**
719
+ * A placement's loop (schema v47): what a component's clock does past its
720
+ * period. A code instance names its period (`loopFrames`), because a code
721
+ * component has no length of its own; a Studio-made component's period is its
722
+ * definition's length, shown here and edited on the definition. Both go
723
+ * through `useSelectionWrite`, which writes into whichever container holds the
724
+ * layer. Off removes the fields, the absence every older placement has.
725
+ */
726
+ function PlacementLoopRows(props: { doc: Doc3D; layer: InstanceLayer3D | ComponentLayer3D }) {
727
+ const { doc, layer } = props;
728
+ const write = useSelectionWrite();
729
+ const loop = layer.kind === "instance" ? layer.instance.loop : layer.component.loop;
730
+ // A loop needs a period the moment it is switched on: one second of frames.
731
+ const second = Math.max(1, Math.round(doc.fps.num / doc.fps.den));
732
+ const def = layer.kind === "component" ? doc.components?.[layer.component.def] : undefined;
733
+ return (
734
+ <>
735
+ <InspectorRow label="Loop">
736
+ <SegmentedControl
737
+ ariaLabel="Placement loop"
738
+ value={loop ?? PLACEMENT_LOOP_OFF}
739
+ options={PLACEMENT_LOOP_OPTIONS}
740
+ onChange={(next) => write((base) => withPlacementLoop(base, next, second))}
741
+ />
742
+ </InspectorRow>
743
+ {layer.kind === "instance" && loop !== undefined ? (
744
+ <InspectorRow>
745
+ <NumberField
746
+ value={layer.instance.loopFrames ?? second}
747
+ label="Every"
748
+ ariaLabel="Loop period"
749
+ title="The loop's period, in frames"
750
+ unit="f"
751
+ min={1}
752
+ step={1}
753
+ onCommit={(next) =>
754
+ write((base) =>
755
+ base.kind === "instance" && base.instance.loop !== undefined
756
+ ? { ...base, instance: { ...base.instance, loopFrames: Math.max(1, Math.round(next)) } }
757
+ : base,
758
+ )
759
+ }
760
+ />
761
+ </InspectorRow>
762
+ ) : null}
763
+ {def !== undefined && loop !== undefined ? (
764
+ <InspectorRow label="Every" className="insp-readout">
765
+ <span className="insp-dim" data-testid="time-loop-period" title="The definition's length">
766
+ {`${defDurationFrames(def)}f · ${formatDuration(framesToSeconds(defDurationFrames(def), doc.fps))}`}
767
+ </span>
768
+ </InspectorRow>
769
+ ) : null}
770
+ </>
771
+ );
772
+ }
773
+
774
+ /** One placement's loop set to `next` ("off", "hold" or "loop"). Off deletes
775
+ * the keys rather than writing undefined, the `exactOptionalPropertyTypes`
776
+ * rule; an instance switched on keeps its period or takes `period`. */
777
+ function withPlacementLoop(layer: Layer3D, next: string, period: number): Layer3D {
778
+ const on = next === PLACEMENT_LOOP_OFF ? null : (next as MediaLoop);
779
+ if (layer.kind === "instance") {
780
+ const { loop: _loop, loopFrames, ...rest } = layer.instance;
781
+ return { ...layer, instance: on === null ? rest : { ...rest, loop: on, loopFrames: loopFrames ?? period } };
782
+ }
783
+ if (layer.kind === "component") {
784
+ const { loop: _loop, ...rest } = layer.component;
785
+ return { ...layer, component: on === null ? rest : { ...rest, loop: on } };
786
+ }
787
+ return layer;
788
+ }
789
+
790
+ /**
791
+ * The recording's sound AS AUTHORED, out of the document it is read from.
792
+ *
793
+ * The one record `setVideoSound` may legally be handed. See `SoundRows`'s
794
+ * header: a resolved layer's `video.sound` carries a SAMPLED `gain` whenever
795
+ * the path is keyframed, and writing that back is how an animation gets
796
+ * flattened by a mute toggle.
797
+ *
798
+ * SCENE-ADDRESSED and not `homeOf`, matching the clip ops above: every
799
+ * `setVideoSound` call resolves its layer through `doc.scenesById`, so a
800
+ * container id that is a definition has to answer undefined here rather than
801
+ * hand back a record the op would then refuse to write.
802
+ */
803
+ function baseSoundOf(doc: Doc3D, sceneId: string, layerId: string): VideoSound | undefined {
804
+ const layer: Layer3D | undefined = doc.scenesById[sceneId]?.layersById[layerId];
805
+ return layer !== undefined && layer.kind === "video" ? layer.video.sound : undefined;
806
+ }
807
+
808
+ /** The recording's gain set from dB on one layer, the one builder its typed
809
+ * value and its drag both run. Another layer, or a video with no sound, is
810
+ * left alone. */
811
+ function withGain(subject: Layer3D, db: number): Layer3D {
812
+ return subject.kind === "video" && subject.video.sound !== undefined
813
+ ? {
814
+ ...subject,
815
+ video: { ...subject.video, sound: { ...subject.video.sound, gain: roundGain(dbToGain(db)) } },
816
+ }
817
+ : subject;
818
+ }
819
+
820
+ /** Four decimals, `audioLoudness.ts`'s own rule for a stored gain: enough to be
821
+ * exact at any audible level, few enough that the number written into the
822
+ * document is one a human can read back. Clamped to the mixer's range, so the
823
+ * field's dB ceiling and the document's linear one are one limit. */
824
+ function roundGain(gain: number): number {
825
+ const clamped = Math.max(0, Math.min(MAX_AUDIO_GAIN, Number.isFinite(gain) ? gain : 1));
826
+ return Math.round(clamped * 1e4) / 1e4;
827
+ }
828
+
829
+ /** A fade of 0 DELETES the key: absent already means no fade, and an explicit
830
+ * 0 is a second way to say the same thing that codegen then prints. */
831
+ function withFade(sound: VideoSound, key: "fadeInFrames" | "fadeOutFrames", value: number): VideoSound {
832
+ const frames = Math.max(0, Math.round(Number.isFinite(value) ? value : 0));
833
+ return frames === 0 ? without(sound, key) : { ...sound, [key]: frames };
834
+ }
835
+
836
+ /** Drop one optional key rather than setting it to `undefined`. Under
837
+ * `exactOptionalPropertyTypes` those are different values, and codegen prints
838
+ * the record by key order — a present-but-undefined key is a key. */
839
+ function without(sound: VideoSound, key: "muted" | "fadeInFrames" | "fadeOutFrames"): VideoSound {
840
+ const { [key]: _drop, ...rest } = sound;
841
+ void _drop;
842
+ return rest;
843
+ }
844
+
845
+ /** A plain video-block patch, for the ONE field here that is neither a clip op
846
+ * nor a gated sound field. `loop` has no arithmetic and no gate: it picks
847
+ * which of two branches `sourceTimeAt` takes past the end. */
848
+ function patchVideo(
849
+ doc: Doc3D,
850
+ sceneId: string,
851
+ layerId: string,
852
+ next: (video: VideoLayer3D["video"]) => VideoLayer3D["video"],
853
+ ): Doc3D {
854
+ const scene = doc.scenesById[sceneId];
855
+ const layer: Layer3D | undefined = scene?.layersById[layerId];
856
+ if (scene === undefined || layer === undefined || layer.kind !== "video") return doc;
857
+ const video = next(layer.video);
858
+ if (video === layer.video) return doc;
859
+ return {
860
+ ...doc,
861
+ scenesById: {
862
+ ...doc.scenesById,
863
+ [sceneId]: { ...scene, layersById: { ...scene.layersById, [layerId]: { ...layer, video } } },
864
+ },
865
+ };
866
+ }