@dailies-studio/shot-editor 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/dist/.vite/manifest.json +382 -0
  2. package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
  3. package/dist/assets/AgentPanel-DJEphah0.css +1 -0
  4. package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
  5. package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
  6. package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
  7. package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
  8. package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
  9. package/dist/assets/CurvePane-BjFVabKX.css +1 -0
  10. package/dist/assets/CurvePane-R22WzllJ.js +1 -0
  11. package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
  12. package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
  13. package/dist/assets/FontPicker-v0IOIArM.css +1 -0
  14. package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
  15. package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
  16. package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
  17. package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
  18. package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
  19. package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
  20. package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
  21. package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
  22. package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
  23. package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
  24. package/dist/assets/SoundMode-BmcZru3i.js +3 -0
  25. package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
  26. package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
  27. package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
  28. package/dist/assets/board-B88EPuLO.css +1 -0
  29. package/dist/assets/board-CL-he7SL.js +3 -0
  30. package/dist/assets/components-0oOSPN0l.js +1 -0
  31. package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
  32. package/dist/assets/googleFonts-BNon6M7X.js +1 -0
  33. package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
  34. package/dist/assets/index-4F5NQmI8.css +1 -0
  35. package/dist/assets/index-BGHQGV5z.js +117 -0
  36. package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  37. package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  38. package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  39. package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  40. package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  41. package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  42. package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  43. package/dist/assets/libraryModel-CpIwfFha.js +1 -0
  44. package/dist/assets/lucide-6iK64tTB.js +9319 -0
  45. package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
  46. package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
  47. package/dist/assets/webmcp-BsB9gxWC.js +2 -0
  48. package/dist/index.html +34 -3
  49. package/package.json +12 -9
  50. package/src/addFamilies.ts +66 -0
  51. package/src/api.ts +1631 -55
  52. package/src/app/App.tsx +977 -198
  53. package/src/app/lazyPanel.tsx +137 -0
  54. package/src/app/lazySurfaces.tsx +203 -0
  55. package/src/audio/index.tsx +41 -10
  56. package/src/audio/meter.ts +485 -0
  57. package/src/audio/meterMath.ts +273 -0
  58. package/src/audio/meterStore.ts +155 -0
  59. package/src/audio/offlineMix.ts +289 -0
  60. package/src/audio/preview.ts +928 -118
  61. package/src/bridge.ts +15 -5
  62. package/src/bridgeTypes.ts +1611 -36
  63. package/src/channels.ts +17 -32
  64. package/src/fonts/googleFonts.ts +4 -0
  65. package/src/hostApi.ts +174 -0
  66. package/src/icons.tsx +770 -0
  67. package/src/layerWords.ts +61 -0
  68. package/src/main.tsx +56 -4
  69. package/src/panels/ResizeHandle.tsx +33 -43
  70. package/src/panels/agent/AgentDots.tsx +36 -0
  71. package/src/panels/agent/AgentPanel.tsx +119 -43
  72. package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
  73. package/src/panels/agent/agent.css +113 -97
  74. package/src/panels/agent/agentGate.ts +35 -0
  75. package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
  76. package/src/panels/code/CodeStudio.tsx +373 -0
  77. package/src/panels/code/FileTree.tsx +265 -0
  78. package/src/panels/code/codeBufferGuard.ts +52 -0
  79. package/src/panels/code/codeParts.tsx +321 -0
  80. package/src/panels/comments/CommentComposer.tsx +239 -0
  81. package/src/panels/comments/CommentThread.tsx +346 -0
  82. package/src/panels/comments/CommentsPanel.tsx +336 -0
  83. package/src/panels/comments/InboxButton.tsx +131 -0
  84. package/src/panels/comments/MentionMenu.tsx +98 -0
  85. package/src/panels/comments/parts.tsx +171 -0
  86. package/src/panels/export/ExportDialog.tsx +919 -96
  87. package/src/panels/export/ExportScrim.tsx +57 -0
  88. package/src/panels/files/AssetsTab.tsx +656 -0
  89. package/src/panels/files/FilePanel.tsx +1466 -216
  90. package/src/panels/files/LayerMarks.tsx +40 -0
  91. package/src/panels/files/LeftColumn.tsx +37 -0
  92. package/src/panels/files/McpSection.tsx +690 -47
  93. package/src/panels/files/MissingAssetsNotice.tsx +60 -0
  94. package/src/panels/files/assetDrag.ts +79 -0
  95. package/src/panels/files/resourceParts.tsx +255 -0
  96. package/src/panels/fonts/FontPicker.tsx +762 -0
  97. package/src/panels/fonts/faceCache.ts +207 -0
  98. package/src/panels/fonts/fontPicker.css +271 -0
  99. package/src/panels/fonts/pickerModel.ts +265 -0
  100. package/src/panels/fonts/pickerRequest.ts +46 -0
  101. package/src/panels/header/AccountAvatar.tsx +76 -0
  102. package/src/panels/header/AgentChip.tsx +77 -0
  103. package/src/panels/header/AppMenu.tsx +213 -0
  104. package/src/panels/header/Header.tsx +564 -84
  105. package/src/panels/header/HistoryControls.tsx +31 -5
  106. package/src/panels/header/PresenceStack.tsx +329 -0
  107. package/src/panels/header/ProjectTitle.tsx +232 -0
  108. package/src/panels/header/ShareButton.tsx +59 -0
  109. package/src/panels/header/StatusBadges.tsx +358 -27
  110. package/src/panels/header/SyncDeletedDialog.tsx +88 -0
  111. package/src/panels/header/ZoomControls.tsx +46 -15
  112. package/src/panels/header/agentHandoff.ts +52 -0
  113. package/src/panels/header/leave.ts +26 -0
  114. package/src/panels/help/HelpSection.tsx +189 -163
  115. package/src/panels/insert/InsertDrawer.tsx +723 -0
  116. package/src/panels/insert/iconSet.ts +109 -0
  117. package/src/panels/inspector/ExposeDot.tsx +117 -0
  118. package/src/panels/inspector/FramingRows.tsx +314 -44
  119. package/src/panels/inspector/InspectorFooter.tsx +91 -0
  120. package/src/panels/inspector/InspectorHead.tsx +425 -0
  121. package/src/panels/inspector/InspectorPanel.tsx +408 -124
  122. package/src/panels/inspector/KeyframeDot.tsx +373 -0
  123. package/src/panels/inspector/KeyframeSection.tsx +257 -0
  124. package/src/panels/inspector/MarkerSection.tsx +55 -11
  125. package/src/panels/inspector/ParamRow.tsx +84 -53
  126. package/src/panels/inspector/RightColumn.tsx +152 -0
  127. package/src/panels/inspector/TimeSection.tsx +866 -0
  128. package/src/panels/inspector/animate/EaseControl.tsx +244 -0
  129. package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
  130. package/src/panels/inspector/animate/ShotSection.tsx +562 -48
  131. package/src/panels/inspector/design/AlignRow.tsx +95 -31
  132. package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
  133. package/src/panels/inspector/design/ClonerSection.tsx +323 -62
  134. package/src/panels/inspector/design/ComponentSection.tsx +50 -43
  135. package/src/panels/inspector/design/CornerSection.tsx +45 -14
  136. package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
  137. package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
  138. package/src/panels/inspector/design/DeviceSection.tsx +210 -0
  139. package/src/panels/inspector/design/FillSection.tsx +83 -51
  140. package/src/panels/inspector/design/FontField.tsx +196 -0
  141. package/src/panels/inspector/design/HtmlSection.tsx +23 -12
  142. package/src/panels/inspector/design/InstanceSection.tsx +373 -154
  143. package/src/panels/inspector/design/MaskSection.tsx +84 -0
  144. package/src/panels/inspector/design/MaterialSection.tsx +177 -0
  145. package/src/panels/inspector/design/MediaSection.tsx +273 -120
  146. package/src/panels/inspector/design/MeshSection.tsx +910 -82
  147. package/src/panels/inspector/design/PaintRows.tsx +550 -0
  148. package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
  149. package/src/panels/inspector/design/ShadowSection.tsx +221 -48
  150. package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
  151. package/src/panels/inspector/design/StrokeSection.tsx +228 -43
  152. package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
  153. package/src/panels/inspector/design/TransformSection.tsx +244 -50
  154. package/src/panels/inspector/design/TypographySection.tsx +679 -110
  155. package/src/panels/inspector/layout.ts +295 -0
  156. package/src/panels/inspector/propValue.ts +199 -0
  157. package/src/panels/inspector/scene/CameraSection.tsx +232 -22
  158. package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
  159. package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
  160. package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
  161. package/src/panels/inspector/scene/SceneSection.tsx +219 -44
  162. package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
  163. package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
  164. package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
  165. package/src/panels/inspector/timeModel.ts +190 -0
  166. package/src/panels/inspector/useDocWrite.ts +438 -146
  167. package/src/panels/library/LibraryBrowser.tsx +435 -0
  168. package/src/panels/library/LibraryGrid.tsx +190 -0
  169. package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
  170. package/src/panels/library/LibraryRail.tsx +84 -0
  171. package/src/panels/library/LibrarySoundPanel.tsx +293 -0
  172. package/src/panels/library/SoundHistory.tsx +245 -0
  173. package/src/panels/library/StockSounds.tsx +390 -0
  174. package/src/panels/library/libraryModel.ts +147 -0
  175. package/src/panels/library/stockSearch.ts +77 -0
  176. package/src/panels/maskMarks.tsx +117 -0
  177. package/src/panels/material/MaterialCommentNode.tsx +54 -0
  178. package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
  179. package/src/panels/material/MaterialGraphMenu.tsx +45 -0
  180. package/src/panels/material/MaterialGroupNode.tsx +56 -0
  181. package/src/panels/material/MaterialLibrary.tsx +253 -0
  182. package/src/panels/material/MaterialNode.tsx +453 -0
  183. package/src/panels/material/MaterialNodePalette.tsx +349 -0
  184. package/src/panels/material/MaterialParameterControls.tsx +525 -0
  185. package/src/panels/material/MaterialRerouteNode.tsx +42 -0
  186. package/src/panels/material/MaterialWorkspace.tsx +501 -0
  187. package/src/panels/material/NodePreview.tsx +70 -0
  188. package/src/panels/material/flowModel.ts +441 -0
  189. package/src/panels/material/parameterValues.ts +231 -0
  190. package/src/panels/material/previewScheduler.ts +235 -0
  191. package/src/panels/material/reactFlow.css +6 -0
  192. package/src/panels/material/socketGlyph.ts +85 -0
  193. package/src/panels/material/viewportMemory.ts +58 -0
  194. package/src/panels/preferences/PreferencesDialog.tsx +215 -0
  195. package/src/panels/share/ShareComponentDialog.tsx +247 -0
  196. package/src/panels/stage/StageMenu.tsx +35 -5
  197. package/src/panels/stage/StagePanel.tsx +338 -23
  198. package/src/panels/stage/cellSlots.tsx +70 -0
  199. package/src/panels/storyboard/BoardCanvas.tsx +739 -0
  200. package/src/panels/storyboard/BoardCursors.tsx +158 -0
  201. package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
  202. package/src/panels/storyboard/BoardPlan.tsx +42 -0
  203. package/src/panels/storyboard/ComponentSheet.tsx +187 -0
  204. package/src/panels/storyboard/ComponentStage.tsx +235 -0
  205. package/src/panels/storyboard/MiniSketch.tsx +173 -0
  206. package/src/panels/storyboard/PartPicker.tsx +64 -0
  207. package/src/panels/storyboard/PlanPanel.tsx +617 -0
  208. package/src/panels/storyboard/ReferencePreview.tsx +217 -0
  209. package/src/panels/storyboard/ShotBrief.tsx +360 -0
  210. package/src/panels/storyboard/ShotCard.tsx +251 -0
  211. package/src/panels/storyboard/StagePreview.tsx +348 -0
  212. package/src/panels/storyboard/StillThumbs.tsx +162 -0
  213. package/src/panels/storyboard/StoryboardView.tsx +79 -0
  214. package/src/panels/storyboard/board.css +1065 -0
  215. package/src/panels/storyboard/board.ts +12 -0
  216. package/src/panels/storyboard/boardClipboard.ts +111 -0
  217. package/src/panels/storyboard/boardOrder.ts +68 -0
  218. package/src/panels/storyboard/boardRows.tsx +425 -0
  219. package/src/panels/storyboard/liveCap.ts +90 -0
  220. package/src/panels/storyboard/plan.css +295 -0
  221. package/src/panels/storyboard/posterCache.ts +133 -0
  222. package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
  223. package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
  224. package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
  225. package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
  226. package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
  227. package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
  228. package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
  229. package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
  230. package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
  231. package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
  232. package/src/panels/storyboard/shotboard/sketch.css +1392 -0
  233. package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
  234. package/src/panels/storyboard/storyboardDrop.ts +164 -0
  235. package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
  236. package/src/panels/studio/ComponentSidePanel.tsx +355 -0
  237. package/src/panels/studio/ComponentStudio.tsx +243 -0
  238. package/src/panels/studio/ComponentsHome.tsx +318 -0
  239. package/src/panels/studio/ComponentsPreview.tsx +179 -0
  240. package/src/panels/studio/EveryUse.tsx +244 -0
  241. package/src/panels/studio/ThisUsePanel.tsx +679 -0
  242. package/src/panels/studio/componentParts.tsx +341 -0
  243. package/src/panels/studio/components.ts +6 -0
  244. package/src/panels/studio/newCodeComponent.ts +66 -0
  245. package/src/panels/timeline/AudioFxButton.tsx +290 -0
  246. package/src/panels/timeline/AudioLane.tsx +674 -85
  247. package/src/panels/timeline/BlockRow.tsx +154 -85
  248. package/src/panels/timeline/ChannelRow.tsx +378 -129
  249. package/src/panels/timeline/CurvePane.tsx +508 -0
  250. package/src/panels/timeline/Filmstrip.tsx +361 -0
  251. package/src/panels/timeline/GenerateDrawer.tsx +407 -0
  252. package/src/panels/timeline/KeyStretch.tsx +115 -0
  253. package/src/panels/timeline/LifespanRow.tsx +730 -47
  254. package/src/panels/timeline/MarkerLane.tsx +46 -8
  255. package/src/panels/timeline/PlayButton.tsx +91 -0
  256. package/src/panels/timeline/PropertyPicker.tsx +254 -0
  257. package/src/panels/timeline/ShotRow.tsx +400 -0
  258. package/src/panels/timeline/SoundMode.tsx +1406 -0
  259. package/src/panels/timeline/TimelineDock.tsx +2338 -272
  260. package/src/panels/timeline/TransportVolume.tsx +163 -0
  261. package/src/panels/timeline/beatRuler.ts +74 -0
  262. package/src/panels/timeline/curveModel.ts +500 -0
  263. package/src/panels/timeline/duckUnderVoice.ts +68 -0
  264. package/src/panels/timeline/keyframeMenu.ts +138 -0
  265. package/src/panels/timeline/labelWidth.ts +49 -0
  266. package/src/panels/timeline/marquee.ts +155 -0
  267. package/src/panels/timeline/model.ts +1860 -199
  268. package/src/panels/timeline/snap.ts +125 -43
  269. package/src/panels/timeline/staggerPreview.tsx +92 -0
  270. package/src/panels/timeline/takeMeasure.ts +72 -0
  271. package/src/panels/timeline/useLive.ts +48 -0
  272. package/src/panels/timeline/viewport.ts +234 -0
  273. package/src/panels/toolbar/BackChip.tsx +116 -0
  274. package/src/panels/toolbar/CameraViewBar.tsx +118 -49
  275. package/src/panels/toolbar/CanvasBar.tsx +117 -0
  276. package/src/panels/toolbar/InsertButton.tsx +38 -0
  277. package/src/panels/toolbar/IsolationTrail.tsx +156 -0
  278. package/src/panels/toolbar/PanelToggles.tsx +86 -0
  279. package/src/panels/toolbar/ShadingBar.tsx +119 -26
  280. package/src/panels/toolbar/ToolDockBar.tsx +329 -80
  281. package/src/panels/toolbar/ToolFooter.tsx +102 -0
  282. package/src/panels/toolbar/ViewModeBar.tsx +247 -38
  283. package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
  284. package/src/panels/toolbar/useAddRows.tsx +326 -0
  285. package/src/panels/toolbar/useDismiss.ts +122 -0
  286. package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
  287. package/src/panels/versions/versionModel.ts +103 -0
  288. package/src/panels/virtualWindow.ts +271 -0
  289. package/src/shotTokens.css +151 -60
  290. package/src/stage/FrustumGizmo.tsx +504 -84
  291. package/src/stage/GridPlane.tsx +225 -16
  292. package/src/stage/NodePreviews.tsx +628 -0
  293. package/src/stage/PerfHud.tsx +437 -47
  294. package/src/stage/SplitRenderer.tsx +816 -169
  295. package/src/stage/StageApp.tsx +4274 -486
  296. package/src/stage/StageRootBoundary.tsx +114 -0
  297. package/src/stage/StageTools.tsx +651 -151
  298. package/src/stage/TransformGizmo.tsx +696 -0
  299. package/src/stage/adaptivePresentation.ts +145 -0
  300. package/src/stage/bridgeClient.ts +2522 -41
  301. package/src/stage/exportWorker.ts +9 -0
  302. package/src/stage/feed.ts +305 -14
  303. package/src/stage/fontPreview.ts +176 -0
  304. package/src/stage/frustumGeometry.ts +319 -1
  305. package/src/stage/furnitureTokens.ts +33 -0
  306. package/src/stage/gizmoMath.ts +508 -16
  307. package/src/stage/gpuTimer.ts +276 -0
  308. package/src/stage/instanceModules.ts +194 -33
  309. package/src/stage/interactions/move.ts +24 -5
  310. package/src/stage/isolation.ts +291 -33
  311. package/src/stage/jsSelfProfiler.ts +325 -0
  312. package/src/stage/main.tsx +116 -16
  313. package/src/stage/modalState.ts +222 -23
  314. package/src/stage/moduleEpoch.ts +60 -0
  315. package/src/stage/motionPathStage.ts +279 -0
  316. package/src/stage/motionPreview.ts +74 -0
  317. package/src/stage/overlays/Overlays.tsx +1112 -159
  318. package/src/stage/overlays/Rulers.tsx +495 -0
  319. package/src/stage/panes.ts +159 -10
  320. package/src/stage/project.ts +68 -3
  321. package/src/stage/renderScaleController.ts +730 -0
  322. package/src/stage/rulers.ts +129 -0
  323. package/src/stage/sceneExtent.ts +75 -0
  324. package/src/stage/shading.ts +20 -0
  325. package/src/stage/splineStage.ts +632 -0
  326. package/src/stage/stagePerformanceTelemetry.ts +717 -0
  327. package/src/stage/stageTheme.ts +36 -0
  328. package/src/stage/stageWarmup.ts +11 -0
  329. package/src/stage/textAutoSize.ts +124 -0
  330. package/src/stage/tier1Export.ts +641 -0
  331. package/src/stage/tokenColor.ts +14 -9
  332. package/src/stage/view.ts +56 -9
  333. package/src/stage/workingView.ts +277 -33
  334. package/src/state/accountPrefs.ts +261 -0
  335. package/src/state/agentActions.ts +173 -15
  336. package/src/state/agentActivity.ts +198 -0
  337. package/src/state/agentQuestion.ts +239 -0
  338. package/src/state/bootOpen.ts +42 -0
  339. package/src/state/cameraVerbs.ts +139 -0
  340. package/src/state/clientId.ts +6 -1
  341. package/src/state/comments.ts +1099 -0
  342. package/src/state/context.tsx +19 -1
  343. package/src/state/editorPrefs.ts +220 -0
  344. package/src/state/errorReports.ts +121 -0
  345. package/src/state/fieldWrite.ts +161 -0
  346. package/src/state/fontRecents.ts +70 -0
  347. package/src/state/fonts.ts +656 -0
  348. package/src/state/graphClipboard.ts +414 -0
  349. package/src/state/imageThumbs.ts +216 -0
  350. package/src/state/insert.ts +444 -67
  351. package/src/state/isolationTrail.ts +92 -0
  352. package/src/state/keyMoves.ts +134 -0
  353. package/src/state/keyframeClipboard.ts +177 -0
  354. package/src/state/keymap.ts +1153 -117
  355. package/src/state/layoutPrefs.ts +272 -0
  356. package/src/state/layouts.ts +205 -0
  357. package/src/state/measureMedia.ts +167 -0
  358. package/src/state/menu.ts +2341 -28
  359. package/src/state/motionStamp.ts +7 -0
  360. package/src/state/panelSize.ts +375 -5
  361. package/src/state/pendingChanges.ts +119 -0
  362. package/src/state/playHold.ts +78 -0
  363. package/src/state/playbackPreparation.ts +562 -0
  364. package/src/state/posterCapture.ts +242 -0
  365. package/src/state/presence.ts +254 -0
  366. package/src/state/projectComponents.ts +160 -0
  367. package/src/state/renderEvents.ts +298 -18
  368. package/src/state/runtimeErrors.ts +46 -0
  369. package/src/state/selection.ts +12 -0
  370. package/src/state/shortcuts.ts +1606 -0
  371. package/src/state/sketchGuide.ts +51 -0
  372. package/src/state/splineRefs.ts +87 -0
  373. package/src/state/stageHealth.ts +259 -0
  374. package/src/state/stageWatchdog.ts +47 -0
  375. package/src/state/stillStage.ts +330 -0
  376. package/src/state/stillThumbs.ts +474 -0
  377. package/src/state/store.ts +23591 -2982
  378. package/src/state/storyboardModel.ts +528 -0
  379. package/src/state/sync.ts +1257 -0
  380. package/src/state/syncCache.ts +159 -0
  381. package/src/state/triangles.ts +13 -0
  382. package/src/state/types.ts +1698 -55
  383. package/src/state/unsavedGuard.ts +49 -0
  384. package/src/state/viewMode.ts +200 -0
  385. package/src/state/webmcp.ts +1128 -0
  386. package/src/state/workspace.ts +1147 -0
  387. package/src/styles/assets-tab.css +304 -0
  388. package/src/styles/canvas.css +545 -0
  389. package/src/styles/comments.css +1435 -0
  390. package/src/styles/controls.css +162 -0
  391. package/src/styles/dialogs.css +390 -0
  392. package/src/styles/insert-drawer.css +586 -0
  393. package/src/styles/inspector-board.css +207 -0
  394. package/src/styles/inspector-object.css +486 -0
  395. package/src/styles/inspector-shot.css +252 -0
  396. package/src/styles/inspector.css +360 -0
  397. package/src/styles/left-column.css +1628 -0
  398. package/src/styles/library-preview.css +575 -0
  399. package/src/styles/library.css +512 -0
  400. package/src/styles/modes/code.css +569 -0
  401. package/src/styles/modes/component.css +1561 -0
  402. package/src/styles/modes/material.css +1201 -0
  403. package/src/styles/modes/storyboard.css +518 -0
  404. package/src/styles/share-component.css +49 -0
  405. package/src/styles/shell.css +653 -0
  406. package/src/styles/shot-brief.css +360 -0
  407. package/src/styles/states.css +76 -0
  408. package/src/styles/sync.css +380 -0
  409. package/src/styles/timeline-graph.css +384 -0
  410. package/src/styles/timeline-meters.css +89 -0
  411. package/src/styles/timeline-rows.css +1959 -0
  412. package/src/styles/timeline-sound.css +1003 -0
  413. package/src/styles/timeline.css +754 -0
  414. package/src/styles/tool-footer.css +525 -0
  415. package/src/styles/topbar.css +886 -0
  416. package/src/styles/versions.css +262 -0
  417. package/src/styles.css +72 -2921
  418. package/src/tools.ts +108 -23
  419. package/dist/assets/index-B7notge7.css +0 -1
  420. package/dist/assets/index-BBE6Ta9L.js +0 -52
  421. package/src/panels/files/icons.tsx +0 -118
  422. package/src/panels/header/DocSwitcher.tsx +0 -85
  423. package/src/panels/header/TransportBar.tsx +0 -86
  424. package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
  425. package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
  426. package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
  427. package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
  428. package/src/panels/inspector/animate/useBlocks.ts +0 -43
  429. package/src/panels/material/MaterialPicker.tsx +0 -172
  430. package/src/panels/timeline/SceneStrip.tsx +0 -145
  431. package/src/panels/toolbar/AddMenu.tsx +0 -230
  432. package/src/stage/MoveGizmo.tsx +0 -411
