@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
@@ -13,21 +13,23 @@ import { createRoot } from "react-dom/client";
13
13
  import { StageAssetsContext, StudioSurfaceContext } from "@dailies-studio/runtime";
14
14
  import type { StageAssetResolver } from "@dailies-studio/runtime";
15
15
  // The stage is its OWN document and inherits nothing from the chrome's
16
- // stylesheet, so it adopts the design tokens by importing them. tokens.css and
17
- // NOT styles.css: the palette is inert until referenced, while the widget rules
18
- // and the global box-sizing reset would repaint the author's own content
19
- // differently here than in the export — which is exactly the preview != runtime
20
- // split this whole lane refuses.
21
- import "@dailies-studio/ui/tokens.css";
22
- // Then THIS LANE's palette on top, because the overlays drawn over the stage
23
- // read --accent, --guide and --frame-border, and the house values are not the
24
- // ones the chrome around them is using. Without it the selection quad and the
25
- // tilt handles paint in the house rose while every control beside them is
26
- // blue — the overlays were never wrong, they were reading a token nobody had
27
- // overridden for this document. Same file the chrome imports; one definition.
28
- import "../shotTokens.css";
29
- import { createStageClient } from "./bridgeClient.js";
16
+ // stylesheet, so it adopts the design tokens itself: the house file and THIS
17
+ // LANE's palette (the overlays read --accent, --guide, --frame-border), the same
18
+ // two files the chrome imports, one definition. tokens.css and NOT styles.css:
19
+ // the widget rules and the global box-sizing reset would repaint the author's
20
+ // own content differently here than in the export.
21
+ //
22
+ // AS TEXT, rescoped onto the furniture (furnitureTokens.ts says why): imported
23
+ // plainly they would declare every house name on the authored root.
24
+ import houseTokens from "@dailies-studio/ui/tokens.css?raw";
25
+ import laneTokens from "../shotTokens.css?raw";
26
+ import { scopeToFurniture } from "./furnitureTokens.js";
27
+ import { instanceComponentIdsOf, instanceModuleOf } from "@dailies-studio/shot/doc";
28
+ import { createStageClient, reportFatalStage } from "./bridgeClient.js";
29
+ import type { StageClient } from "./bridgeClient.js";
30
30
  import { StageApp } from "./StageApp.js";
31
+ import { StageRootBoundary } from "./StageRootBoundary.js";
32
+ import { STAGE_THEME_TOKENS_ID } from "./tokenColor.js";
31
33
 
32
34
  // WITHOUT THIS PROVIDER, IMAGE AND VIDEO LAYERS ARE BLANK. `useStageAssetResolver`
33
35
  // falls back to the bare content-addressed filename, which resolves against
@@ -44,7 +46,19 @@ export function boot(): void {
44
46
  return;
45
47
  }
46
48
  document.documentElement.style.height = "100%";
47
- document.body.style.cssText = "margin:0;height:100%;overflow:hidden;background:#000";
49
+ document.body.style.cssText =
50
+ "margin:0;height:100%;overflow:hidden;background:var(--dailies-stage-boot-background,#050505)";
51
+ const tokens = document.createElement("style");
52
+ tokens.dataset.dailiesStageTokens = "";
53
+ tokens.textContent = scopeToFurniture(houseTokens) + scopeToFurniture(laneTokens);
54
+ document.head.appendChild(tokens);
55
+ // A sibling token probe lets editor-only GL materials resolve the light
56
+ // palette without putting changing custom properties on authored content.
57
+ const themeTokens = document.createElement("div");
58
+ themeTokens.id = STAGE_THEME_TOKENS_ID;
59
+ themeTokens.dataset.dailiesStageFurniture = "";
60
+ themeTokens.hidden = true;
61
+ document.body.appendChild(themeTokens);
48
62
  const container = document.createElement("div");
49
63
  container.style.cssText = "position:fixed;inset:0";
50
64
  document.body.appendChild(container);
