@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
package/src/stage/view.ts CHANGED
@@ -144,16 +144,63 @@ export function designToScreen(
144
144
  /** Space left around the frame when fitting, as a fraction of the viewport. */
145
145
  export const FIT_MARGIN = 0.08;
146
146
 
147
- /** The view that shows the whole frame with a margin. Pure in the viewport and
148
- * the frame, so the chrome's "fit" button and a window resize agree. */
149
- export function fitView(viewport: Viewport, frame: FrameSize): ShotView {
147
+ /**
148
+ * The view that shows the whole frame with a margin. Pure in the viewport and
149
+ * the frame, so the chrome's "fit" button and a window resize agree.
150
+ *
151
+ * ── WHY IT CAN FIT A SUB-RECT ────────────────────────────────────────────
152
+ *
153
+ * The stage renders full-bleed and every chrome panel is docked over it, so the
154
+ * viewport is the whole window and fitting to it framed the shot behind the
155
+ * header and the timeline dock — the author pressed Fit and got a frame with
156
+ * its top and bottom covered. `within` is the part of that viewport the chrome
157
+ * is NOT covering, in the SAME coordinates as `viewport` (pane-local: the
158
+ * caller hands this a pane's size, so it hands `within` a pane-local rect).
159
+ * The render still fills the viewport; only what the frame is sized and
160
+ * centred against moves.
161
+ *
162
+ * Defaulting `within` to the whole viewport keeps `panX/panY` at 0 for every
163
+ * caller that does not pass one, so nothing that fits an unobstructed viewport
164
+ * changed.
165
+ */
166
+ export function fitView(
167
+ viewport: Viewport,
168
+ frame: FrameSize,
169
+ within: { x: number; y: number; width: number; height: number } = {
170
+ x: 0,
171
+ y: 0,
172
+ width: viewport.width,
173
+ height: viewport.height,
174
+ },
175
+ /** Per side, as a fraction. A preview hole IS the frame, so it passes 0. */
176
+ margin: number = FIT_MARGIN,
177
+ ): ShotView {
150
178
  if (viewport.width <= 0 || viewport.height <= 0) return { zoom: 1, panX: 0, panY: 0 };
151
- const usable = 1 - FIT_MARGIN * 2;
152
- const zoom = Math.min(
153
- (viewport.width * usable) / frame.width,
154
- (viewport.height * usable) / frame.height,
155
- );
156
- return { zoom: clampZoom(zoom), panX: 0, panY: 0 };
179
+ const usable = 1 - margin * 2;
180
+ const zoom = Math.min((within.width * usable) / frame.width, (within.height * usable) / frame.height);
181
+ // `panX/panY` are the frame CENTRE's displacement from the VIEWPORT centre
182
+ // (see `ShotView`, and `frameRect` which derives the screen rect from it), so
183
+ // centring in `within` is the difference between the two centres — not
184
+ // `within`'s origin, which is the sign error this shape invites.
185
+ return {
186
+ zoom: clampZoom(zoom),
187
+ panX: finitePan(within.x + within.width / 2 - viewport.width / 2),
188
+ panY: finitePan(within.y + within.height / 2 - viewport.height / 2),
189
+ };
190
+ }
191
+
192
+ /**
193
+ * 0 for a pan that is not a number, for the same reason `clampZoom` answers 1.
194
+ *
195
+ * The pan used to be the constant 0, so `fitView` was TOTAL: no input could
196
+ * make it return a view the camera could not use. Two centres subtracted is
197
+ * not — a non-finite viewport or sub-rect (a first-frame measure, a mid-resize
198
+ * `Infinity`) makes it NaN, and a NaN pan is a `setViewOffset` of NaN, which is
199
+ * a projection matrix of NaN: the pane draws nothing at all, with no error
200
+ * anywhere. `paneRects` guards the same class of input for the same reason.
201
+ */
202
+ function finitePan(n: number): number {
203
+ return Number.isFinite(n) ? n : 0;
157
204
  }
158
205
 
159
206
  /**
@@ -1,4 +1,4 @@
1
- // The working pane's free-orbit camera, as pure spherical arithmetic.
1
+ // The working pane's free-orbit camera, as pure spherical arithmetic.
2
2
  //
3
3
  // ── COORDINATE CONVENTION, STATED ONCE ──────────────────────────────────
4
4
  //
@@ -32,7 +32,7 @@
32
32
  //
33
33
  // THREE-FREE: plain arrays and numbers, so the chrome can render a readout of
34
34
  // the view and these functions are testable in node.
35
- import { DEFAULT_FOV_DEG, planeDistance } from "@dailies-studio/shot/doc";
35
+ import { DEFAULT_FOV_DEG, MAX_CAMERA_PITCH_DEG, planeDistance } from "@dailies-studio/shot/doc";
36
36
  import { FIT_MARGIN } from "./view.js";
37
37
  import type { FrameSize } from "./view.js";
38
38
 
@@ -87,6 +87,26 @@ export interface WorkingView {
87
87
  ortho?: boolean;
88
88
  }
89
89
 
90
+ /**
91
+ * How far behind the orbit position the ORTHOGRAPHIC eye stands.
92
+ *
93
+ * An orthographic projection does not depend on where along the view axis the
94
+ * eye sits, so this is free to the picture — and `StageTools` needs it because
95
+ * `Raycaster.setFromCamera` starts an ortho ray AT the near plane, so anything
96
+ * behind that plane draws perfectly and answers no click. Standing the eye back
97
+ * puts the whole slab in front of a positive near plane.
98
+ *
99
+ * NAMED, rather than inlined at the one call site, because it is the reason
100
+ * `GridPlane` cannot measure its fade from `cameraPosition` in ortho: this is
101
+ * many times `gridFade`'s far end at every legal dolly, so measured from here
102
+ * the entire floor resolves to the backdrop colour and disappears. Two places
103
+ * reasoning about the same number by restating it is how that shipped;
104
+ * `gizmoMath.test.ts` now pins the relationship against this export.
105
+ */
106
+ export function orthoEyePushBack(distance: number): number {
107
+ return Math.max(distance * 8, 10_000);
108
+ }
109
+
90
110
  export const ORBIT_DEG_PER_PX = 0.3;
91
111
 
92
112
  /**
@@ -100,12 +120,32 @@ export const ORBIT_DEG_PER_PX = 0.3;
100
120
  export const ORBIT_STEP_DEG = 15;
101
121
 
102
122
 
103
- /** Off the poles, both ends. At phi 0 or 180 the eye is straight above or below
104
- * the target, `lookAt`'s up vector is parallel to the view direction, and the
105
- * camera's roll is undefined — which shows up as the view snapping to an
106
- * arbitrary rotation exactly at the top of a drag. */
107
- export const MIN_PHI_DEG = 5;
108
- export const MAX_PHI_DEG = 175;
123
+ /**
124
+ * The poles are REACHABLE, and Numpad7 really does look straight down.
125
+ *
126
+ * ── WHAT USED TO BREAK, AND WHERE ───────────────────────────────────────
127
+ *
128
+ * The clamp was [5, 175], so "top" and "bottom" landed five degrees short and
129
+ * an author could never get a true plan or elevation view — the one thing a
130
+ * numpad axis key is for. The reason given was that at phi 0 or 180 the
131
+ * camera's roll is undefined, and that is true of `lookAt` WITH A WORLD-UP
132
+ * VECTOR: up becomes parallel to the view direction, the cross product is zero,
133
+ * and the quaternion comes out NaN.
134
+ *
135
+ * But `viewBasis` never had that problem. Its closed form for up is
136
+ * `[-cosφ·sinθ, sinφ, -cosφ·cosθ]`, which at φ = 0 is `[-sinθ, 0, -cosθ]` — a
137
+ * unit vector, perpendicular to the view direction, and continuous through the
138
+ * pole. The turntable convention: at the pole the roll is decided by theta,
139
+ * which is exactly what an author spinning a top view expects.
140
+ *
141
+ * So the fix was never a bigger epsilon. `StageTools` sets `cam.up` from
142
+ * `viewBasis(view).up` instead of from world +y, and the clamp opens to the
143
+ * full range. For every interior phi the two are the same vector (`viewBasis`'s
144
+ * own header states that identity, and gizmoMath.test.ts pins it against a real
145
+ * `Matrix4.lookAt`), so nothing between the poles moved by a float.
146
+ */
147
+ export const MIN_PHI_DEG = 0;
148
+ export const MAX_PHI_DEG = 180;
109
149
 
110
150
  /** A degenerate distance is not a far-away camera, it is a camera AT the target:
111
151
  * `lookAt` gets a zero-length direction and writes a NaN quaternion, and every
@@ -224,11 +264,50 @@ export function workingPose(view: WorkingView): { position: [number, number, num
224
264
  * `panBy` states, for the same reason.
225
265
  */
226
266
  export function orbitBy(view: WorkingView, dxPx: number, dyPx: number): WorkingView {
227
- return {
267
+ const next: WorkingView = {
228
268
  ...view,
229
269
  theta: wrapDeg(view.theta - dxPx * ORBIT_DEG_PER_PX),
230
270
  phi: clampPhi(clampPhi(view.phi) - dyPx * ORBIT_DEG_PER_PX),
231
271
  };
272
+ // ── AUTO PERSPECTIVE, ON THE WAY OUT OF AN AXIS VIEW ──────────────────
273
+ //
274
+ // `presetWorkingView` puts the pane in ORTHOGRAPHIC, because that is what a
275
+ // front or a top view is FOR: a plan and an elevation with no vanishing point
276
+ // to read a size wrong from. Turning away from one and staying flat is the
277
+ // other half of that — an orthographic three-quarter view of a scene with
278
+ // depth in it reads as broken, which is why Blender enables Auto Perspective
279
+ // by default and why leaving it out would make the preset key a trap.
280
+ //
281
+ // STATELESS, on the pose rather than on a remembered flag: "was this view
282
+ // reached by a preset" is a question that needs one more field on a record
283
+ // three lanes copy, and "is this view ON an axis" answers the same question
284
+ // from what is already there. Manual orthographic (Numpad5) at a
285
+ // three-quarter angle is untouched, which is the case the flag would have
286
+ // been for.
287
+ if (view.ortho === true && next.ortho === true && isAxisView(view) && !isAxisView(next)) {
288
+ const { ortho: _drop, ...perspective } = next;
289
+ return perspective;
290
+ }
291
+ return next;
292
+ }
293
+
294
+ /** Within this many degrees of an axis, a pose IS that axis view. Half of one
295
+ * numpad step, so a single Numpad4 press always leaves the neighbourhood. */
296
+ const AXIS_VIEW_EPSILON_DEG = 1e-6;
297
+
298
+ /**
299
+ * Is the eye on one of the six axis views?
300
+ *
301
+ * The poles (phi 0 / 180) are axis views whatever theta is — straight down is
302
+ * straight down. Otherwise it is the equator with theta on a quarter turn.
303
+ */
304
+ export function isAxisView(view: WorkingView): boolean {
305
+ const phi = clampPhi(view.phi);
306
+ const near = (a: number, b: number): boolean => Math.abs(a - b) <= AXIS_VIEW_EPSILON_DEG;
307
+ if (near(phi, MIN_PHI_DEG) || near(phi, MAX_PHI_DEG)) return true;
308
+ if (!near(phi, 90)) return false;
309
+ const theta = Math.abs(wrapDeg(view.theta));
310
+ return near(theta, 0) || near(theta, 90) || near(theta, 180);
232
311
  }
233
312
 
234
313
  /**
@@ -367,6 +446,101 @@ export function dollyBy(view: WorkingView, wheelDeltaY: number, bounds?: DollyBo
367
446
  return { ...view, distance };
368
447
  }
369
448
 
449
+ /**
450
+ * Point the orbit at a new place WITHOUT moving the eye.
451
+ *
452
+ * ── ORBIT AROUND THE SELECTION ──────────────────────────────────────────
453
+ *
454
+ * `orbitBy` always pivots on `view.target`, and nothing ever fed the selection
455
+ * into it — so after a few pans the pivot was wherever the target had drifted
456
+ * to, and orbiting swung the thing the author was working on straight off the
457
+ * pane. Blender's "Orbit Around Selection" fixes it by re-pointing the pivot,
458
+ * and the whole trick is that the EYE STAYS PUT: `workingPose` run backwards,
459
+ * so the picture at the moment of re-targeting is identical to the pixel and
460
+ * only the next turn behaves differently. A re-target that moved the eye would
461
+ * be a jump cut on the way into a gesture.
462
+ *
463
+ * `workingPose` puts the eye at `target + r(sinφ sinθ, cosφ, sinφ cosθ)`, so
464
+ * inverting is one length and two inverse trig calls — the same convention
465
+ * `workingViewForIsolation` inverts, stated once more rather than shared
466
+ * because that one also has an ortho flag and a fit to answer for.
467
+ *
468
+ * Refuses (returns the view unchanged) when the new target is where the eye
469
+ * already is: there is no direction to derive angles from, and `lookAt` on a
470
+ * zero-length direction writes a NaN quaternion.
471
+ */
472
+ export function retargetKeepingEye(
473
+ view: WorkingView,
474
+ target: readonly [number, number, number],
475
+ ): WorkingView {
476
+ if (target.some((n) => !Number.isFinite(n))) return view;
477
+ const eye = workingPose(view).position;
478
+ const dx = eye[0] - target[0];
479
+ const dy = eye[1] - target[1];
480
+ const dz = eye[2] - target[2];
481
+ const distance = Math.hypot(dx, dy, dz);
482
+ if (!(distance > MIN_DISTANCE)) return view;
483
+ return {
484
+ ...view,
485
+ // acos of the y component over the radius, which is phi by construction.
486
+ // Clamped for the float that lands a hair outside [-1, 1].
487
+ phi: (Math.acos(Math.max(-1, Math.min(1, dy / distance))) * 180) / Math.PI,
488
+ theta: wrapDeg((Math.atan2(dx, dz) * 180) / Math.PI),
489
+ distance: clampDistance(distance),
490
+ target: [target[0], target[1], target[2]],
491
+ };
492
+ }
493
+
494
+ /**
495
+ * Dolly TOWARDS A POINT: the world point under the cursor holds still while
496
+ * everything else moves in or out around it.
497
+ *
498
+ * ── THE GAP THIS CLOSES ─────────────────────────────────────────────────
499
+ *
500
+ * The camera pane has anchored its wheel at the cursor since it shipped
501
+ * (`wheelToView`), and the working pane did not: a plain `dollyBy` only
502
+ * shortens the distance to the TARGET, so wheeling toward a layer off to one
503
+ * side approached the middle of the pane instead and the author had to pan
504
+ * after every notch. Blender calls it "Zoom to Mouse Position" and everyone
505
+ * turns it on.
506
+ *
507
+ * ── WHY THE RATIO IS TAKEN AFTER THE CLAMP ──────────────────────────────
508
+ *
509
+ * The whole rig scales UNIFORMLY about `anchor`, which makes that point a fixed
510
+ * point of the map — and a fixed point is what lets the wheel coalescer's
511
+ * summed deltas compose. Re-raycasting a stationary cursor after each flush
512
+ * returns the same anchor, so clamping per-step and clamping the composed step
513
+ * land on the same view, exactly as `dollyBy`'s own note requires.
514
+ *
515
+ * That only holds if the scale factor is the one the DISTANCE actually took.
516
+ * Deriving it from the raw `exp(dy / 320)` instead would keep sliding the
517
+ * target after the distance had hit a wall — the picture would go on drifting
518
+ * while the dolly had stopped, which is the one thing a wall must not do.
519
+ *
520
+ * `dollyBy` itself is untouched: this is a second function, so every caller
521
+ * that wants the plain dolly (the zoom buttons, the numpad, the fallback when
522
+ * the cursor's ray meets nothing) keeps it byte-identical.
523
+ */
524
+ export function dollyTowards(
525
+ view: WorkingView,
526
+ anchor: readonly [number, number, number],
527
+ wheelDeltaY: number,
528
+ bounds?: DollyBounds,
529
+ ): WorkingView {
530
+ const next = dollyBy(view, wheelDeltaY, bounds);
531
+ const was = clampDistance(view.distance);
532
+ const k = was > 0 ? next.distance / was : 1;
533
+ if (!Number.isFinite(k) || anchor.some((n) => !Number.isFinite(n))) return next;
534
+ return {
535
+ ...next,
536
+ target: [
537
+ anchor[0] + (view.target[0] - anchor[0]) * k,
538
+ anchor[1] + (view.target[1] - anchor[1]) * k,
539
+ anchor[2] + (view.target[2] - anchor[2]) * k,
540
+ ],
541
+ };
542
+ }
543
+
370
544
  /**
371
545
  * The view an author gets before they touch anything: an off-axis three-quarter
372
546
  * look with the WHOLE canvas in shot.
@@ -461,25 +635,38 @@ export type WorkingViewPreset = "front" | "back" | "right" | "left" | "top" | "b
461
635
  * to lean on mid-layout. Front is theta 0 (the shot camera's own axis, per
462
636
  * this file's convention) and back wraps to -180 via `wrapDeg`.
463
637
  *
464
- * TOP AND BOTTOM LAND ON THE POLE CLAMPS, not on 0/180: straight down the
465
- * pole the camera's roll is undefined (see MIN_PHI_DEG), so "top" is the
466
- * closest pose that still has a stable up — 5 degrees off vertical, keeping
467
- * the author's theta so the horizon stays where they left it.
638
+ * TOP AND BOTTOM ARE THE POLES. They used to land five degrees short, because
639
+ * `lookAt` with world-up writes a NaN quaternion straight down the pole — so
640
+ * "top" was a nearly-plan view and a true plan view was unreachable, which is
641
+ * the one thing a numpad axis key exists for. `StageTools` now takes `cam.up`
642
+ * from `viewBasis`, whose closed form is well defined AT the pole (see
643
+ * MIN_PHI_DEG), and the clamp opens to the full range.
644
+ *
645
+ * The author's THETA is kept, which is what decides the roll up there — spin
646
+ * the view before pressing Numpad7 and the plan view arrives rotated the same
647
+ * way, which is the turntable behaviour Blender has.
468
648
  */
469
649
  export function presetWorkingView(view: WorkingView, preset: WorkingViewPreset): WorkingView {
650
+ // ── AND IT GOES ORTHOGRAPHIC ────────────────────────────────────────────
651
+ //
652
+ // Which is what a front or a top view is FOR: a plan and an elevation, with
653
+ // no vanishing point to read a size wrong from. Blender's Auto Perspective,
654
+ // on by default there and now here; `orbitBy` is the other half, dropping
655
+ // back to perspective on the way out of an axis view.
656
+ const flat = { ...view, ortho: true } as const;
470
657
  switch (preset) {
471
658
  case "front":
472
- return { ...view, theta: 0, phi: 90 };
659
+ return { ...flat, theta: 0, phi: 90 };
473
660
  case "back":
474
- return { ...view, theta: wrapDeg(180), phi: 90 };
661
+ return { ...flat, theta: wrapDeg(180), phi: 90 };
475
662
  case "right":
476
- return { ...view, theta: 90, phi: 90 };
663
+ return { ...flat, theta: 90, phi: 90 };
477
664
  case "left":
478
- return { ...view, theta: -90, phi: 90 };
665
+ return { ...flat, theta: -90, phi: 90 };
479
666
  case "top":
480
- return { ...view, phi: MIN_PHI_DEG };
667
+ return { ...flat, phi: MIN_PHI_DEG };
481
668
  case "bottom":
482
- return { ...view, phi: MAX_PHI_DEG };
669
+ return { ...flat, phi: MAX_PHI_DEG };
483
670
  }
484
671
  }
485
672
 
@@ -498,15 +685,39 @@ export const INITIAL_WORKING_VIEW: WorkingView = defaultWorkingView(
498
685
  WORKING_FOV_DEG,
499
686
  );
500
687
 
688
+ /**
689
+ * Frame a WORLD of a given extent around a point, keeping the caller's angles:
690
+ * `defaultWorkingView`'s own derivation with the extent substituted for the
691
+ * canvas diagonal. Home inside a component-driven camera fits the component's
692
+ * world with this rather than an empty canvas thousands of units away.
693
+ */
694
+ export function workingViewForExtent(
695
+ view: WorkingView,
696
+ centre: readonly [number, number, number],
697
+ extent: number,
698
+ fovDeg: number = WORKING_FOV_DEG,
699
+ ): WorkingView {
700
+ const span = Number.isFinite(extent) && extent > 0 ? extent : 1;
701
+ return {
702
+ theta: view.theta,
703
+ phi: view.phi,
704
+ distance: clampDistance(planeDistance(span, fovDeg, 1) / (1 - FIT_MARGIN * 2)),
705
+ target: [centre[0], centre[1], centre[2]],
706
+ ...(view.ortho === undefined ? {} : { ortho: view.ortho }),
707
+ };
708
+ }
709
+
501
710
  /**
502
711
  * The working view, projected onto the FRAMING model — "align the camera to
503
712
  * this view", and the half of the round trip that can fail.
504
713
  *
505
714
  * ── WHY THIS CAN REFUSE, AND WHY REFUSING IS THE FEATURE ────────────────
506
715
  *
507
- * A `Framing` is four numbers (x, y, zoom, roll) describing a camera that looks
508
- * straight down -z from the +z side. A free orbit is five, and most of them
509
- * describe a pose no framing can name. Writing "the closest framing" for a pose
716
+ * A `Framing` is five numbers (x, y, zoom, rollDeg, focusZ), of which only four
717
+ * are POSE — `focusZ` picks the focus plane, not where the camera stands — and
718
+ * all four describe a camera that looks straight down -z from the +z side. A
719
+ * free orbit has five pose degrees of freedom, and most of them describe a pose
720
+ * no framing can name. Writing "the closest framing" for a pose
510
721
  * the author is looking at from behind the canvas would hand them a camera
511
722
  * pointing at something else and call it aligned; naming the reason costs one
512
723
  * message and is the only answer they can act on.
@@ -530,7 +741,7 @@ export const INITIAL_WORKING_VIEW: WorkingView = defaultWorkingView(
530
741
  * the framing already had.
531
742
  */
532
743
  export type ViewFramingResult =
533
- | { ok: true; x: number; y: number; zoom: number }
744
+ | { ok: true; x: number; y: number; zoom: number; yawDeg: number; pitchDeg: number }
534
745
  | { ok: false; refusal: string };
535
746
 
536
747
  /**
@@ -564,7 +775,7 @@ export function framingFromWorkingView(
564
775
  return {
565
776
  ok: false,
566
777
  refusal:
567
- "The working camera is not looking anywhere a framing could describe — press Home to " +
778
+ "The working camera is not looking anywhere a framing could describe. Press Home to " +
568
779
  "put it back, then align.",
569
780
  };
570
781
  }
@@ -574,7 +785,7 @@ export function framingFromWorkingView(
574
785
  ok: false,
575
786
  refusal:
576
787
  "The working camera is behind the canvas, and the shot camera always looks at it from the " +
577
- "front — orbit back round to the front of the layers, then align.",
788
+ "front. Orbit back round to the front of the layers, then align.",
578
789
  };
579
790
  }
580
791
  // Towards the plane means -z: the eye is in front of it, so the axis has to
@@ -585,7 +796,7 @@ export function framingFromWorkingView(
585
796
  ok: false,
586
797
  refusal:
587
798
  "The working camera is looking along the canvas rather than at it, so there is no " +
588
- "rectangle for the shot camera to frame — orbit until the canvas faces you, then align.",
799
+ "rectangle for the shot camera to frame. Orbit until the canvas faces you, then align.",
589
800
  };
590
801
  }
591
802
 
@@ -607,10 +818,21 @@ export function framingFromWorkingView(
607
818
  const span = view.ortho === true ? clampDistance(view.distance) : t;
608
819
  const zoom = frame.height / (2 * span * Math.tan((fovDeg * DEG) / 2));
609
820
  if (!Number.isFinite(zoom) || zoom <= 0) {
610
- return { ok: false, refusal: "The working camera is too close to the canvas to express as a framing — dolly back, then align." };
821
+ return { ok: false, refusal: "The working camera is too close to the canvas to express as a framing. Dolly back, then align." };
611
822
  }
612
- // The one mapping, undone: world y is design -y.
613
- return { ok: true, x: hit[0], y: -hit[1], zoom };
823
+ // The one mapping, undone: world y is design -y. And THE ANGLES (schema
824
+ // v26): the shot camera stands on the same sphere the working view orbits,
825
+ // so its yaw is the view's azimuth and its pitch the view's elevation. The
826
+ // orbit pivot moves from the view's target to the hit on the plane, but the
827
+ // eye and the direction are the same, so the picture is.
828
+ return {
829
+ ok: true,
830
+ x: hit[0],
831
+ y: -hit[1],
832
+ zoom,
833
+ yawDeg: wrapDeg(view.theta),
834
+ pitchDeg: Math.max(-MAX_CAMERA_PITCH_DEG, Math.min(MAX_CAMERA_PITCH_DEG, 90 - clampPhi(view.phi))),
835
+ };
614
836
  }
615
837
 
616
838
  /**
@@ -622,15 +844,37 @@ export function framingFromWorkingView(
622
844
  * claim, and so the fitter has one place to derive a distance from a lens.
623
845
  */
624
846
  export function workingViewForFraming(
625
- framing: { x: number; y: number; zoom: number },
847
+ framing: { x: number; y: number; zoom: number; yawDeg?: number; pitchDeg?: number },
626
848
  frame: FrameSize,
627
849
  fovDeg: number = WORKING_FOV_DEG,
850
+ /**
851
+ * Which extent has to fit inside the pane.
852
+ *
853
+ * ── "height" IS THE ROUND TRIP; "diagonal" IS THE FIT ───────────────────
854
+ *
855
+ * `framingFromWorkingView` derives a zoom from the framed HEIGHT, so the
856
+ * inverse has to use the height too or "align, then look at what you aligned
857
+ * to" stops being an identity. That is what this function is for and it
858
+ * stays the default, byte-identical.
859
+ *
860
+ * But FRAME SELECTED keeps the author's angles, and a rectangle seen from an
861
+ * angle is turned: the extent that has to fit is its DIAGONAL. Framing the
862
+ * height instead under-fits at every oblique view, which is the ponytail
863
+ * ceiling `fitWorkingViewToSelection` used to carry — the box lands correct
864
+ * head-on and creeps off the pane the further round the author has orbited.
865
+ * The diagonal is rotation-invariant, so no angle can push a corner out; it
866
+ * is `defaultWorkingView`'s own derivation with the framed rectangle
867
+ * substituted for the canvas.
868
+ */
869
+ fit: "height" | "diagonal" = "height",
628
870
  ): WorkingView {
629
871
  const zoom = framing.zoom > 0 ? framing.zoom : 1;
872
+ const extent = fit === "diagonal" ? Math.hypot(frame.width, frame.height) : frame.height;
873
+ // The framing's own orbit angles (v26): the inverse of `framingFromWorkingView`.
630
874
  return {
631
- theta: 0,
632
- phi: 90,
633
- distance: clampDistance(frame.height / (2 * zoom * Math.tan((fovDeg * DEG) / 2))),
875
+ theta: wrapDeg(framing.yawDeg ?? 0),
876
+ phi: clampPhi(90 - (framing.pitchDeg ?? 0)),
877
+ distance: clampDistance(extent / (2 * zoom * Math.tan((fovDeg * DEG) / 2))),
634
878
  target: [framing.x, -framing.y, 0],
635
879
  };
636
880
  }