@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,861 @@
1
+ // The node canvas: React Flow's props and handlers, and nothing else.
2
+ //
3
+ // Everything this file used to also be now lives beside it — the graph-to-flow
4
+ // conversion in `flowModel.ts`, the socket vocabulary in `socketGlyph.ts`, the
5
+ // pan memory in `viewportMemory.ts`, the thumbnails in `previewScheduler.ts`,
6
+ // the four renderers in their own files. What is left is the wiring, which is
7
+ // the part that has to read as one page.
8
+ //
9
+ // ── THE THREE RULES THIS FILE IS HELD TO ────────────────────────────────
10
+ //
11
+ // NO EDIT HAPPENS HERE. Every handler ends at a store verb, so a gesture is
12
+ // one transaction, one undo entry and one refusal route. v1 built the next
13
+ // graph inline and handed it to `onUpdate`, which is how "connect" ended up
14
+ // with its own duplicate-edge rule that the validator did not share.
15
+ //
16
+ // NO VALIDATOR PER POINTER FRAME. `isValidConnection` fires on every hover
17
+ // while a wire is in hand; v1 ran `validateMaterialGraph` over the WHOLE
18
+ // graph inside it, which is O(nodes + edges) per mouse move and was
19
+ // measurably the slowest thing in the lane. It asks the memoised type index
20
+ // one question now. The problem LIST still runs the full validator, once per
21
+ // graph, in a `useMemo`.
22
+ //
23
+ // THE KEYMAP OWNS THE KEYS. This canvas has no `onKeyDown` of its own. It is
24
+ // focusable and it carries the `.material-graph-canvas` class, which is the
25
+ // whole of its contract with `state/keymap.ts` — that class under the event
26
+ // target is what puts the resolver in the graph lane. A local handler here
27
+ // was a second binding site for Delete, and the two disagreed about whether
28
+ // an edge counted as a selection.
29
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
30
+ import { createPortal } from "react-dom";
31
+ import {
32
+ Background,
33
+ MiniMap,
34
+ ReactFlow,
35
+ ReactFlowProvider,
36
+ SelectionMode,
37
+ useNodesInitialized,
38
+ useReactFlow,
39
+ useStore as useFlowStore,
40
+ } from "@xyflow/react";
41
+ import { IconMinus, IconPlus, LockedControls } from "@dailies-studio/ui";
42
+ import type {
43
+ Connection,
44
+ Edge,
45
+ EdgeChange,
46
+ EdgeMouseHandler,
47
+ FinalConnectionState,
48
+ NodeChange,
49
+ NodeMouseHandler,
50
+ OnNodeDrag,
51
+ Viewport,
52
+ } from "@xyflow/react";
53
+ import {
54
+ indexMaterialGraphTypes,
55
+ materialGraphPixelSignature,
56
+ materialGraphSemanticSignature,
57
+ validateMaterialGraph,
58
+ } from "@dailies-studio/shot/doc";
59
+ import type { MaterialGraph3D } from "@dailies-studio/shot/doc";
60
+ import { useShotEditor, useStore } from "../../state/context.js";
61
+ import { MATERIAL_GRAPH_GRID } from "../../state/store.js";
62
+ import {
63
+ buildMaterialFlow,
64
+ graphPositionOf,
65
+ groupUnderPoint,
66
+ nodesInScope,
67
+ segmentIntersectsRect,
68
+ } from "./flowModel.js";
69
+ import type { MaterialFlowNode } from "./flowModel.js";
70
+ import { MaterialConnectProvider, MaterialNodeCard } from "./MaterialNode.js";
71
+ import { MaterialGroupNode } from "./MaterialGroupNode.js";
72
+ import { MaterialCommentNode } from "./MaterialCommentNode.js";
73
+ import { MaterialRerouteNode } from "./MaterialRerouteNode.js";
74
+ import { MaterialNodePalette } from "./MaterialNodePalette.js";
75
+ import { useMaterialGraphMenu } from "./MaterialGraphMenu.js";
76
+ import { MaterialPreviewProvider, NODE_PREVIEW_PX } from "./NodePreview.js";
77
+ import { createPreviewScheduler, visiblePreviewNodes } from "./previewScheduler.js";
78
+ import type { PreviewEntry } from "./previewScheduler.js";
79
+ import { materialViewportKey, recallMaterialViewport, rememberMaterialViewport } from "./viewportMemory.js";
80
+ // React Flow's own sheet, in this chunk with the canvas that needs it.
81
+ import "./reactFlow.css";
82
+
83
+ const NODE_TYPES = {
84
+ material: MaterialNodeCard,
85
+ materialGroup: MaterialGroupNode,
86
+ materialComment: MaterialCommentNode,
87
+ materialReroute: MaterialRerouteNode,
88
+ } as const;
89
+
90
+ const FIT_VIEW = { padding: 0.2, maxZoom: 1 } as const;
91
+ const NO_PREVIEWS: ReadonlyMap<string, PreviewEntry> = new Map();
92
+
93
+ /** Fallback card size for the two drag hit tests, used only until React Flow
94
+ * has measured a card. Same nominal box `MATERIAL_NODE_CARD` names in the
95
+ * store, and for the same reason: a hit test may not depend on the zoom. */
96
+ const MATERIAL_CARD_W = 180;
97
+ const MATERIAL_CARD_H = 96;
98
+
99
+ export interface MaterialGraphCanvasProps {
100
+ /** The graph the workspace resolved THROUGH THE SLOT. Passed rather than
101
+ * looked up, so the canvas cannot disagree with the header about which
102
+ * surface is on screen. */
103
+ graph: MaterialGraph3D;
104
+ materialId: string;
105
+ layerId: string;
106
+ slot: number | null;
107
+ /** Where the graph's zoom goes: a place in the canvas bar the workspace
108
+ * keeps for it (MaterialWorkspace.tsx). Absent or null, no zoom is shown. */
109
+ zoomSlot?: HTMLElement | null;
110
+ }
111
+
112
+ export function MaterialGraphCanvas(props: MaterialGraphCanvasProps) {
113
+ // A viewer's cards show every field in its own locked state. The zoom is a
114
+ // view control, so it stays outside.
115
+ const readOnly = useShotEditor((state) => state.readOnly);
116
+ return (
117
+ <ReactFlowProvider>
118
+ <LockedControls value={readOnly}>
119
+ <MaterialGraphCanvasInner {...props} />
120
+ </LockedControls>
121
+ {/* INSIDE THE PROVIDER, and portalled out to the canvas bar. The zoom
122
+ drives this flow's own viewport, which only a component under the
123
+ provider can reach; the portal keeps it in this subtree while its
124
+ buttons sit in the bar with the rest of the mode's verbs. */}
125
+ {props.zoomSlot == null ? null : createPortal(<GraphZoom />, props.zoomSlot)}
126
+ </ReactFlowProvider>
127
+ );
128
+ }
129
+
130
+ /**
131
+ * The graph's zoom, in the canvas bar: out, the percentage, in, Fit.
132
+ *
133
+ * It replaces React Flow's own `<Controls>` island, which floated over the
134
+ * graph's corner in a second look. The readout subscribes to the ZOOM alone,
135
+ * not the viewport, so a pan does not re-render it.
136
+ */
137
+ function GraphZoom() {
138
+ const flow = useReactFlow();
139
+ const zoom = useFlowStore((flowState) => flowState.transform[2]);
140
+ return (
141
+ <div className="zoom-controls" role="group" aria-label="Graph zoom" data-testid="material-zoom">
142
+ <button
143
+ type="button"
144
+ className="btn is-icon"
145
+ aria-label="Zoom the graph out"
146
+ title="Zoom the graph out"
147
+ onClick={() => void flow.zoomOut()}
148
+ >
149
+ <IconMinus />
150
+ </button>
151
+ <span className="zoom-readout" data-testid="material-zoom-level">
152
+ {Math.round(zoom * 100)}%
153
+ </span>
154
+ <button
155
+ type="button"
156
+ className="btn is-icon"
157
+ aria-label="Zoom the graph in"
158
+ title="Zoom the graph in"
159
+ onClick={() => void flow.zoomIn()}
160
+ >
161
+ <IconPlus />
162
+ </button>
163
+ {/* Named for its subject: the dock head under the graph has a "Fit" of
164
+ its own, for the timeline. */}
165
+ <button
166
+ type="button"
167
+ className="btn"
168
+ aria-label="Fit the graph"
169
+ title="Fit the whole graph (Home or F over the graph)"
170
+ onClick={() => void flow.fitView(FIT_VIEW)}
171
+ >
172
+ Fit
173
+ </button>
174
+ </div>
175
+ );
176
+ }
177
+
178
+ function MaterialGraphCanvasInner({ graph, materialId, layerId, slot }: MaterialGraphCanvasProps) {
179
+ const store = useStore();
180
+ const flow = useReactFlow<MaterialFlowNode, Edge>();
181
+ const initialised = useNodesInitialized();
182
+ const menu = useMaterialGraphMenu();
183
+ const openMenu = menu.open;
184
+ const surface = useRef<HTMLElement | null>(null);
185
+
186
+ const assets = useShotEditor((state) => state.doc?.assets);
187
+ const path = useShotEditor((state) => state.materialGraphPath);
188
+ const selection = useShotEditor((state) => state.materialGraphSelection);
189
+ const palette = useShotEditor((state) => state.materialPalette);
190
+ const snapToGrid = useShotEditor((state) => state.materialSnapToGrid);
191
+ // A VIEWER reads the graph: no node moves and no wire is drawn.
192
+ const readOnly = useShotEditor((state) => state.readOnly);
193
+ const frameRequest = useShotEditor((state) => state.materialFrameRequest);
194
+
195
+ /** Positions while a drag is live. React Flow needs the cards to follow the
196
+ * pointer, and the DOCUMENT must not: sixty writes per drag is sixty undo
197
+ * entries. Committed once at drag stop, through `moveMaterialNodes`. */
198
+ const [draft, setDraft] = useState<Readonly<Record<string, { x: number; y: number }>> | null>(null);
199
+ const [previews, setPreviews] = useState<ReadonlyMap<string, PreviewEntry>>(NO_PREVIEWS);
200
+ /** What the live drag would do on release: the wire it would be spliced into,
201
+ * and the group box it would join. Both are ANSWERS on screen, never edits —
202
+ * the document learns about either only at `onNodeDragStop`. */
203
+ const [spliceEdge, setSpliceEdge] = useState<string | null>(null);
204
+ const [dropGroup, setDropGroup] = useState<string | null>(null);
205
+ /** Box sizes while a resize is live, for the reason `draft` exists: the flow
206
+ * is controlled, so nothing follows the handle unless this does. */
207
+ const [draftSize, setDraftSize] = useState<Readonly<
208
+ Record<string, { width: number; height: number }>
209
+ > | null>(null);
210
+
211
+ const types = useMemo(() => indexMaterialGraphTypes(graph), [graph]);
212
+ const problems = useMemo(() => validateMaterialGraph(graph, assets ?? {}), [graph, assets]);
213
+ const built = useMemo(
214
+ () =>
215
+ buildMaterialFlow({
216
+ graph,
217
+ materialId,
218
+ path,
219
+ assets: assets ?? {},
220
+ selection,
221
+ problems,
222
+ types,
223
+ }),
224
+ [graph, materialId, path, assets, selection, problems, types],
225
+ );
226
+ // The draft is spliced in AFTER the memo, so a drag re-renders the cards it
227
+ // moves and leaves every other card's object identity alone. The drop
228
+ // highlight rides the same splice, for the same reason.
229
+ const nodes = useMemo(
230
+ () =>
231
+ draft === null && dropGroup === null && draftSize === null
232
+ ? built.nodes
233
+ : built.nodes.map((node) => {
234
+ const at = draft?.[node.id];
235
+ const box = draftSize?.[node.id];
236
+ const marked = node.id === dropGroup;
237
+ if (at === undefined && box === undefined && !marked) return node;
238
+ return {
239
+ ...node,
240
+ ...(at === undefined ? {} : { position: at }),
241
+ ...(box === undefined ? {} : { style: { ...node.style, ...box } }),
242
+ ...(marked ? { className: [node.className, "is-drop-target"].filter(Boolean).join(" ") } : {}),
243
+ };
244
+ }),
245
+ [built.nodes, draft, dropGroup, draftSize],
246
+ );
247
+ // Only the one wire changes identity, so a pointer frame during a drag
248
+ // re-renders one edge rather than all of them.
249
+ const edges = useMemo(
250
+ () =>
251
+ spliceEdge === null
252
+ ? built.edges
253
+ : built.edges.map((edge) =>
254
+ edge.id === spliceEdge
255
+ ? { ...edge, className: `${edge.className ?? ""} is-splice-target`.trim() }
256
+ : edge,
257
+ ),
258
+ [built.edges, spliceEdge],
259
+ );
260
+
261
+ const canConnect = useCallback(
262
+ (source: { nodeId: string; socket: string }, target: { nodeId: string; socket: string }): boolean =>
263
+ types.canConnect(source, target).ok,
264
+ [types],
265
+ );
266
+
267
+ // ── viewport memory ──────────────────────────────────────────────────────
268
+ const viewportKey = materialViewportKey(materialId, path);
269
+ const restored = useRef<string | null>(null);
270
+ useEffect(() => {
271
+ // React Flow renders nothing until it has measured its container, and it
272
+ // reports a zero-size viewport until then — restoring into that lands the
273
+ // graph off screen with no way back but the frame key.
274
+ if (!initialised || restored.current === viewportKey) return;
275
+ restored.current = viewportKey;
276
+ const remembered = recallMaterialViewport(viewportKey);
277
+ if (remembered === null) void flow.fitView(FIT_VIEW);
278
+ else void flow.setViewport(remembered);
279
+ }, [initialised, viewportKey, flow]);
280
+
281
+ // The FRAME mailbox: a fresh identity per ask, like `appMenu`, because only
282
+ // this component knows its own viewport.
283
+ const framed = useRef(frameRequest);
284
+ useEffect(() => {
285
+ if (frameRequest === null || framed.current === frameRequest) {
286
+ framed.current = frameRequest;
287
+ return;
288
+ }
289
+ framed.current = frameRequest;
290
+ void flow.fitView(FIT_VIEW);
291
+ }, [frameRequest, flow]);
292
+
293
+ const rememberNow = useCallback(
294
+ (viewport: Viewport) => rememberMaterialViewport(materialViewportKey(materialId, path), viewport),
295
+ [materialId, path],
296
+ );
297
+
298
+ // ── node previews ────────────────────────────────────────────────────────
299
+ const scheduler = useMemo(
300
+ () =>
301
+ createPreviewScheduler({
302
+ request: async (request) => (await store.stage()?.requestNodePreviews(request)) ?? null,
303
+ onChange: (entries) => setPreviews(entries),
304
+ }),
305
+ [store],
306
+ );
307
+ useEffect(() => () => scheduler.dispose(), [scheduler]);
308
+
309
+ const askForPreviews = useCallback(() => {
310
+ const box = surface.current?.getBoundingClientRect();
311
+ // INSIDE A FUNCTION, NOTHING IS PREVIEWED. A function body compiles with
312
+ // its call site's arguments bound, so a thumbnail of a node in there would
313
+ // be a picture of one call among several — v2 is main-scope only, and a
314
+ // wrong picture is worse than none.
315
+ if (box === undefined || path.length > 0) {
316
+ scheduler.ask(null);
317
+ return;
318
+ }
319
+ const wanted = visiblePreviewNodes(nodesInScope(graph, path), flow.getViewport(), box);
320
+ if (wanted.length === 0) {
321
+ scheduler.ask(null);
322
+ return;
323
+ }
324
+ scheduler.ask({
325
+ materialId,
326
+ layerId,
327
+ slot,
328
+ nodeIds: wanted,
329
+ size: Math.round(NODE_PREVIEW_PX * (window.devicePixelRatio || 1)),
330
+ pixelSignature: materialGraphPixelSignature(graph),
331
+ semanticSignature: materialGraphSemanticSignature(graph),
332
+ });
333
+ }, [graph, path, materialId, layerId, slot, flow, scheduler]);
334
+
335
+ useEffect(() => {
336
+ askForPreviews();
337
+ }, [askForPreviews, initialised]);
338
+
339
+ // ── handlers ─────────────────────────────────────────────────────────────
340
+ /**
341
+ * The store's selection, written from one place.
342
+ *
343
+ * ── WHY THIS IS THE CHANGE STREAM AND NOT `onSelectionChange` ─────────
344
+ *
345
+ * In a CONTROLLED flow — one where `nodes` is a prop, which is the only kind
346
+ * this canvas can be — React Flow does not apply a selection itself. Look at
347
+ * `triggerNodeChanges`: it applies changes only when it owns the nodes, and
348
+ * otherwise emits them and waits for the owner to write them back. So
349
+ * `onSelectionChange` fires from React Flow's LOOKUP, which is filled from
350
+ * our `nodes` prop, which is filled from the store — it can only ever echo
351
+ * what we already wrote. Wiring the store to it makes clicking a card do
352
+ * precisely nothing, which is what it did.
353
+ *
354
+ * So the change stream is the writer, and it is still exactly one: both
355
+ * halves land in `setMaterialGraphSelection`, which is identity-stable when
356
+ * the answer has not moved.
357
+ */
358
+ const writeSelection = useCallback(
359
+ (next: { nodeIds?: readonly string[]; edgeIds?: readonly string[] }) => {
360
+ // The half that did not change is re-read LIVE rather than closed over:
361
+ // one gesture emits node changes and edge changes separately, and a
362
+ // stale read would make the second undo the first.
363
+ const current = store.getState().materialGraphSelection;
364
+ store.setMaterialGraphSelection({
365
+ nodeIds: next.nodeIds ?? current.nodeIds,
366
+ edgeIds: next.edgeIds ?? current.edgeIds,
367
+ });
368
+ },
369
+ [store],
370
+ );
371
+
372
+ const onNodesChange = useCallback(
373
+ (changes: NodeChange<MaterialFlowNode>[]) => {
374
+ // POSITION, SELECT and a LIVE RESIZE. Removal is impossible because
375
+ // `deleteKeyCode` is null — the keymap owns Delete.
376
+ let moved: Record<string, { x: number; y: number }> | null = null;
377
+ let picked: Set<string> | null = null;
378
+ let sized: Record<string, { width: number; height: number }> | null = null;
379
+ let settled = false;
380
+ for (const change of changes) {
381
+ if (change.type === "position" && change.dragging === true && change.position !== undefined) {
382
+ moved ??= {};
383
+ moved[change.id] = change.position;
384
+ } else if (change.type === "select") {
385
+ picked ??= new Set(store.getState().materialGraphSelection.nodeIds);
386
+ if (change.selected) picked.add(change.id);
387
+ else picked.delete(change.id);
388
+ } else if (change.type === "dimensions" && change.dimensions !== undefined) {
389
+ // THE SAME BARGAIN THE POSITION DRAFT MAKES. The flow is controlled,
390
+ // so React Flow cannot resize a box on its own; without this the
391
+ // corner handle would drag against a card that never moved. Held
392
+ // locally until `onResizeEnd` commits, so a resize is one entry and
393
+ // not one per frame.
394
+ if (change.resizing === true) {
395
+ sized ??= {};
396
+ sized[change.id] = change.dimensions;
397
+ } else if (change.resizing === false) settled = true;
398
+ }
399
+ }
400
+ if (picked !== null) writeSelection({ nodeIds: [...picked] });
401
+ // The commit has landed in the document, so the local override must go or
402
+ // it would outrank it.
403
+ if (settled) setDraftSize(null);
404
+ if (sized !== null) {
405
+ const next = sized;
406
+ setDraftSize((current) => ({ ...(current ?? {}), ...next }));
407
+ }
408
+ if (moved === null) return;
409
+ const next = moved;
410
+ setDraft((current) => ({ ...(current ?? {}), ...next }));
411
+ },
412
+ [store, writeSelection],
413
+ );
414
+
415
+ const onEdgesChange = useCallback(
416
+ (changes: EdgeChange<Edge>[]) => {
417
+ let picked: Set<string> | null = null;
418
+ for (const change of changes) {
419
+ if (change.type !== "select") continue;
420
+ picked ??= new Set(store.getState().materialGraphSelection.edgeIds);
421
+ if (change.selected) picked.add(change.id);
422
+ else picked.delete(change.id);
423
+ }
424
+ if (picked !== null) writeSelection({ edgeIds: [...picked] });
425
+ },
426
+ [store, writeSelection],
427
+ );
428
+
429
+ /**
430
+ * WHAT THE DRAG WOULD DO IF IT ENDED NOW — the wire it would splice into and
431
+ * the box it would join. Live state, so the answer is on screen before the
432
+ * author commits to it; both are recomputed from final positions at drop.
433
+ */
434
+ const onNodeDrag = useCallback<OnNodeDrag<MaterialFlowNode>>(
435
+ (_event, dragged) => {
436
+ // SPLICE NEEDS ONE CARD WITH TWO ENDS. Dragging a selection says nothing
437
+ // about which card goes in the wire, and a node with no inputs or no
438
+ // outputs cannot sit in the middle of one.
439
+ const alone = draft === null || Object.keys(draft).length === 1;
440
+ const data = dragged.data;
441
+ const spliceable =
442
+ alone &&
443
+ (dragged.type === "material" || dragged.type === "materialReroute") &&
444
+ data.inputs.length > 0 &&
445
+ data.outputs.length > 0;
446
+
447
+ let hit: string | null = null;
448
+ if (spliceable) {
449
+ // Which of this card's inputs are already wired. Built once per pointer
450
+ // Which of this card's inputs are already wired, so the highlight
451
+ // promises only splices `dropMaterialNodes` will actually make. Built
452
+ // once per pointer frame rather than once per candidate wire.
453
+ const filled = new Set(
454
+ graph.edges
455
+ .filter((candidate) => candidate.target.nodeId === dragged.id)
456
+ .map((candidate) => `${candidate.target.nodeId}\u0000${candidate.target.socket}`),
457
+ );
458
+ const self = flow.getInternalNode(dragged.id);
459
+ const box = self?.measured;
460
+ if (self !== undefined && box?.width != null && box.height != null) {
461
+ const rect = {
462
+ x: self.internals.positionAbsolute.x,
463
+ y: self.internals.positionAbsolute.y,
464
+ width: box.width,
465
+ height: box.height,
466
+ };
467
+ for (const edge of built.edges) {
468
+ if (edge.source === dragged.id || edge.target === dragged.id) continue;
469
+ const from = flow.getInternalNode(edge.source);
470
+ const to = flow.getInternalNode(edge.target);
471
+ if (from === undefined || to === undefined) continue;
472
+ // ponytail: the wire is approximated as a straight segment between
473
+ // the two cards' facing edges. Edges are drawn smoothstep, so a
474
+ // sharply bent one can be reported a few pixels off. Upgrade by
475
+ // sampling `getSmoothStepPath` if it ever reads wrong.
476
+ const a = {
477
+ x: from.internals.positionAbsolute.x + (from.measured?.width ?? 0),
478
+ y: from.internals.positionAbsolute.y + (from.measured?.height ?? 0) / 2,
479
+ };
480
+ const b = {
481
+ x: to.internals.positionAbsolute.x,
482
+ y: to.internals.positionAbsolute.y + (to.measured?.height ?? 0) / 2,
483
+ };
484
+ if (!segmentIntersectsRect(a, b, rect)) continue;
485
+ // CROSSING IS NOT ENOUGH — it has to be a splice the graph would
486
+ // accept. Two type questions against the memoised index, which is
487
+ // the same budget `isValidConnection` spends per hover.
488
+ const source = { nodeId: edge.source, socket: edge.sourceHandle ?? "value" };
489
+ const target = { nodeId: edge.target, socket: edge.targetHandle ?? "value" };
490
+ // A FREE input, matching `dropMaterialNodes` exactly — the
491
+ // highlight must promise the edit the drop will actually make, and
492
+ // an already-wired input is one this gesture will not take.
493
+ const takes = data.inputs.some(
494
+ (socket) =>
495
+ !filled.has(`${dragged.id}\u0000${socket.id}`) &&
496
+ types.canConnect(source, { nodeId: dragged.id, socket: socket.id }).ok,
497
+ );
498
+ const gives = data.outputs.some(
499
+ (socket) => types.canConnect({ nodeId: dragged.id, socket: socket.id }, target).ok,
500
+ );
501
+ if (takes && gives) {
502
+ hit = edge.id;
503
+ break;
504
+ }
505
+ }
506
+ }
507
+ }
508
+ setSpliceEdge(hit);
509
+
510
+ // THE BOX, for any dragged card. A group cannot go inside a group, and a
511
+ // card's CENTRE decides — using a corner would join a box the author only
512
+ // brushed the edge of.
513
+ const centre = graphPositionOf(
514
+ { position: dragged.position, parentId: dragged.parentId },
515
+ graph,
516
+ );
517
+ const size = dragged.measured;
518
+ setDropGroup(
519
+ dragged.type === "materialGroup"
520
+ ? null
521
+ : groupUnderPoint(graph, path, [
522
+ centre[0] + (size?.width ?? MATERIAL_CARD_W) / 2,
523
+ centre[1] + (size?.height ?? MATERIAL_CARD_H) / 2,
524
+ ]),
525
+ );
526
+ },
527
+ [draft, flow, built.edges, types, graph, path],
528
+ );
529
+
530
+ const onNodeDragStop = useCallback(() => {
531
+ const held = draft;
532
+ const splicing = spliceEdge;
533
+ setDraft(null);
534
+ setSpliceEdge(null);
535
+ setDropGroup(null);
536
+ if (held === null) return;
537
+ const positions: Record<string, [number, number]> = {};
538
+ const membership: Record<string, string | null> = {};
539
+ let single: string | null = null;
540
+ for (const [id, at] of Object.entries(held)) {
541
+ const node = built.nodes.find((candidate) => candidate.id === id);
542
+ if (node === undefined) continue;
543
+ const landed = graphPositionOf({ position: at, parentId: node.parentId }, graph);
544
+ positions[id] = landed;
545
+ single = single === null ? id : "";
546
+ if (node.type === "materialGroup") continue;
547
+ // RECOMPUTED, not read off the hover state: the last `onNodeDrag` fired
548
+ // wherever the pointer last MOVED, and a drop can land somewhere else.
549
+ // The document should agree with where the card actually is.
550
+ const size = node.measured;
551
+ const box = groupUnderPoint(graph, path, [
552
+ landed[0] + (size?.width ?? MATERIAL_CARD_W) / 2,
553
+ landed[1] + (size?.height ?? MATERIAL_CARD_H) / 2,
554
+ ]);
555
+ const before = graph.nodesById[id]?.groupId ?? null;
556
+ if (box !== before) membership[id] = box;
557
+ }
558
+ // ONE CALL for the whole drag: twelve cards moved, whatever they joined,
559
+ // and the wire they were dropped across are one gesture and one undo entry.
560
+ store.dropMaterialNodes(materialId, path, positions, {
561
+ ...(Object.keys(membership).length === 0 ? {} : { membership }),
562
+ ...(splicing !== null && single !== null && single !== ""
563
+ ? { splice: { nodeId: single, edgeId: splicing } }
564
+ : {}),
565
+ });
566
+ }, [draft, spliceEdge, built.nodes, graph, store, materialId, path]);
567
+
568
+ const onConnect = useCallback(
569
+ (connection: Connection) => {
570
+ if (connection.sourceHandle === null || connection.targetHandle === null) return;
571
+ store.connectMaterialSockets(
572
+ materialId,
573
+ path,
574
+ { nodeId: connection.source, socket: connection.sourceHandle },
575
+ { nodeId: connection.target, socket: connection.targetHandle },
576
+ );
577
+ },
578
+ [store, materialId, path],
579
+ );
580
+
581
+ /** Set by `onReconnect`, read by `onReconnectEnd`: React Flow's own way of
582
+ * telling "moved somewhere" from "let go over nothing", which is the
583
+ * gesture that means DISCONNECT. */
584
+ const reconnected = useRef(false);
585
+
586
+ const onConnectEnd = useCallback(
587
+ (event: MouseEvent | TouchEvent, connection: FinalConnectionState) => {
588
+ if (connection.isValid === true || connection.fromHandle?.id == null) return;
589
+ const point = "changedTouches" in event ? event.changedTouches[0] : event;
590
+ if (point === undefined) return;
591
+ const at = flow.screenToFlowPosition({ x: point.clientX, y: point.clientY });
592
+ // A WIRE DROPPED IN EMPTY SPACE IS A QUESTION, not a mistake: "what could
593
+ // take this?". The palette answers it filtered, and `addMaterialNode`
594
+ // draws the wire for whatever is picked.
595
+ store.openMaterialPalette(at, {
596
+ nodeId: connection.fromHandle.nodeId,
597
+ socket: connection.fromHandle.id,
598
+ side: connection.fromHandle.type === "source" ? "source" : "target",
599
+ type: socketTypeOf(connection.fromHandle.nodeId, connection.fromHandle.id, connection.fromHandle.type, built.nodes),
600
+ });
601
+ },
602
+ [flow, store, built.nodes],
603
+ );
604
+
605
+ const onNodeDoubleClick: NodeMouseHandler<MaterialFlowNode> = useCallback(
606
+ (_event, node) => {
607
+ if (node.data.node.kind !== "subgraph") return;
608
+ store.enterMaterialSubgraph(node.id);
609
+ },
610
+ [store],
611
+ );
612
+
613
+ const targetsAt = useCallback(
614
+ (event: { clientX: number; clientY: number }, over: { nodeIds?: string[]; edgeIds?: string[] }) => {
615
+ const at = flow.screenToFlowPosition({ x: event.clientX, y: event.clientY });
616
+ return {
617
+ materialId,
618
+ path,
619
+ nodeIds: over.nodeIds ?? [],
620
+ edgeIds: over.edgeIds ?? [],
621
+ at: { x: at.x, y: at.y },
622
+ };
623
+ },
624
+ [flow, materialId, path],
625
+ );
626
+
627
+ const onNodeContextMenu: NodeMouseHandler<MaterialFlowNode> = useCallback(
628
+ (event, node) => {
629
+ event.preventDefault();
630
+ // The press SELECTS what it landed on unless that was already part of the
631
+ // selection, which is the rule every other menu in this chrome follows:
632
+ // right-clicking outside a selection is a new subject, inside it is the
633
+ // whole of it.
634
+ const nodeIds = selection.nodeIds.includes(node.id) ? [...selection.nodeIds] : [node.id];
635
+ if (!selection.nodeIds.includes(node.id)) {
636
+ store.setMaterialGraphSelection({ nodeIds, edgeIds: [] });
637
+ }
638
+ openMenu({ x: event.clientX, y: event.clientY }, targetsAt(event, { nodeIds }));
639
+ },
640
+ [openMenu, selection.nodeIds, store, targetsAt],
641
+ );
642
+
643
+ const onEdgeContextMenu: EdgeMouseHandler<Edge> = useCallback(
644
+ (event, edge) => {
645
+ event.preventDefault();
646
+ const edgeIds = selection.edgeIds.includes(edge.id) ? [...selection.edgeIds] : [edge.id];
647
+ if (!selection.edgeIds.includes(edge.id)) {
648
+ store.setMaterialGraphSelection({ nodeIds: [], edgeIds });
649
+ }
650
+ openMenu({ x: event.clientX, y: event.clientY }, targetsAt(event, { edgeIds }));
651
+ },
652
+ [openMenu, selection.edgeIds, store, targetsAt],
653
+ );
654
+
655
+ const isValidConnection = useCallback(
656
+ (connection: Connection | Edge): boolean => {
657
+ const sourceHandle = connection.sourceHandle ?? null;
658
+ const targetHandle = connection.targetHandle ?? null;
659
+ if (sourceHandle === null || targetHandle === null) return false;
660
+ return types.canConnect(
661
+ { nodeId: connection.source, socket: sourceHandle },
662
+ { nodeId: connection.target, socket: targetHandle },
663
+ ).ok;
664
+ },
665
+ [types],
666
+ );
667
+
668
+ // Where the palette sits, and where what it adds lands. The request carries a
669
+ // GRAPH point (or null for the keyboard route, which has no pointer to ask),
670
+ // and only this component can turn one into a screen point.
671
+ const paletteAt = useMemo(() => {
672
+ if (palette === null) return null;
673
+ const box = surface.current?.getBoundingClientRect();
674
+ if (palette.at !== null) {
675
+ const screen = flow.flowToScreenPosition(palette.at);
676
+ return {
677
+ screenAt: { x: screen.x - (box?.left ?? 0), y: screen.y - (box?.top ?? 0) },
678
+ graphAt: [palette.at.x, palette.at.y] as const,
679
+ };
680
+ }
681
+ const centre = flow.screenToFlowPosition({
682
+ x: (box?.left ?? 0) + (box?.width ?? 0) / 2,
683
+ y: (box?.top ?? 0) + (box?.height ?? 0) / 2,
684
+ });
685
+ return {
686
+ screenAt: { x: (box?.width ?? 0) / 2, y: (box?.height ?? 0) / 2 },
687
+ graphAt: [centre.x, centre.y] as const,
688
+ };
689
+ }, [palette, flow]);
690
+
691
+ return (
692
+ <section
693
+ className="material-graph-canvas"
694
+ aria-label="Material node graph"
695
+ tabIndex={0}
696
+ ref={surface}
697
+ // FOCUS FOLLOWS THE PRESS, because `state/keymap.ts` decides it is in the
698
+ // graph lane by asking the event TARGET for `.material-graph-canvas`. A
699
+ // canvas nobody has focused sends its keys to `<body>` and the whole lane
700
+ // silently does not exist. Fields and buttons keep their own presses.
701
+ onPointerDownCapture={(event) => {
702
+ const target = event.target;
703
+ if (
704
+ target instanceof Element &&
705
+ target.closest("button, input, textarea, select, [contenteditable='true']") !== null
706
+ ) {
707
+ return;
708
+ }
709
+ event.currentTarget.focus({ preventScroll: true });
710
+ }}
711
+ >
712
+ <MaterialPreviewProvider entries={previews}>
713
+ <MaterialConnectProvider canConnect={canConnect}>
714
+ <ReactFlow<MaterialFlowNode, Edge>
715
+ nodes={nodes}
716
+ edges={edges}
717
+ nodeTypes={NODE_TYPES}
718
+ nodesDraggable={!readOnly}
719
+ nodesConnectable={!readOnly}
720
+ edgesReconnectable={!readOnly}
721
+ onNodesChange={onNodesChange}
722
+ onEdgesChange={onEdgesChange}
723
+ onNodeDrag={onNodeDrag}
724
+ onNodeDragStop={onNodeDragStop}
725
+ onConnect={onConnect}
726
+ onConnectEnd={onConnectEnd}
727
+ isValidConnection={isValidConnection}
728
+ onReconnectStart={() => {
729
+ reconnected.current = false;
730
+ }}
731
+ onReconnect={(oldEdge, connection) => {
732
+ if (connection.sourceHandle === null || connection.targetHandle === null) return;
733
+ reconnected.current = true;
734
+ store.reconnectMaterialEdge(materialId, path, oldEdge.id, {
735
+ source: { nodeId: connection.source, socket: connection.sourceHandle },
736
+ target: { nodeId: connection.target, socket: connection.targetHandle },
737
+ });
738
+ }}
739
+ onReconnectEnd={(_event, edge) => {
740
+ // LET GO OVER NOTHING IS A DISCONNECT. It is the gesture every
741
+ // node editor binds, and without it the only way to remove one
742
+ // wire is to select it and press Delete.
743
+ if (reconnected.current) return;
744
+ store.disconnectMaterialEdges(materialId, path, [edge.id]);
745
+ }}
746
+ onNodeDoubleClick={onNodeDoubleClick}
747
+ onNodeContextMenu={onNodeContextMenu}
748
+ onEdgeContextMenu={onEdgeContextMenu}
749
+ onPaneContextMenu={(event) => {
750
+ event.preventDefault();
751
+ openMenu({ x: event.clientX, y: event.clientY }, targetsAt(event, {}));
752
+ }}
753
+ onDoubleClick={(event) => {
754
+ // The PANE only. A double-click on a card is how a subgraph is
755
+ // entered, and on a field it is how a word is selected.
756
+ if (!(event.target instanceof Element)) return;
757
+ if (event.target.classList.contains("react-flow__pane") !== true) return;
758
+ const at = flow.screenToFlowPosition({ x: event.clientX, y: event.clientY });
759
+ store.openMaterialPalette({ x: at.x, y: at.y });
760
+ }}
761
+ onMoveEnd={(_event, viewport) => {
762
+ rememberNow(viewport);
763
+ askForPreviews();
764
+ }}
765
+ snapToGrid={snapToGrid}
766
+ snapGrid={[MATERIAL_GRAPH_GRID, MATERIAL_GRAPH_GRID]}
767
+ deleteKeyCode={null}
768
+ // SAME ROOT CAUSE AS THE BOARD'S, and here the skipped rung is
769
+ // worse. React Flow's a11y handler clears node selection on Escape
770
+ // synchronously, so `materialEscape` then read an empty selection
771
+ // and took its next rung, which closes the whole workspace. The
772
+ // ladder is meant to drop the selection first.
773
+ disableKeyboardA11y
774
+ // OFF, because a double-click on the pane opens the palette and
775
+ // d3-zoom's own dblclick handler calls `stopImmediatePropagation`
776
+ // — with it on, the native event never reaches React's delegated
777
+ // root and the palette silently never opened.
778
+ zoomOnDoubleClick={false}
779
+ // ── THE POINTER, AS EVERY OTHER CANVAS TOOL SPELLS IT ──────────
780
+ //
781
+ // Left-drag on empty space SELECTS; the middle and right buttons
782
+ // pan. React Flow's default is the other way round, which makes
783
+ // the commonest thing in a node graph — "take those four and move
784
+ // them" — the one gesture behind a modifier, while panning, which
785
+ // an author does with the wheel anyway, gets the whole left button.
786
+ // Figma, Blender's node editor and Unreal all read it this way, and
787
+ // an author arrives here with those in their fingers.
788
+ //
789
+ // Shift-drag still marquees, so the old habit is not punished.
790
+ selectionOnDrag
791
+ // TOUCHING counts, not enclosing. A long wire chain never fits in
792
+ // a rubber band drawn inside the viewport, so "fully contained"
793
+ // would mean zooming out before every multi-select.
794
+ selectionMode={SelectionMode.Partial}
795
+ // MIDDLE BUTTON ONLY, and deliberately not the right one. React
796
+ // Flow's `Pane.onContextMenu` opens with
797
+ // if (Array.isArray(panOnDrag) && panOnDrag.includes(2)) return;
798
+ // — an UNCONDITIONAL bail, not a "did this right press actually
799
+ // pan" test. Listing button 2 here would trade every right-click
800
+ // menu on this canvas (add node here, paste here, snap while
801
+ // dragging) for a second way to do what the middle button and
802
+ // Space already do. The menu is worth more.
803
+ panOnDrag={[1]}
804
+ // Hold-to-pan, and the contract with `state/keymap.ts`: the graph
805
+ // lane returns `null` for Space so nothing calls `preventDefault`
806
+ // and React Flow's own window listener sees the key.
807
+ panActivationKeyCode="Space"
808
+ // A HAND TREMOR IS NOT AN EDIT. The default is 1px, so brushing a
809
+ // card while clicking it committed a `moveMaterialNodes` — a real
810
+ // undo entry that restores a position nobody meant to change and
811
+ // buries the edit before it.
812
+ nodeDragThreshold={4}
813
+ // One socket-row pitch (rows are 24px), up from the 20px default:
814
+ // the closest handle inside the radius wins, so a wire dropped a
815
+ // little short still lands rather than opening the palette.
816
+ connectionRadius={24}
817
+ // …and the same idea at the other end: a click ON a socket is a
818
+ // click, not a one-pixel wire dropped back where it started.
819
+ connectionDragThreshold={3}
820
+ selectionKeyCode="Shift"
821
+ multiSelectionKeyCode={["Meta", "Control"]}
822
+ minZoom={0.15}
823
+ maxZoom={2.5}
824
+ proOptions={{ hideAttribution: true }}
825
+ >
826
+ <Background gap={MATERIAL_GRAPH_GRID} size={1} color="var(--material-grid-dot)" />
827
+ <MiniMap pannable zoomable position="bottom-right" ariaLabel="Material graph overview" />
828
+ </ReactFlow>
829
+ </MaterialConnectProvider>
830
+ </MaterialPreviewProvider>
831
+ {palette === null || paletteAt === null ? null : (
832
+ <MaterialNodePalette
833
+ materialId={materialId}
834
+ path={path}
835
+ request={palette}
836
+ screenAt={paletteAt.screenAt}
837
+ graphAt={paletteAt.graphAt}
838
+ />
839
+ )}
840
+ {menu.menu}
841
+ </section>
842
+ );
843
+ }
844
+
845
+ /**
846
+ * The declared type of the socket a drag started from.
847
+ *
848
+ * Read off the flow nodes rather than the graph, because those already carry
849
+ * the PER-NODE sockets — a subgraph call's are its function's signature, and
850
+ * the static spec would say it has none.
851
+ */
852
+ function socketTypeOf(
853
+ nodeId: string,
854
+ socket: string,
855
+ side: "source" | "target",
856
+ nodes: readonly MaterialFlowNode[],
857
+ ) {
858
+ const node = nodes.find((candidate) => candidate.id === nodeId);
859
+ const list = side === "source" ? node?.data.outputs : node?.data.inputs;
860
+ return list?.find((candidate) => candidate.id === socket)?.type ?? "numeric";
861
+ }