@@ -57,6 +71,8 @@ export function boot(): void {
57
71
  client.setDisposer(() => {
58
72
  root.unmount();
59
73
  container.remove();
74
+ themeTokens.remove();
75
+ tokens.remove();
60
76
  delete window.__shotStudio;
61
77
  });
62
78
 
@@ -73,10 +89,19 @@ export function boot(): void {
73
89
  // frame and no wall clock; anything that reads "no ambient clock" as "a
74
90
  // real page" and arms an animation would be animating under the selection
75
91
  // handles while a gesture reads live geometry off a moving target.
92
+ // The boundary wraps StageApp ONLY — the providers stay outside, so a
93
+ // catch cannot cost them, and the bridge handle was installed before any
94
+ // render and survives regardless. See StageRootBoundary's header for why
95
+ // this is not the placement rule's "boundary above the stage".
76
96
  root.render(
77
97
  <StudioSurfaceContext.Provider value={true}>
78
98
  <StageAssetsContext.Provider value={stageAssets}>
79
- <StageApp client={client} />
99
+ <StageRootBoundary
100
+ report={(m) => reportFatalStage(client, m)}
101
+ inset={() => client.getState().inset}
102
+ >
103
+ <StageApp client={client} />
104
+ </StageRootBoundary>
80
105
  </StageAssetsContext.Provider>
81
106
  </StudioSurfaceContext.Provider>,
82
107
  );
@@ -84,6 +109,30 @@ export function boot(): void {
84
109
  const unsubscribe = client.subscribe(renderOnConnect);
85
110
  renderOnConnect();
86
111
  forwardShotDocChanged();
112
+ const hot = typeof import.meta.hot?.on === "function" ? import.meta.hot : null;
113
+ if (hot !== null) watchHotUpdates(client, hot, (url) => import(/* @vite-ignore */ url), import.meta.env.BASE_URL ?? "/");
114
+
115
+ // An uncaught throw in this iframe used to die in ITS console — a surface
116
+ // the author has no reason to have open. Forwarded through the same lane
117
+ // every other stage failure takes, so the chrome names it. Listeners, not
118
+ // containment: the per-instance boundary, ClockDriver's frame containment,
119
+ // the stage-root boundary above and the panel boundaries own recovery; this
120
+ // is the last-resort report for whatever they do not cover (an event
121
+ // handler, an unawaited rejection).
122
+ // Kept in the stage's health too (bridge v35), which is how an agent that
123
+ // cannot see the strip hears of it.
124
+ window.addEventListener("error", (ev) => {
125
+ // Chromium's ResizeObserver notice is not an error of anything on stage
126
+ // (it reports a layout loop the browser already broke), so an agent is
127
+ // not told the stage failed over it.
128
+ if (!/^ResizeObserver loop/.test(ev.message)) client.recordError("stage", ev.message);
129
+ client.handlers()?.onError(`stage error: ${ev.message}`);
130
+ });
131
+ window.addEventListener("unhandledrejection", (ev) => {
132
+ const reason = ev.reason instanceof Error ? ev.reason.message : String(ev.reason);
133
+ client.recordError("stage", `unhandled rejection: ${reason}`);
134
+ client.handlers()?.onError(`stage rejection: ${reason}`);
135
+ });
87
136
  }
88
137
 
89
138
  /**
@@ -108,3 +157,54 @@ function forwardShotDocChanged(): void {
108
157
  }
109
158
  });
110
159
  }
160
+
161
+ /** One module of a Vite `update` payload, as its HMR client imports it. */
162
+ interface HotUpdate {
163
+ type?: string;
164
+ path?: string;
165
+ acceptedPath?: string;
166
+ timestamp?: number;
167
+ explicitImportRequired?: boolean;
168
+ }
169
+
170
+ /**
171
+ * What code the stage runs, told to its health (bridge v35 `health()`).
172
+ *
173
+ * A component edit is a Fast Refresh update here, never a reload: the stage
174
+ * keeps its document and its load time, and when the new module does not
175
+ * evaluate, Vite's client only logs it and the stage keeps the previous code.
176
+ * So each update's placed components are imported again at the URL Vite
177
+ * imported them at (its module map answers, nothing re-runs): one that
178
+ * rejects is a component failure in the stage's health, and the update moves
179
+ * `loadedAt`, which a code change's check waits on. Local only: the hosted
180
+ * code host serves no HMR and moves the module epoch instead.
181
+ */
182
+ export function watchHotUpdates(
183
+ client: StageClient,
184
+ hot: { on(event: "vite:afterUpdate", listener: (payload: { updates?: HotUpdate[] }) => void): void },
185
+ importModule: (url: string) => Promise<unknown>,
186
+ base: string,
187
+ ): void {
188
+ hot.on("vite:afterUpdate", (payload) => {
189
+ const doc = client.getState().doc;
190
+ const placed = new Set(doc === null ? [] : instanceComponentIdsOf(doc).map((id) => `/${instanceModuleOf(id)}`));
191
+ const updates = (payload.updates ?? []).filter(
192
+ (u) => u.type === "js-update" && typeof u.path === "string" && placed.has(u.path),
193
+ );
194
+ void Promise.all(
195
+ updates.map(async (u): Promise<readonly [string, string | null]> => {
196
+ const [path = u.path!, query] = (u.acceptedPath ?? u.path!).split("?");
197
+ const url = `${base}${path.slice(1)}?${u.explicitImportRequired === true ? "import&" : ""}t=${u.timestamp ?? 0}${query === undefined ? "" : `&${query}`}`;
198
+ try {
199
+ await importModule(url);
200
+ return [u.path!, null];
201
+ } catch (e) {
202
+ return [
203
+ u.path!,
204
+ `instance module ${u.path!.slice(1)} did not hot-update, so the stage still runs its previous code: ${e instanceof Error ? e.message : String(e)}`,
205
+ ];
206
+ }
207
+ }),
208
+ ).then((checked) => client.noteHotUpdate(new Map(checked)));
209
+ });
210
+ }
@@ -4,7 +4,7 @@
4
4
  // what a modal transform MEANS — which gesture kind each mode+axis emits and
5
5
  // with what numbers — and none of the plumbing. StageApp owns the pointer,
6
6
  // the rays and the capture-phase key listener; the store's gesture lane owns
7
- // what a delta DOES (base write, block, refusal). Nothing here imports three,
7
+ // what a delta DOES (base write, keyframe, refusal). Nothing here imports three,
8
8
  // so every row of the mapping table is testable in node.
9
9
  //
10
10
  // ── THE ONE IDEA: A MODAL IS THE EXISTING GESTURE LANE, RE-DRIVEN ───────
@@ -35,6 +35,20 @@ export interface ModalState {
35
35
  * deliberately not selectable (see the keymap's starter lane). */
36
36
  target: "layers" | "camera";
37
37
  axis: ModalAxis;
38
+ /**
39
+ * Is the constraint the PLANE normal to `axis`, rather than the axis itself?
40
+ *
41
+ * Blender's `⇧X`: "move in everything except x". The one thing a
42
+ * keyboard-only author could not say in this editor — the drawn gizmo grew
43
+ * plane pads for it, and this is the same constraint on the keys.
44
+ *
45
+ * MOVE ONLY, and `axisRefusal` says so by name for the other two. A rotation
46
+ * happens about one axis and has no plane; a `size` is a Vec2, so every
47
+ * "plane" of it is either a single-axis scale (which `S X` already is) or a
48
+ * uniform one (which bare `S` already is), and a second spelling for a row
49
+ * that exists is a row that can drift from it.
50
+ */
51
+ plane: boolean;
38
52
  }
39
53
 
40
54
  /** Degrees of tilt/roll per CSS pixel dragged. A whole screen width should be
@@ -68,6 +82,40 @@ export const SNAP_PX = 10;
68
82
  export const SNAP_DEG = 15;
69
83
  export const SNAP_RATIO = 0.1;
70
84
 
85
+ /**
86
+ * The smallest factor a POINTER may ask for, matching `MIN_RATIO` in
87
+ * interactions/move.ts.
88
+ *
89
+ * ── WHY THIS IS NOT `SNAP_RATIO` ────────────────────────────────────────
90
+ *
91
+ * It was. One constant did two jobs — the ctrl step AND the floor — and while
92
+ * the scale was exponential in the travel that was harmless, because reaching
93
+ * 0.1 took 736px of leftward drag and nobody went further. Measured about the
94
+ * pivot instead, a cursor halfway to the pivot IS 0.5 and a cursor a tenth of
95
+ * the way IS 0.1, so the floor started biting in the middle of ordinary
96
+ * gestures and a selection could not be shrunk past a tenth in one drag.
97
+ *
98
+ * Two constants: the step stays a tenth (which is what a scale's round numbers
99
+ * are), and the floor drops to where it only does the job it is for — keeping
100
+ * `applyGesture`'s refusal of a non-positive factor from firing on every
101
+ * pointermove of an overshoot. CTRL still floors at a whole step, so a snap can
102
+ * never round a layer to nothing.
103
+ */
104
+ export const MIN_SCALE_RATIO = 1e-3;
105
+
106
+ /**
107
+ * Ctrl's quantise, as ONE function.
108
+ *
109
+ * The mapping table below had it as a local closure and the pointer lane had
110
+ * nothing at all, so a drag on an arm and `G X` rounded by different rules —
111
+ * or rather, one rounded and the other did not. Off by default in the sense
112
+ * that matters: `snap: false` returns the value byte-identical, so a drag with
113
+ * no modifier is exactly the drag that shipped.
114
+ */
115
+ export function snapTo(n: number, step: number, snap: boolean): number {
116
+ return snap ? Math.round(n / step) * step : n;
117
+ }
118
+
71
119
  export function modalTarget(hasSelection: boolean): ModalState["target"] {
72
120
  return hasSelection ? "layers" : "camera";
73
121
  }
@@ -81,10 +129,41 @@ export function modalTarget(hasSelection: boolean): ModalState["target"] {
81
129
  * honest — a pill saying "Scale · Z" over a gesture that ignores z would be
82
130
  * a lie.
83
131
  */
84
- export function modalAxisToggle(state: ModalState, axis: "x" | "y" | "z"): ModalState {
132
+ export function modalAxisToggle(
133
+ state: ModalState,
134
+ axis: "x" | "y" | "z",
135
+ /** Shift was held: constrain to the PLANE normal to `axis` instead of to the
136
+ * axis. Blender's `⇧X`. */
137
+ plane = false,
138
+ ): ModalState {
139
+ if (plane && state.mode !== "move") return state;
85
140
  if (state.mode === "scale" && axis === "z") return state;
86
- if (state.target === "camera" && (state.mode === "scale" || axis === "z")) return state;
87
- return { ...state, axis: state.axis === axis ? null : axis };
141
+ // ── THE CAMERA'S CELLS, ROW BY ROW, MATCHING `axisRefusal` EXACTLY ──────
142
+ //
143
+ // This function is the GATE and `axisRefusal` is only its explanation: the
144
+ // caller refuses when this returns the state unchanged and then asks that
145
+ // one for the sentence. So a cell the two disagree about is a key that is
146
+ // swallowed with an empty pill — the silent-modal failure this file's header
147
+ // calls the one thing an author cannot debug by looking.
148
+ //
149
+ // A ROLL'S Z IS NOT A MISSING CHANNEL. `z` names the view axis, which is the
150
+ // only axis a framing turns about, so it is accepted and changes nothing —
151
+ // the same `z`-means-roll spelling the layer arm has.
152
+ if (state.target === "camera") {
153
+ if (state.mode === "scale") return state;
154
+ if (state.mode === "rotate" && (axis === "x" || axis === "y")) return state;
155
+ if (state.mode === "move" && axis === "z") return state;
156
+ }
157
+ // A CAMERA HAS ONE PLANE and it is the one it already moves in, so a plane
158
+ // lock there is a constraint that constrains nothing — refused rather than
159
+ // accepted as a label over unchanged behaviour.
160
+ if (plane && state.target === "camera") return state;
161
+ // The SAME constraint again clears it; a different one replaces it. Pressing
162
+ // `X` while `⇧X` is live is a different constraint, so it replaces — which is
163
+ // Blender's behaviour and the only one that lets an author correct a
164
+ // mis-pressed shift without leaving the gesture.
165
+ const same = state.axis === axis && state.plane === plane;
166
+ return { ...state, axis: same ? null : axis, plane: same ? false : plane };
88
167
  }
89
168
 
90
169
  /**
@@ -98,12 +177,33 @@ export function modalAxisToggle(state: ModalState, axis: "x" | "y" | "z"): Modal
98
177
  *
99
178
  * Data rather than a thrown string, because the pill renders it.
100
179
  */
101
- export function axisRefusal(state: ModalState, axis: "x" | "y" | "z"): string | null {
180
+ export function axisRefusal(
181
+ state: ModalState,
182
+ axis: "x" | "y" | "z",
183
+ plane = false,
184
+ ): string | null {
185
+ // The PLANE rows first, because they are refused for reasons the axis rows
186
+ // are not: a rotation happens about one axis and has no plane at all, and a
187
+ // `size` is a Vec2 whose every plane is a row that already exists.
188
+ if (plane && state.mode === "rotate") return "A ROTATION HAS NO PLANE";
189
+ if (plane && state.mode === "scale") return "SIZE HAS NO PLANES · USE X OR Y";
190
+ if (plane && state.target === "camera") return "A FRAMING IS ALREADY FLAT";
102
191
  if (state.mode === "scale" && axis === "z") {
103
192
  return "SIZE HAS NO Z";
104
193
  }
105
194
  if (state.target === "camera" && state.mode === "scale") return "A ZOOM HAS NO AXIS";
106
- if (state.target === "camera" && axis === "z") return "A FRAMING HAS NO Z";
195
+ // A CAMERA ROLL TURNS ABOUT THE VIEW AXIS AND NOTHING ELSE. Naming x or y
196
+ // here is asking to TILT the camera off the design-plane normal, which is the
197
+ // one thing the whole derived-framing model does not express: every layer's
198
+ // pixel-exactness at z = 0 is an arithmetic consequence of looking straight
199
+ // down -z, so a framing that leaned would not be a framing.
200
+ if (state.target === "camera" && state.mode === "rotate" && (axis === "x" || axis === "y")) {
201
+ return "A FRAMING ROLLS ABOUT THE VIEW AXIS ONLY";
202
+ }
203
+ // MOVE ONLY. `Z` on a camera ROTATE is not a missing channel, it is the roll
204
+ // axis being named explicitly — the same `z`-means-roll spelling the layer
205
+ // arm accepts — so it falls through to the accept below.
206
+ if (state.target === "camera" && state.mode === "move" && axis === "z") return "A FRAMING HAS NO Z";
107
207
  return null;
108
208
  }
109
209
 
@@ -122,7 +222,10 @@ export function axisRefusal(state: ModalState, axis: "x" | "y" | "z"): string |
122
222
  export function modalReadout(delta: GestureDelta | FramingDelta | null): string {
123
223
  if (delta === null) return "";
124
224
  if (!("kind" in delta)) {
125
- // A framing delta: the camera's own move or zoom.
225
+ // A framing delta: the camera's own move, zoom or roll. Roll is asked
226
+ // first because a roll delta carries `dzoom: 1` and zero travel, so the
227
+ // move row below would print "0, 0" over a camera that is visibly turning.
228
+ if (delta.droll !== undefined) return `${round(delta.droll)}°`;
126
229
  return delta.dzoom === 1
127
230
  ? `${round(delta.dx)}, ${round(delta.dy)}`
128
231
  : `×${delta.dzoom.toFixed(2)}`;
@@ -167,9 +270,24 @@ export function initialGestureKind(state: ModalState): GestureKind {
167
270
  /** The pill's text: the transform, its axis, and — for the camera — its
168
271
  * subject, so the implicit target is never ambiguous. */
169
272
  export function modalLabel(state: ModalState): string {
170
- const axis = state.axis === null ? "" : ` · ${state.axis.toUpperCase()}`;
273
+ // A PLANE LOCK IS NAMED BY WHAT IT MOVES IN, not by the axis it is normal to
274
+ // — "MOVE · YZ" is what the author is about to do, and `⇧X` is only how they
275
+ // asked for it. The gizmo's plane pads name themselves the same way.
276
+ const axis =
277
+ state.axis === null
278
+ ? ""
279
+ : state.plane
280
+ ? ` · ${(["x", "y", "z"] as const)
281
+ .filter((a) => a !== state.axis)
282
+ .join("")
283
+ .toUpperCase()}`
284
+ : ` · ${state.axis.toUpperCase()}`;
171
285
  if (state.target === "camera") {
172
- return state.mode === "scale" ? "CAMERA ZOOM" : `CAMERA MOVE${axis}`;
286
+ if (state.mode === "scale") return "CAMERA ZOOM";
287
+ // No axis suffix: a camera roll is about the view axis and has no other,
288
+ // which `axisRefusal` says out loud the moment an author asks for one.
289
+ if (state.mode === "rotate") return "CAMERA ROLL";
290
+ return `CAMERA MOVE${axis}`;
173
291
  }
174
292
  switch (state.mode) {
175
293
  case "move":
@@ -248,10 +366,32 @@ export function layerGestureFor(
248
366
  * rounding what the author literally spelled would be the tool arguing.
249
367
  */
250
368
  snap = false,
369
+ /**
370
+ * What the pointer did ABOUT THE PROJECTED PIVOT, when the stage could
371
+ * measure it: how far round it has swept, and how far from it the cursor now
372
+ * sits as a proportion of where the gesture started.
373
+ *
374
+ * ── WHY ROTATE AND SCALE STOPPED BEING HORIZONTAL TRAVEL ────────────────
375
+ *
376
+ * They were `travel.x * TILT_DEG_PER_PX` and `exp(travel.x / 320)`: the
377
+ * pointer's ANGULAR position round the selection was ignored entirely, so `R`
378
+ * did not feel like turning anything and pulling toward or away from the
379
+ * pivot during `S` did nothing. Blender measures both about the pivot, and
380
+ * the ponytail note that used to sit on the scale row named exactly this
381
+ * upgrade and exactly this way of getting it — "a projected pivot on
382
+ * `ModalLive`".
383
+ *
384
+ * Null when there is no pivot on screen to measure about (no working pane, a
385
+ * selection with no live frame at this playhead, a cursor sitting ON the
386
+ * projected pivot where the ratio is a division by nothing). The travel rows
387
+ * are the fallback, so the gesture degrades to its old feel rather than
388
+ * refusing — a rotation that stops responding is worse than one that responds
389
+ * the way it did last week.
390
+ */
391
+ measured: { sweepDeg: number; ratio: number } | null = null,
251
392
  ): GestureDelta | null {
252
393
  const s = screenScale > 0 && Number.isFinite(screenScale) ? screenScale : 1;
253
- const quantise = (n: number, step: number): number =>
254
- snap && exact === null ? Math.round(n / step) * step : n;
394
+ const quantise = (n: number, step: number): number => snapTo(n, step, snap && exact === null);
255
395
  switch (state.mode) {
256
396
  case "move": {
257
397
  if (state.axis === "z") {
@@ -275,18 +415,49 @@ export function layerGestureFor(
275
415
  dx: state.axis === "y" ? 0 : exact,
276
416
  dy: state.axis === "x" ? 0 : exact,
277
417
  dz: 0,
418
+ // A TYPED VALUE IS THE STRONGEST "leave this alone" there is. `G X
419
+ // 200 Enter` has to land on 200, and a smart guide would move it to
420
+ // whatever edge happened to be near — silently, and against a number
421
+ // the author spelled out. Same field, stronger claim than ctrl's.
422
+ suppressGuides: true,
278
423
  };
279
424
  }
280
425
  if (planar === null) return null;
426
+ // ── A PLANE LOCK PASSES ALL THREE COMPONENTS ──────────────────────
427
+ //
428
+ // …because the CALLER has already done the constraining: the stage
429
+ // unprojects onto the lock plane itself, so the component along the
430
+ // locked-out axis is zero by construction rather than by a term here.
431
+ // Zeroing `dz` as the unconstrained row does would throw away the whole
432
+ // point of `⇧X` — a move in y and z is exactly a move with a z in it.
433
+ // CTRL asked for a round number, so the chrome's smart guides step aside
434
+ // for as long as it is held — see `GestureDelta.suppressGuides`. The same
435
+ // statement the pointer lane makes, from the lane that quantises the same
436
+ // way.
437
+ const rounded = snap && exact === null;
438
+ if (state.plane) {
439
+ return {
440
+ kind: "move",
441
+ dx: quantise(planar.dx, SNAP_PX),
442
+ dy: quantise(planar.dy, SNAP_PX),
443
+ dz: quantise(planar.dz ?? 0, SNAP_PX),
444
+ suppressGuides: rounded,
445
+ };
446
+ }
281
447
  return {
282
448
  kind: "move",
283
449
  dx: state.axis === "y" ? 0 : quantise(planar.dx, SNAP_PX),
284
450
  dy: state.axis === "x" ? 0 : quantise(planar.dy, SNAP_PX),
285
451
  dz: 0,
452
+ suppressGuides: rounded,
286
453
  };
287
454
  }
288
455
  case "rotate": {
289
- const deg = exact ?? quantise(travel.x * TILT_DEG_PER_PX, SNAP_DEG);
456
+ // THE ANGLE SWEPT ABOUT THE PIVOT, when there is one to sweep about —
457
+ // Blender's measure, and the one that makes `R` feel like turning
458
+ // something. The horizontal-travel row below it is the fallback, for a
459
+ // pivot that is not on screen to be measured against.
460
+ const deg = exact ?? quantise(measured?.sweepDeg ?? travel.x * TILT_DEG_PER_PX, SNAP_DEG);
290
461
  // transformFor's tilt arms: tilt.x (rx) rides `dy`, tilt.y (ry) rides
291
462
  // `dx`. Roll is the default AND the z constraint — the view-axis-most
292
463
  // rotation this lane has.
@@ -321,17 +492,18 @@ export function layerGestureFor(
321
492
  // A ratio has no vector to rotate, so both are gone, and `axis` now names
322
493
  // the layer's OWN dimension — which is what `size` is measured in.
323
494
  //
324
- // EXPONENTIAL IN THE TRAVEL, so the factor is strictly positive at every
325
- // input and one screen inch is the same proportion whatever the layer
326
- // already is. It is also independent of `screenScale`, which is why a
327
- // modal scale is correct even while isolation has swapped an orthographic
328
- // rig into the pane the scale was measured against.
495
+ // THE CURSOR'S DISTANCE FROM THE PIVOT, over where it started —
496
+ // Blender's own measure, so pulling away grows the selection and pushing
497
+ // toward it shrinks. `measured` carries it; the stage refuses to measure
498
+ // when the cursor is ON the projected pivot, where the ratio is a
499
+ // division by nothing.
500
+ //
501
+ // THE FALLBACK IS THE OLD ROW, and it earns its keep: exponential in the
502
+ // horizontal travel, so the factor is strictly positive at every input,
503
+ // one screen inch is the same proportion whatever the layer already is,
504
+ // and it is independent of `screenScale` — which is what kept a modal
505
+ // scale correct while isolation had an orthographic rig in the pane.
329
506
  //
330
- // ponytail: Blender measures the ratio as the cursor's distance from the
331
- // pivot over the anchor's, which we cannot do without projecting the
332
- // pivot every frame — and which is undefined when the anchor lands on it.
333
- // Upgrade path is a projected pivot on `ModalLive`, if anyone reports the
334
- // feel differing.
335
507
  // A TYPED scale is the factor itself: `S 2` doubles it, which is what
336
508
  // the number means everywhere else in this editor and in Blender.
337
509
  // Quantised to a TENTH, which is what a scale's round numbers are —
@@ -348,7 +520,12 @@ export function layerGestureFor(
348
520
  // agree about what is representable, exactly as the axis-less numeric
349
521
  // move does above.
350
522
  if (exact !== null && !(exact > 0)) return null;
351
- const ratio = exact ?? Math.max(SNAP_RATIO, quantise(Math.exp(travel.x / SCALE_FALLOFF_PX), SNAP_RATIO));
523
+ const raw = measured?.ratio ?? Math.exp(travel.x / SCALE_FALLOFF_PX);
524
+ // Floored at a whole STEP while ctrl is held, so a quantise can never
525
+ // round a layer to nothing, and at `MIN_SCALE_RATIO` otherwise, so a free
526
+ // drag can shrink as far as the author drags — see that constant.
527
+ const floor = snap && exact === null ? SNAP_RATIO : MIN_SCALE_RATIO;
528
+ const ratio = exact ?? Math.max(floor, quantise(raw, SNAP_RATIO));
352
529
  const axis = state.axis === "x" || state.axis === "y" ? state.axis : "both";
353
530
  // Copied out of the readonly tuple the caller holds: `GestureDelta` is
354
531
  // the wire shape the store re-derives from on every capture, and handing
@@ -363,6 +540,11 @@ export function layerGestureFor(
363
540
  * G is the body drag (`planar` on the z = 0 plane; an axis zeroes a
364
541
  * component); S is the apex drag (exponential dzoom at the wheel's own
365
542
  * falloff, so a modal zoom and a wheel notch are the same step).
543
+ *
544
+ * R has a drawn grip too now: the transform gizmo's `arc:z` on the camera
545
+ * (`framingArcDelta`). The modal is the keyboard path to the same fourth pose
546
+ * number, which until either existed could only be reached by typing into the
547
+ * inspector.
366
548
  */
367
549
  export function framingDeltaFor(
368
550
  state: ModalState,
@@ -370,7 +552,24 @@ export function framingDeltaFor(
370
552
  planar: { dx: number; dy: number } | null,
371
553
  dollyFalloffPx: number,
372
554
  exact: number | null = null,
555
+ measured?: { sweepDeg: number } | null,
556
+ snap = false,
373
557
  ): FramingDelta | null {
558
+ if (state.mode === "rotate") {
559
+ // THE LAYER ROTATE ROW'S OWN MEASURE, deliberately: the angle swept about
560
+ // the pivot when one is on screen, horizontal travel when it is not. `R`
561
+ // has to feel like one gesture whether the subject is a layer or the
562
+ // camera, and two mappings would make the same drag turn two things by
563
+ // different amounts. Ctrl quantises to the same `SNAP_DEG` for the same
564
+ // reason, through the shared `snapTo` rather than a second closure.
565
+ //
566
+ // Sign is NEGATED against the layer arm. Rolling the CAMERA clockwise
567
+ // turns the picture counter-clockwise, so sharing the layer's sign would
568
+ // make the same drag appear to rotate the frame backwards.
569
+ const raw = measured?.sweepDeg ?? travel.x * TILT_DEG_PER_PX;
570
+ const deg = exact ?? snapTo(raw, SNAP_DEG, snap && exact === null);
571
+ return { dx: 0, dy: 0, dzoom: 1, droll: -deg };
572
+ }
374
573
  if (state.mode === "scale") {
375
574
  // `dzoom` is a MULTIPLIER, so a typed value maps onto it directly — and
376
575
  // must be positive, or `applyFramingDelta` takes the camera's zoom to zero
@@ -0,0 +1,60 @@
1
+ // The stage's MODULE EPOCH: which build of the project's modules it imports.
2
+ //
3
+ // Locally nothing ever posts, so the epoch stays null and every URL the stage
4
+ // builds is exactly what the project's Vite serves (instanceModules.ts). The
5
+ // hosted boot script compiles user code in the browser and posts
6
+ // `{ epoch, version }` on this channel after each compile; the stage then
7
+ // re-resolves its instance modules under `?v=<epoch>`, inside the same
8
+ // readiness gates an id-set change already holds (StageApp.tsx
9
+ // `useInstanceModules`).
10
+ //
11
+ // A BroadcastChannel, not the bridge: the epoch is a same-origin fact about
12
+ // the served modules, not chrome-stage protocol, so the bridge and its version
13
+ // constant do not move.
14
+ import { useSyncExternalStore } from "react";
15
+ import { MODULE_EPOCH_CHANNEL, MODULE_EPOCH_MESSAGE_VERSION } from "../hostApi.js";
16
+
17
+ // The channel and the message version live with the host interface
18
+ // (hostApi.ts), which the boot script bundles without React.
19
+ export { MODULE_EPOCH_CHANNEL, MODULE_EPOCH_MESSAGE_VERSION };
20
+
21
+ /** Null until the first epoch arrives, which locally is never. */
22
+ export type ModuleEpoch = number | null;
23
+
24
+ let epoch: ModuleEpoch = null;
25
+ const listeners = new Set<() => void>();
26
+
27
+ function receive(data: unknown): void {
28
+ if (data === null || typeof data !== "object") return;
29
+ const { epoch: next, version } = data as { epoch?: unknown; version?: unknown };
30
+ if (version !== MODULE_EPOCH_MESSAGE_VERSION) return;
31
+ if (typeof next !== "number" || !Number.isSafeInteger(next) || next < 0) return;
32
+ // Forward only: a late or repeated message never rolls the stage back to
33
+ // modules it has already replaced.
34
+ if (epoch !== null && next <= epoch) return;
35
+ epoch = next;
36
+ for (const listener of listeners) listener();
37
+ }
38
+
39
+ // Opened when the stage loads, before it can ask for a document, so an epoch
40
+ // posted ahead of the first load is not missed.
41
+ if (typeof BroadcastChannel === "function") {
42
+ const channel = new BroadcastChannel(MODULE_EPOCH_CHANNEL);
43
+ channel.onmessage = (event: MessageEvent<unknown>) => receive(event.data);
44
+ // Node (the test runner) would otherwise keep its process alive for an open
45
+ // channel. Browsers have no unref.
46
+ (channel as { unref?: () => void }).unref?.();
47
+ }
48
+
49
+ export function getModuleEpoch(): ModuleEpoch {
50
+ return epoch;
51
+ }
52
+
53
+ export function subscribeModuleEpoch(listener: () => void): () => void {
54
+ listeners.add(listener);
55
+ return () => listeners.delete(listener);
56
+ }
57
+
58
+ export function useModuleEpoch(): ModuleEpoch {
59
+ return useSyncExternalStore(subscribeModuleEpoch, getModuleEpoch);
60
+ }