@@ -0,0 +1,696 @@
1
+ // The transform gizmo in the working pane, on whatever the author is moving.
2
+ //
3
+ // ── ONE GIZMO, TWO SUBJECTS, AND THE FORK IS `modalTarget` ──────────────
4
+ //
5
+ // With a selection it hangs on the selection's centre and moves layers. With
6
+ // NOTHING selected it hangs on the shot camera's apex and moves the camera —
7
+ // which is not a second feature bolted on, it is the SAME rule `G` and `S`
8
+ // already follow (modalState.ts: "layers when the selection is non-empty, else
9
+ // the CAMERA's framing"). Importing `modalTarget` rather than restating it is
10
+ // what keeps the drawn gizmo and the keyboard from ever disagreeing about whose
11
+ // transform the author is about to change.
12
+ //
13
+ // The camera's arms mean something slightly different, and `framingArmDelta`
14
+ // owns that: it has no free z, so its Z arm is the DOLLY. See that function.
15
+ //
16
+ // ── THE HANDLES, AND WHY THE CAMERA GETS FEWER ──────────────────────────
17
+ //
18
+ // Layers get three ARROWS (one design axis each), three PLANE PADS (two axes at
19
+ // once) and a view-plane DISC at the centre. The camera gets the arrows and ONE
20
+ // arc, and that is honesty rather than economy: a `Framing` is (x, y, zoom,
21
+ // roll) with no free z, so the xz and yz pads would be pads for a motion it
22
+ // cannot perform, and the xy pad would be an exact duplicate of the camera
23
+ // body's own drag that already pans it. The arms start clear of the body's
24
+ // grab box — see `GIZMO_PX.armStart` — and they outrank it in the pointer
25
+ // lane, so both stay reachable.
26
+ //
27
+ // Layers also get three ROTATE ARCS, one per design axis, which is every
28
+ // rotation the document model HAS: `arc:z` is `rollDeg` (and takes the
29
+ // selection's median as its pivot, the way the camera pane's ring already
30
+ // does), `arc:x` and `arc:y` are the two `tilt` channels (which are pivot-free,
31
+ // because `applyGesture` can only orbit a pivot in the design z plane). There
32
+ // is no fourth arc because there is no fourth channel. The camera keeps `arc:z`
33
+ // alone: a roll is the one rotation its framing has, and the arc is the grip
34
+ // the drawn camera never had for it (the `R` modal is the keyboard path).
35
+ //
36
+ // ── AND WHY THERE ARE NO SCALE HANDLES ──────────────────────────────────
37
+ //
38
+ // Because there already are. A selection quad in this pane draws the same
39
+ // eight resize handles the camera pane does (Overlays' `SelectionQuad`), and a
40
+ // multi-select draws the union box, whose corner IS a uniform scale about the
41
+ // opposite corner (`unionScale`). Cubes on these arms would be a third way to
42
+ // write `size`, competing for the same pixels as the arrowheads. `S` and its
43
+ // axis constraints remain the keyboard path.
44
+ //
45
+ // ── WHY AN ARM DRAG SOLVES A LINE AND A PAD SOLVES A PLANE ──────────────
46
+ //
47
+ // An arm the author has grabbed must stay under the cursor, and only the
48
+ // closest-approach solve between the axis line and the pointer ray does that
49
+ // (`axisParam`). Unprojecting onto a plane and zeroing two components — what
50
+ // the modal lane does, because it has no drawn arm to stay under — drifts away
51
+ // from the pointer as the view turns and runs away entirely at a grazing angle.
52
+ // A PAD has no line to stay on: the grabbed point stays under the cursor
53
+ // exactly when both rays are unprojected onto the pad's own plane, which is
54
+ // `designDelta`, the same solve a layer body drag uses.
55
+ //
56
+ // ── EDITOR-ONLY, THE SAME THREE WAYS THE FRUSTUM IS ─────────────────────
57
+ //
58
+ // `layers.set(EDITOR_LAYER)`, SplitRenderer's post pass pinned to bit 0, and no
59
+ // `SHOT_LAYER_KEY` userData. It also writes no document: the DRAG does, through
60
+ // the same `onGestureStart`/`onGesture`/`onGestureEnd` scrub every other
61
+ // transform opens.
62
+ //
63
+ // ── AND WHY IT IS MESHES AND NOT LINES ──────────────────────────────────
64
+ //
65
+ // `LineSegments` has no line width on either backend, so the arms were 1px
66
+ // hairlines with a 14px invisible grab slop around them: the affordance was
67
+ // four times thinner than its own hit area, and nothing said which end of an
68
+ // axis was positive. Cylinders and cones are `MeshBasicNodeMaterial`, which is
69
+ // a node material like everything else in this lane and therefore correct on
70
+ // the WebGPU backend as well as the WebGL2 one this stage pins.
71
+ import { useEffect, useMemo } from "react";
72
+ import { useFrame } from "@react-three/fiber";
73
+ import {
74
+ BufferAttribute,
75
+ BufferGeometry as ThreeBufferGeometry,
76
+ CircleGeometry,
77
+ ConeGeometry,
78
+ CylinderGeometry,
79
+ Line,
80
+ Matrix4,
81
+ Mesh,
82
+ PlaneGeometry,
83
+ Quaternion,
84
+ Vector3,
85
+ } from "three";
86
+ import type { BufferGeometry, Camera, Object3D } from "three";
87
+ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from "three/webgpu";
88
+ import { EDITOR_LAYER } from "./StageTools.js";
89
+ import {
90
+ ARC_SEGMENTS,
91
+ DESIGN_AXES,
92
+ GIZMO_PX,
93
+ MIN_ARM_SCREEN_PX,
94
+ MIN_PAD_SCREEN_AREA,
95
+ alongAxis,
96
+ arcPoints,
97
+ armGrabbable,
98
+ gizmoAxisOf,
99
+ gizmoHitAt,
100
+ padAxes,
101
+ padCentre,
102
+ padCorners,
103
+ screenArea,
104
+ } from "./gizmoMath.js";
105
+ import type { DesignAxis, GizmoHandleId, GizmoTarget, Vec3 } from "./gizmoMath.js";
106
+ import { modalTarget } from "./modalState.js";
107
+ import { toScreen, worldPerPixel } from "./project.js";
108
+ import { tokenColor } from "./tokenColor.js";
109
+ import type { StageTheme } from "./stageTheme.js";
110
+ import type { StageFeed } from "./feed.js";
111
+ import type { PaneRect } from "./panes.js";
112
+
113
+ /** Over everything, and over the frustum too: a handle that a camera gizmo
114
+ * could hide is a handle the author cannot grab. */
115
+ const GIZMO_RENDER_ORDER = 2_000_000;
116
+
117
+ const AXIS_FALLBACK: Readonly<Record<DesignAxis, string>> = {
118
+ x: "#ef4444",
119
+ y: "#22c55e",
120
+ z: "#3b82f6",
121
+ };
122
+
123
+ /** How far a hovered handle blends towards the theme's contrast ink. Enough to
124
+ * read as hot at a glance, little enough that red / green / blue still name
125
+ * the axis. */
126
+ const HOVER_TINT = 0.45;
127
+
128
+ /**
129
+ * What a handle that cannot answer a press is drawn at.
130
+ *
131
+ * `armGrabbable` has always had the answer and only the HIT TEST asked it, so
132
+ * a foreshortened arm — the design y at Numpad7, the design z under isolation's
133
+ * orthographic rig — was painted at full strength and then silently ignored
134
+ * every press. Hiding it would lie about the geometry; dimming it does not. The
135
+ * axis is still there and still that colour, it just stops claiming to be a
136
+ * handle.
137
+ *
138
+ * The same value dims every OTHER handle while one is being dragged, which is
139
+ * the same statement about the same thing: those cannot be grabbed either while
140
+ * a scrub is open.
141
+ */
142
+ const DEAD_OPACITY = 0.3;
143
+
144
+ /** A pad's fill is a wash, not a solid: it sits over the geometry the author is
145
+ * placing, and a solid quad there hides exactly what they are aiming at. */
146
+ const PAD_OPACITY = 0.45;
147
+
148
+ /** The view-plane disc is the quietest handle on the widget — it does the least
149
+ * constrained thing, so it makes the least visual claim. */
150
+ const DISC_OPACITY = 0.32;
151
+
152
+ /** An arc is a hairline (line width has no setting on either backend), so it
153
+ * sits closer to full strength than the filled handles do or it would read as
154
+ * already dimmed. */
155
+ const ARC_OPACITY = 0.8;
156
+
157
+ /** three's cylinder and cone are authored along +y, so an arm's orientation is
158
+ * one `setFromUnitVectors` off this. (A pad's is NOT — it needs an explicit
159
+ * basis, see where it is built.) */
160
+ const UP = new Vector3(0, 1, 0);
161
+ const _origin = new Vector3();
162
+ const _point = new Vector3();
163
+ const _view = new Vector3();
164
+
165
+ /** Is a world point behind the camera's own plane? View-space z runs NEGATIVE
166
+ * in front, so anything at or past zero has no honest projection. */
167
+ function behind(point: Vector3, camera: Camera): boolean {
168
+ return _view.copy(point).applyMatrix4(camera.matrixWorldInverse).z >= 0;
169
+ }
170
+
171
+ export interface TransformGizmoProps {
172
+ /** Resolved editor theme. It rebuilds only token-backed GL materials. */
173
+ theme: StageTheme;
174
+ feed: StageFeed;
175
+ panes: { working: PaneRect | null; camera: PaneRect | null };
176
+ selection: readonly string[];
177
+ /** The shot camera's own position at this frame, or null when no scene is on
178
+ * screen. Passed in rather than read off the feed: `FrustumGizmo` writes its
179
+ * own copy at priority 0 too, and two priority-0 subscribers have no defined
180
+ * order — so this comes from `rig`, which `StageApp` already memoizes. */
181
+ /** Where the camera IS at the clock's frame, read at FRAME time (StageApp's
182
+ * `playhead`): the arms hang on it when nothing is selected. */
183
+ cameraApexAt: () => Vec3 | null;
184
+ }
185
+
186
+ /**
187
+ * One drawable piece of the widget: an object, the handle it belongs to, and
188
+ * the cached appearance so the frame loop can skip writes that change nothing.
189
+ *
190
+ * `base` is the handle's own colour at rest, and `restAlpha` its own opacity at
191
+ * rest — a pad and an arrow on the same axis are the same hue and deliberately
192
+ * not the same weight.
193
+ *
194
+ * `kind` is carried rather than sniffed off the geometry's `type`, because two
195
+ * pieces share one handle (an arm is a shaft AND a head) and the placement
196
+ * differs between them.
197
+ */
198
+ type PieceKind = "shaft" | "head" | "pad" | "disc" | "arc";
199
+
200
+ interface Piece {
201
+ handle: GizmoHandleId;
202
+ kind: PieceKind;
203
+ object: Object3D;
204
+ material: MeshBasicNodeMaterial | LineBasicNodeMaterial;
205
+ geometry: BufferGeometry;
206
+ base: string;
207
+ fallback: string;
208
+ restAlpha: number;
209
+ tint: number;
210
+ alpha: number;
211
+ }
212
+
213
+ export function TransformGizmo({ theme, feed, panes, selection, cameraApexAt }: TransformGizmoProps) {
214
+ const hotColor = useMemo(
215
+ () => tokenColor("--gizmo-hot", theme === "light" ? "#1c1c1c" : "#ffffff"),
216
+ [theme],
217
+ );
218
+
219
+ // ── ONE MESH PER PIECE, ONE MATERIAL PER PIECE ────────────────────────
220
+ //
221
+ // Not one buffer with vertex colours: vertex colours on a node material would
222
+ // be a second thing to keep working across the WebGPU and WebGL2 backends,
223
+ // and the per-piece opacity the dimming needs is a uniform either way. Ten
224
+ // meshes is the whole cost, and nothing here allocates per frame.
225
+ const pieces = useMemo(() => {
226
+ const built: Piece[] = [];
227
+ /** Shared by every piece: the editor-only flags, the token colour, and the
228
+ * transparency the dimming needs. */
229
+ const dress = (
230
+ handle: GizmoHandleId,
231
+ kind: PieceKind,
232
+ object: Object3D,
233
+ material: MeshBasicNodeMaterial | LineBasicNodeMaterial,
234
+ geometry: BufferGeometry,
235
+ base: string,
236
+ fallback: string,
237
+ restAlpha: number,
238
+ ): void => {
239
+ material.color.copy(tokenColor(base, fallback));
240
+ material.depthTest = false;
241
+ material.depthWrite = false;
242
+ material.toneMapped = false;
243
+ // Always transparent, so a handle going dead or being dimmed under a drag
244
+ // is one `opacity` write rather than a material recompile mid-gesture.
245
+ material.transparent = true;
246
+ material.opacity = restAlpha;
247
+ // The working pane renders under a clay override in solid/wireframe mode,
248
+ // and a grey gizmo is the one thing this must not be.
249
+ material.allowOverride = false;
250
+ object.layers.set(EDITOR_LAYER);
251
+ object.renderOrder = GIZMO_RENDER_ORDER;
252
+ // Position and scale are rewritten every frame from the screen-constant
253
+ // sizing (and an arc's vertices in place), so the bounding sphere the
254
+ // culler would test is never the one the geometry holds.
255
+ object.frustumCulled = false;
256
+ object.matrixAutoUpdate = false;
257
+ built.push({
258
+ handle,
259
+ kind,
260
+ object,
261
+ material,
262
+ geometry,
263
+ base,
264
+ fallback,
265
+ restAlpha,
266
+ tint: 0,
267
+ alpha: restAlpha,
268
+ });
269
+ };
270
+ const add = (
271
+ handle: GizmoHandleId,
272
+ kind: PieceKind,
273
+ geometry: BufferGeometry,
274
+ quaternion: Quaternion,
275
+ base: string,
276
+ fallback: string,
277
+ restAlpha: number,
278
+ ): void => {
279
+ const material = new MeshBasicNodeMaterial();
280
+ const mesh = new Mesh(geometry, material);
281
+ mesh.quaternion.copy(quaternion);
282
+ dress(handle, kind, mesh, material, geometry, base, fallback, restAlpha);
283
+ };
284
+
285
+ for (const { axis, world } of DESIGN_AXES) {
286
+ // The arm's direction is FIXED (a design axis in world), so its rotation
287
+ // is computed once here and never again — only position and scale move.
288
+ const along = new Quaternion().setFromUnitVectors(UP, new Vector3(world[0], world[1], world[2]));
289
+ // Unit primitives: height 1 and radius 1, centred on the origin along +y.
290
+ // Every real dimension is a `scale`, which is what makes the per-frame
291
+ // update three number writes and no geometry rebuild.
292
+ add(`axis:${axis}`, "shaft", new CylinderGeometry(1, 1, 1, 8, 1), along, `--axis-${axis}`, AXIS_FALLBACK[axis], 1);
293
+ add(`axis:${axis}`, "head", new ConeGeometry(1, 1, 16, 1), along, `--axis-${axis}`, AXIS_FALLBACK[axis], 1);
294
+ // The PAD normal to this axis, drawn in the plane of the other two. Its
295
+ // colour is the NORMAL's, matching Blender: the pad you grab to move in
296
+ // x and y is the one that leaves z alone, and naming it "z" is the only
297
+ // label that survives an orbit.
298
+ //
299
+ // ── ITS BASIS IS EXPLICIT, NOT THE SHORTEST ROTATION ───────────────
300
+ //
301
+ // `setFromUnitVectors(FORWARD, normal)` lands the square at whatever
302
+ // in-plane angle the shortest rotation happens to give, while the HIT
303
+ // QUAD is built from `padAxes` — same plane, different square, and a
304
+ // handle whose drawn corners answer no press. Building the rotation from
305
+ // the same two axes the hit test uses is what makes the two one shape.
306
+ const [pu, pv] = padAxes(axis);
307
+ const basis = new Matrix4().makeBasis(
308
+ new Vector3(pu[0], pu[1], pu[2]),
309
+ new Vector3(pv[0], pv[1], pv[2]),
310
+ new Vector3(world[0], world[1], world[2]),
311
+ );
312
+ const facing = new Quaternion().setFromRotationMatrix(basis);
313
+ add(`plane:${axis}`, "pad", new PlaneGeometry(1, 1), facing, `--axis-${axis}`, AXIS_FALLBACK[axis], PAD_OPACITY);
314
+ // ── THE ROTATE ARC ────────────────────────────────────────────────
315
+ //
316
+ // A `Line` rather than a mesh, and its vertices are WORLD points written
317
+ // in place every frame — a torus would have to be posed AND scaled
318
+ // anisotropically to stay screen-constant, and a tube's own thickness
319
+ // would then be wrong. Same construction the frustum uses.
320
+ const geometry = new ThreeBufferGeometry();
321
+ geometry.setAttribute(
322
+ "position",
323
+ new BufferAttribute(new Float32Array((ARC_SEGMENTS + 1) * 3), 3),
324
+ );
325
+ const material = new LineBasicNodeMaterial();
326
+ const line = new Line(geometry, material);
327
+ dress(
328
+ `arc:${axis}`,
329
+ "arc",
330
+ line,
331
+ material,
332
+ geometry,
333
+ `--axis-${axis}`,
334
+ AXIS_FALLBACK[axis],
335
+ ARC_OPACITY,
336
+ );
337
+ }
338
+ // The view-plane disc faces the camera, so its rotation is the ONE that has
339
+ // to be rewritten per frame. Accent rather than an axis colour: it is the
340
+ // handle that belongs to no axis.
341
+ add("screen", "disc", new CircleGeometry(1, 24), new Quaternion(), "--accent", "#0099ff", DISC_OPACITY);
342
+ return built;
343
+ }, [theme]);
344
+
345
+ useEffect(
346
+ () => () => {
347
+ for (const piece of pieces) {
348
+ piece.geometry.dispose();
349
+ piece.material.dispose();
350
+ }
351
+ },
352
+ [pieces],
353
+ );
354
+
355
+ useFrame(() => {
356
+ const working = panes.working;
357
+ const camera = feed.workingCamera;
358
+ // `modalTarget` and not `selection.length` written out again: this is the
359
+ // same fork `G` takes, and one of them drifting is a gizmo that moves
360
+ // something other than what the pill says it will.
361
+ //
362
+ // A selection whose layers are all off-frame at this playhead anchors
363
+ // NOWHERE rather than falling through to the camera — silently retargeting
364
+ // a live selection's gizmo at the camera is the worst answer available.
365
+ const subject = modalTarget(selection.length > 0);
366
+ // A DRIVEN camera has no rig to hang arms on: a component is writing the
367
+ // camera the picture shows, and dragging the rig would move nothing in it.
368
+ // `FrustumGizmo` writes the bit one frame behind, which is invisible.
369
+ const centre =
370
+ subject === "camera"
371
+ ? feed.cameraDriven
372
+ ? null
373
+ : cameraApexAt()
374
+ : selectionCentre(feed, selection);
375
+ if (working === null || camera === null || centre === null) {
376
+ for (const piece of pieces) piece.object.visible = false;
377
+ feed.gizmoHit = null;
378
+ return;
379
+ }
380
+
381
+ _origin.set(centre[0], centre[1], centre[2]);
382
+ const perPixel = worldPerPixel(camera, _origin, working.height);
383
+ // The anchor ON SCREEN, which is what every arm's own screen length is
384
+ // measured against below. An anchor behind the camera projects to garbage,
385
+ // so nothing is grabbable at all — the same refusal each arm makes.
386
+ const anchorVisible = !behind(_origin, camera);
387
+ const anchor = toScreen(_origin, camera, working.width, working.height);
388
+ // THE HIT TABLE, built innermost-first. The order IS the priority — see
389
+ // `gizmoHitAt`, which takes the first target that contains the point rather
390
+ // than the nearest, because a pad sits on top of the two arms bounding it.
391
+ const disc: GizmoTarget[] = [];
392
+ const pads: GizmoTarget[] = [];
393
+ const arms: GizmoTarget[] = [];
394
+ const arcs: GizmoTarget[] = [];
395
+ /** Each arc's projected polyline, kept so the DRAW pass writes the very
396
+ * points the HIT test measured — one curve, not two that agree. */
397
+ const arcWorld = new Map<GizmoHandleId, readonly Vec3[]>();
398
+ // Which handles turned out to be grabbable this frame, so the draw pass can
399
+ // dim the ones that are not without asking the geometry a second time.
400
+ const live = new Set<GizmoHandleId>();
401
+ // THE CAMERA GETS ARROWS ONLY — see the header. Its framing has no free z,
402
+ // and the pads would either duplicate the body drag or promise a motion the
403
+ // document cannot store.
404
+ const full = subject === "layers";
405
+
406
+ for (const { axis, world } of DESIGN_AXES) {
407
+ const root = alongAxis(centre, world, GIZMO_PX.armStart * perPixel);
408
+ const tip = alongAxis(centre, world, GIZMO_PX.arm * perPixel);
409
+ // An arm with EITHER end behind the working camera projects to garbage,
410
+ // so it is drawn but not grabbable — the same refusal the frustum's body
411
+ // makes, and for the same reason: there is no screen segment to measure
412
+ // against.
413
+ let grabbable = anchorVisible && !isBehind(root, camera) && !isBehind(tip, camera);
414
+ if (grabbable) {
415
+ const a = toScreen(_point.set(root[0], root[1], root[2]), camera, working.width, working.height);
416
+ const b = toScreen(_point.set(tip[0], tip[1], tip[2]), camera, working.width, working.height);
417
+ // AN ARM THAT COLLAPSED ON SCREEN IS DRAWN BUT NOT GRABBABLE. The gate
418
+ // lives in `armGrabbable`, which is pure so a decision about whether
419
+ // the whole widget works can be checked without a GL context.
420
+ grabbable = armGrabbable(a, b, MIN_ARM_SCREEN_PX);
421
+ if (grabbable) {
422
+ arms.push({
423
+ handle: `axis:${axis}`,
424
+ origin: centre,
425
+ direction: world,
426
+ shape: { type: "path", points: [a, b], slop: GIZMO_PX.grab },
427
+ });
428
+ live.add(`axis:${axis}`);
429
+ }
430
+ }
431
+ // The camera's widget stops here on x and y; it keeps the z ARC below.
432
+ if (!full && axis !== "z") continue;
433
+ if (full) {
434
+ // The pad NORMAL to this axis, spanning the other two — the SAME four
435
+ // world corners the mesh is drawn at, out of the module that owns them.
436
+ const corners: [number, number][] = [];
437
+ let padVisible = anchorVisible;
438
+ for (const p of padCorners(
439
+ centre,
440
+ axis,
441
+ GIZMO_PX.padAt * perPixel,
442
+ (GIZMO_PX.pad / 2) * perPixel,
443
+ )) {
444
+ if (isBehind(p, camera)) {
445
+ padVisible = false;
446
+ break;
447
+ }
448
+ corners.push(toScreen(_point.set(p[0], p[1], p[2]), camera, working.width, working.height));
449
+ }
450
+ // A pad seen EDGE-ON projects to a sliver nothing can be inside, exactly
451
+ // as a collapsed arm does — and `designDelta` on its plane would then be
452
+ // a division by a grazing angle. `screenArea` is the same claim
453
+ // `armGrabbable` makes, in the units a quad has.
454
+ if (padVisible && screenArea(corners) >= MIN_PAD_SCREEN_AREA) {
455
+ pads.push({
456
+ handle: `plane:${axis}`,
457
+ origin: centre,
458
+ // The pad's own NORMAL: what its drag unprojects onto. See
459
+ // `GizmoGrab.direction`.
460
+ direction: world,
461
+ shape: { type: "quad", corners },
462
+ });
463
+ live.add(`plane:${axis}`);
464
+ }
465
+ }
466
+
467
+ // ── THE ROTATE ARC ──────────────────────────────────────────────────
468
+ //
469
+ // The SAME polyline is drawn and hit-tested, so an arc seen at an angle
470
+ // cannot have a grab shape that is a different curve from its picture.
471
+ // The whole arc is dropped when any point falls behind the camera, for
472
+ // the arm's reason: a projected ghost is not a curve anyone can aim at.
473
+ const points = arcPoints(centre, axis, GIZMO_PX.arc * perPixel, ARC_SEGMENTS);
474
+ arcWorld.set(`arc:${axis}`, points);
475
+ const screen: [number, number][] = [];
476
+ let arcVisible = anchorVisible;
477
+ for (const p of points) {
478
+ if (isBehind(p, camera)) {
479
+ arcVisible = false;
480
+ break;
481
+ }
482
+ screen.push(toScreen(_point.set(p[0], p[1], p[2]), camera, working.width, working.height));
483
+ }
484
+ // An arc seen EDGE-ON collapses to a straight line lying along its own
485
+ // chord — `ringAngle` still answers, but the projected curve gives the
486
+ // author nothing to aim along and every press near the anchor would grab
487
+ // it. `screenArea` of the closed polyline is the same claim `armGrabbable`
488
+ // and the pads make, in the units a curve has.
489
+ if (arcVisible && screenArea(screen) >= MIN_PAD_SCREEN_AREA) {
490
+ arcs.push({
491
+ handle: `arc:${axis}`,
492
+ origin: centre,
493
+ // The arc's own NORMAL: the axis the rotation turns ABOUT, and the
494
+ // plane its drag unprojects onto. See `GizmoGrab.direction`.
495
+ direction: world,
496
+ shape: { type: "path", points: screen, slop: GIZMO_PX.arcGrab },
497
+ });
498
+ live.add(`arc:${axis}`);
499
+ }
500
+ }
501
+
502
+ if (full && anchorVisible) {
503
+ // The disc's normal is the camera's own forward, so a drag on it moves
504
+ // the selection in the plane of the screen — the one placement gesture
505
+ // that needs no axis at all.
506
+ const forward = cameraForward(camera);
507
+ disc.push({
508
+ handle: "screen",
509
+ origin: centre,
510
+ direction: forward,
511
+ shape: { type: "disc", at: anchor, radius: GIZMO_PX.screen },
512
+ });
513
+ live.add("screen");
514
+ }
515
+
516
+ // ORDER IS PRIORITY: innermost first, which is the only order in which every
517
+ // handle stays reachable — and the ARMS outrank the ARCS deliberately, so
518
+ // the two points where each quarter arc crosses the axes that bound it
519
+ // belong to the arms.
520
+ const targets = [...disc, ...pads, ...arms, ...arcs];
521
+ feed.gizmoHit = (x, y) => {
522
+ const hit = gizmoHitAt(targets, x, y);
523
+ return hit === null
524
+ ? null
525
+ : { subject, handle: hit.handle, origin: hit.origin, direction: hit.direction };
526
+ };
527
+
528
+ // ── DRAW ──────────────────────────────────────────────────────────────
529
+ const held = feed.activeHandle;
530
+ for (const piece of pieces) {
531
+ const axis = gizmoAxisOf(piece.handle);
532
+ // ARROWS AND THE ROLL ARC ARE THE CAMERA'S WHOLE WIDGET — see the header
533
+ // for why its framing gets no pads, no disc and no other arc.
534
+ const shown =
535
+ piece.kind === "shaft" || piece.kind === "head" || piece.handle === "arc:z" ? true : full;
536
+ piece.object.visible = shown;
537
+ if (!shown) continue;
538
+ if (piece.kind === "disc") {
539
+ // The only piece whose ROTATION moves: it faces the camera, which turns
540
+ // under the author's orbit while everything else is pinned to a design
541
+ // axis.
542
+ piece.object.quaternion.copy(camera.quaternion);
543
+ const r = GIZMO_PX.screen * perPixel;
544
+ placeAt(piece.object, centre, r, r, 1);
545
+ } else if (piece.kind === "pad" && axis !== null) {
546
+ const side = GIZMO_PX.pad * perPixel;
547
+ placeAt(piece.object, padCentre(centre, axis, GIZMO_PX.padAt * perPixel), side, side, 1);
548
+ } else if (piece.kind === "arc") {
549
+ // WORLD vertices, rewritten in place from the very points the hit test
550
+ // measured — the object stays at the origin with an identity matrix,
551
+ // which is the same construction the frustum gizmo uses.
552
+ const points = arcWorld.get(piece.handle);
553
+ if (points !== undefined) writeArc(piece.geometry, points);
554
+ } else if (axis !== null) {
555
+ const world = DESIGN_AXES.find((a) => a.axis === axis)!.world;
556
+ const shaftEnd = (GIZMO_PX.arm - GIZMO_PX.arrowHead) * perPixel;
557
+ const start = GIZMO_PX.armStart * perPixel;
558
+ // Two pieces share `axis:*`, and `kind` is what tells them apart: the
559
+ // SHAFT is the cylinder, the HEAD is the cone. Neither is a handle of
560
+ // its own — a press on either is a press on the arm.
561
+ if (piece.kind === "shaft") {
562
+ placeAt(
563
+ piece.object,
564
+ alongAxis(centre, world, (start + shaftEnd) / 2),
565
+ GIZMO_PX.shaft * perPixel,
566
+ Math.max(shaftEnd - start, 0),
567
+ GIZMO_PX.shaft * perPixel,
568
+ );
569
+ } else {
570
+ placeAt(
571
+ piece.object,
572
+ alongAxis(centre, world, shaftEnd + (GIZMO_PX.arrowHead * perPixel) / 2),
573
+ GIZMO_PX.arrowWidth * perPixel,
574
+ GIZMO_PX.arrowHead * perPixel,
575
+ GIZMO_PX.arrowWidth * perPixel,
576
+ );
577
+ }
578
+ }
579
+
580
+ // ── HOT, DIM, OR AT REST ────────────────────────────────────────────
581
+ //
582
+ // Blender highlights the gizmo element under the cursor and dims the rest
583
+ // once a transform is running, and this pane did neither — so "is this
584
+ // grabbable" was something the author learned by trying, and a live drag
585
+ // kept three full-strength lines across the geometry being judged.
586
+ //
587
+ // Here, in the frame loop, off two plain fields the pointer lane wrote. A
588
+ // hover therefore costs one uniform write and zero React renders, which
589
+ // is the whole reason `feed.hoverHandle` is a field and not state.
590
+ const hot = held === null ? feed.hoverHandle === piece.handle : held === piece.handle;
591
+ const dim = held === null ? !live.has(piece.handle) : held !== piece.handle;
592
+ const wantTint = hot ? HOVER_TINT : 0;
593
+ const wantAlpha = dim ? DEAD_OPACITY * piece.restAlpha : piece.restAlpha;
594
+ if (piece.tint !== wantTint) {
595
+ piece.tint = wantTint;
596
+ piece.material.color.copy(tokenColor(piece.base, piece.fallback)).lerp(hotColor, wantTint);
597
+ }
598
+ if (piece.alpha !== wantAlpha) {
599
+ piece.alpha = wantAlpha;
600
+ piece.material.opacity = wantAlpha;
601
+ }
602
+ }
603
+ }, 0);
604
+
605
+ useEffect(
606
+ () => () => {
607
+ feed.gizmoHit = null;
608
+ },
609
+ [feed],
610
+ );
611
+
612
+ return (
613
+ <>
614
+ {pieces.map((piece, i) => (
615
+ <primitive key={`${piece.handle}-${i}`} object={piece.object} />
616
+ ))}
617
+ </>
618
+ );
619
+ }
620
+
621
+ /** An arc's world points into its position buffer, past the frustum gizmo's own
622
+ * half-pixel gate: this runs on every committed frame, and a parked playhead
623
+ * with a selection must not re-upload three buffers per seek. */
624
+ function writeArc(geometry: BufferGeometry, points: readonly Vec3[]): void {
625
+ const attribute = geometry.getAttribute("position") as BufferAttribute;
626
+ const buffer = attribute.array as Float32Array;
627
+ let moved = false;
628
+ for (let i = 0; i < points.length; i++) {
629
+ const p = points[i]!;
630
+ for (let c = 0; c < 3; c++) {
631
+ const at = i * 3 + c;
632
+ if (Math.abs(buffer[at]! - p[c]!) > 0.5) moved = true;
633
+ buffer[at] = p[c]!;
634
+ }
635
+ }
636
+ if (moved) attribute.needsUpdate = true;
637
+ }
638
+
639
+ function isBehind(point: Vec3, camera: Camera): boolean {
640
+ return behind(_point.set(point[0], point[1], point[2]), camera);
641
+ }
642
+
643
+ /** The camera's own forward, in world. The third basis column of its world
644
+ * matrix points BACK from the eye, so the forward is its negation. */
645
+ function cameraForward(camera: Camera): Vec3 {
646
+ const e = camera.matrixWorld.elements;
647
+ return [-e[8]!, -e[9]!, -e[10]!];
648
+ }
649
+
650
+ /** One piece's per-frame update: three position writes, three scale writes and
651
+ * one matrix compose. No allocation, because a parked playhead with a
652
+ * selection has to cost nothing. */
653
+ function placeAt(object: Object3D, at: Vec3, sx: number, sy: number, sz: number): void {
654
+ object.position.set(at[0], at[1], at[2]);
655
+ object.scale.set(sx, sy, sz);
656
+ object.updateMatrix();
657
+ }
658
+
659
+ /**
660
+ * Where the gizmo sits: the mean of the selected layers' own origins.
661
+ *
662
+ * The GROUP origin out of `feed.frames`, never the mesh's centre — a paint
663
+ * layer's plane is grown by the rasterizer's shadow padding, so the mesh would
664
+ * put the gizmo a blur radius away from the thing it moves (the same reason
665
+ * `collectFrames` reads userData).
666
+ *
667
+ * Null when nothing selected is on screen this frame, which includes a
668
+ * selection whose lifetime has ended: a handle drawn for a layer that is not
669
+ * there would drag something the author cannot see.
670
+ */
671
+ function selectionCentre(feed: StageFeed, selection: readonly string[]): Vec3 | null {
672
+ const frames = feed.frames;
673
+ if (frames === null || selection.length === 0) return null;
674
+ let x = 0;
675
+ let y = 0;
676
+ let z = 0;
677
+ let count = 0;
678
+ // ONE FRAME PER ID, the first — the same rule `StageTools`' `byId` follows.
679
+ // A cloner stamps its children N times under one id, and averaging all the
680
+ // clones put the gizmo in the empty space between them rather than on the
681
+ // layer the author selected.
682
+ const seen = new Set<string>();
683
+ for (const frame of frames) {
684
+ if (!selection.includes(frame.id) || seen.has(frame.id)) continue;
685
+ seen.add(frame.id);
686
+ const e = frame.matrixWorld.elements;
687
+ x += e[12]!;
688
+ y += e[13]!;
689
+ z += e[14]!;
690
+ count += 1;
691
+ }
692
+ return count === 0 ? null : [x / count, y / count, z / count];
693
+ }
694
+
695
+ // `worldPerPixel` lives in project.ts now: the camera body (FrustumGizmo) sizes
696
+ // itself with the same function, and one copy is how the two stay one rule.