@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,414 @@
1
+ // The material graph's clipboard codec: a selection out, a selection back in.
2
+ //
3
+ // ── WHY A CODEC AND NOT A STRUCTURED CLONE ──────────────────────────────
4
+ //
5
+ // A copied region is not a graph. It has wires whose far end was left behind,
6
+ // it names exposed controls that the graph it lands in may not have, it may
7
+ // reference an image asset that document does not carry, and it may sit inside
8
+ // a function body that only exists in the graph it came from. Handing those
9
+ // nodes to another graph verbatim writes a document `validateMaterialGraph`
10
+ // refuses — and the refusal would arrive at the SAVE, one gesture too late to
11
+ // say what went wrong.
12
+ //
13
+ // So paste is a REWRITE with a report. `remapClip` mints every id fresh,
14
+ // resolves the exposed controls by type, drops what cannot survive the trip,
15
+ // and says what it dropped in sentences the store puts on screen. Nothing here
16
+ // touches the document: the store takes the pieces and builds the one graph it
17
+ // passes through `updateProjectMaterial`'s validated door.
18
+ //
19
+ // ── AND WHY THE TRANSPORT IS TEXT ───────────────────────────────────────
20
+ //
21
+ // `navigator.clipboard` carries strings, which is what lets a copy in one
22
+ // browser tab paste into a graph in another. `decodeClip` is therefore TOTAL
23
+ // over garbage: the string it is handed is whatever the operating system's
24
+ // clipboard happened to hold, and half of the time that is a filename.
25
+ import {
26
+ MATERIAL_GRAPH_EDGE_ID_PREFIX,
27
+ MATERIAL_GRAPH_FUNCTION_ID_PREFIX,
28
+ MATERIAL_GRAPH_NODE_ID_PREFIX,
29
+ MATERIAL_GRAPH_PARAMETER_ID_PREFIX,
30
+ materialGraphNodeSpec,
31
+ } from "@dailies-studio/shot/doc";
32
+ import type {
33
+ MaterialGraph3D,
34
+ MaterialGraphEdge,
35
+ MaterialGraphFunction,
36
+ MaterialGraphNode,
37
+ MaterialGraphParameter,
38
+ ShotAsset,
39
+ } from "@dailies-studio/shot/doc";
40
+
41
+ /** The wrapper string, so a paste can tell one of ours from a filename. */
42
+ export const MATERIAL_CLIP_FORMAT = "dailies.material-graph-clip";
43
+
44
+ /**
45
+ * A copied region, as it crosses the clipboard.
46
+ *
47
+ * `nodes` holds BOTH the copied selection and the bodies of any functions it
48
+ * calls; a body node is the one whose `scope` names a function in `functions`.
49
+ * They share one list because that is how a graph stores them, and keeping the
50
+ * shape the same is what lets `remapClip` walk one array rather than reconcile
51
+ * two.
52
+ */
53
+ export interface MaterialGraphClip {
54
+ format: typeof MATERIAL_CLIP_FORMAT;
55
+ version: 1;
56
+ nodes: MaterialGraphNode[];
57
+ edges: MaterialGraphEdge[];
58
+ parameters: Record<string, MaterialGraphParameter>;
59
+ functions: Record<string, MaterialGraphFunction>;
60
+ }
61
+
62
+ /** What a paste produces: pieces to merge, plus what did not survive. */
63
+ export interface MaterialClipPaste {
64
+ nodes: MaterialGraphNode[];
65
+ edges: MaterialGraphEdge[];
66
+ /** Only the controls the target graph did not already have under a usable
67
+ * id. An empty record is the common case: most pastes reuse. */
68
+ parameters: Record<string, MaterialGraphParameter>;
69
+ /** Only the functions the target graph did not already have. */
70
+ functions: Record<string, MaterialGraphFunction>;
71
+ /** One sentence per thing that was left behind, ready for the error strip. */
72
+ dropped: string[];
73
+ }
74
+
75
+ /**
76
+ * A selection, packed.
77
+ *
78
+ * THREE KINDS OF NODE NEVER TRAVEL. The material output is one per graph, and a
79
+ * second one pasted in is a document the validator refuses; `fn-input` and
80
+ * `fn-output` describe a function's BOUNDARY, and a boundary pasted outside the
81
+ * function it bounds names a socket that is not there. They are skipped rather
82
+ * than refused, because a rubber band that happened to cross the output is a
83
+ * copy of everything else, not a failed copy.
84
+ */
85
+ export function encodeClip(graph: MaterialGraph3D, nodeIds: readonly string[]): MaterialGraphClip {
86
+ const picked = new Map<string, MaterialGraphNode>();
87
+ for (const id of [...new Set(nodeIds)].sort()) {
88
+ const node = graph.nodesById[id];
89
+ if (node === undefined || !copyable(node)) continue;
90
+ picked.set(id, node);
91
+ }
92
+
93
+ // The functions the picked nodes CALL, transitively: a body may itself hold a
94
+ // Subgraph node, and a clip that carried the outer function but not the inner
95
+ // one would paste a call to a function nobody shipped.
96
+ const functions: Record<string, MaterialGraphFunction> = {};
97
+ const bodies = new Map<string, MaterialGraphNode>();
98
+ const pending = [...picked.values()];
99
+ while (pending.length > 0) {
100
+ const node = pending.pop()!;
101
+ if (node.kind !== "subgraph") continue;
102
+ const id = node.functionId;
103
+ if (id === undefined || functions[id] !== undefined) continue;
104
+ const fn = graph.functions?.[id];
105
+ if (fn === undefined) continue;
106
+ functions[id] = fn;
107
+ for (const key of Object.keys(graph.nodesById).sort()) {
108
+ const body = graph.nodesById[key]!;
109
+ if (body.scope !== id || bodies.has(key)) continue;
110
+ bodies.set(key, body);
111
+ pending.push(body);
112
+ }
113
+ }
114
+
115
+ const all = new Map<string, MaterialGraphNode>([...picked, ...bodies]);
116
+ const parameters: Record<string, MaterialGraphParameter> = {};
117
+ for (const node of all.values()) {
118
+ if (node.kind !== "parameter" || node.parameterId === undefined) continue;
119
+ const parameter = graph.parameters[node.parameterId];
120
+ if (parameter !== undefined) parameters[node.parameterId] = parameter;
121
+ }
122
+
123
+ return {
124
+ format: MATERIAL_CLIP_FORMAT,
125
+ version: 1,
126
+ nodes: [...all.values()],
127
+ edges: graph.edges.filter((edge) => all.has(edge.source.nodeId) && all.has(edge.target.nodeId)),
128
+ parameters,
129
+ functions,
130
+ };
131
+ }
132
+
133
+ /** The output node and the two function boundaries never leave their graph. */
134
+ function copyable(node: MaterialGraphNode): boolean {
135
+ if (node.kind === "fn-input" || node.kind === "fn-output") return false;
136
+ return materialGraphNodeSpec(node.kind)?.category !== "output";
137
+ }
138
+
139
+ /**
140
+ * Whatever the clipboard held, as a clip or as null.
141
+ *
142
+ * TOTAL over garbage by construction: it parses inside a try, then checks the
143
+ * wrapper before believing one field of the payload. Everything past the
144
+ * wrapper is coerced to the right SHAPE (an array, a record) rather than
145
+ * validated in detail, because `remapClip` already drops what it cannot use and
146
+ * the store puts the result through `validateMaterialGraph` after that. Two
147
+ * validators for one string would be two places to disagree.
148
+ */
149
+ export function decodeClip(text: string): MaterialGraphClip | null {
150
+ if (typeof text !== "string" || text.length === 0) return null;
151
+ let parsed: unknown;
152
+ try {
153
+ parsed = JSON.parse(text);
154
+ } catch {
155
+ return null;
156
+ }
157
+ const clip = asRecord(parsed);
158
+ if (clip === null || clip["format"] !== MATERIAL_CLIP_FORMAT || clip["version"] !== 1) return null;
159
+ const nodes = Array.isArray(clip["nodes"]) ? clip["nodes"] : [];
160
+ const kept: MaterialGraphNode[] = [];
161
+ for (const raw of nodes) {
162
+ const node = asRecord(raw);
163
+ if (node === null) continue;
164
+ if (typeof node["id"] !== "string" || typeof node["kind"] !== "string") continue;
165
+ if (!finitePair(node["position"])) continue;
166
+ if (materialGraphNodeSpec(node["kind"]) === null) continue;
167
+ kept.push(node as unknown as MaterialGraphNode);
168
+ }
169
+ const edges = (Array.isArray(clip["edges"]) ? clip["edges"] : []).filter(isEdge);
170
+ return {
171
+ format: MATERIAL_CLIP_FORMAT,
172
+ version: 1,
173
+ nodes: kept,
174
+ edges,
175
+ parameters: recordOfShape(clip["parameters"], isParameter),
176
+ functions: recordOfShape(clip["functions"], isFunction),
177
+ };
178
+ }
179
+
180
+ /** How a paste's node ids and control ids are minted. The store owns the
181
+ * source, so a replayed undo mints the same ids the first run did. */
182
+ export interface MaterialClipRemapOptions {
183
+ /** The function body the paste lands in, or undefined for the main scope. */
184
+ scope?: string | undefined;
185
+ /** The document's assets, so a texture node with nowhere to read from is
186
+ * dropped here rather than refused at the save. Absent skips the check. */
187
+ assets?: Readonly<Record<string, ShotAsset>> | undefined;
188
+ /** Added to every top-level node's position, so a paste lands where the
189
+ * author asked rather than exactly on top of what they copied. */
190
+ offset?: readonly [number, number] | undefined;
191
+ }
192
+
193
+ /**
194
+ * A clip, rewritten to fit the graph it is landing in.
195
+ *
196
+ * FOUR RESOLUTIONS, and each is the one that keeps the paste honest:
197
+ *
198
+ * IDS are all fresh, so pasting into the graph a clip came from is a copy
199
+ * rather than a no-op that silently overwrote the original.
200
+ * A CONTROL the target already has under the same id is REUSED when the type
201
+ * agrees and re-keyed when it does not. Reuse is what an author means by
202
+ * pasting a Roughness node back into the material it came from; re-keying
203
+ * is what stops a float control from being silently redefined as a colour.
204
+ * A FUNCTION the target already has is reused whole, body and all, and the
205
+ * clip's copy of it is thrown away — otherwise every paste of one call site
206
+ * would fork the function it calls.
207
+ * ANYTHING THAT CANNOT LAND is dropped and NAMED. A texture with no asset, a
208
+ * Subgraph whose function travelled in neither the clip nor the target, a
209
+ * group box that was outside the rubber band.
210
+ */
211
+ export function remapClip(
212
+ clip: MaterialGraphClip,
213
+ graph: MaterialGraph3D,
214
+ mint: (prefix: string) => string,
215
+ options: MaterialClipRemapOptions = {},
216
+ ): MaterialClipPaste {
217
+ const dropped: string[] = [];
218
+
219
+ // 1. A function the target already has is REUSED; only the ones it does not
220
+ // have are minted, and only those carry their bodies across.
221
+ const fnId = new Map<string, string>();
222
+ const reusedFn = new Set<string>();
223
+ for (const id of Object.keys(clip.functions).sort()) {
224
+ if (graph.functions?.[id] !== undefined) {
225
+ fnId.set(id, id);
226
+ reusedFn.add(id);
227
+ } else fnId.set(id, mint(MATERIAL_GRAPH_FUNCTION_ID_PREFIX));
228
+ }
229
+
230
+ // 2. Which nodes survive. A body node of a reused function is dropped in
231
+ // silence — the target already holds an identical one — while everything
232
+ // else that cannot land says so.
233
+ const survives = new Map<string, MaterialGraphNode>();
234
+ for (const node of clip.nodes) {
235
+ if (node.scope !== undefined && reusedFn.has(node.scope)) continue;
236
+ const spec = materialGraphNodeSpec(node.kind);
237
+ if (spec === null) continue;
238
+ if (spec.fields.includes("assetId") && options.assets !== undefined) {
239
+ const asset = node.assetId === undefined ? undefined : options.assets[node.assetId];
240
+ if (asset === undefined || asset.kind !== "image") {
241
+ dropped.push(`${spec.label} was left out: this document has no image asset "${node.assetId ?? ""}".`);
242
+ continue;
243
+ }
244
+ }
245
+ if (node.kind === "subgraph") {
246
+ const target = node.functionId;
247
+ const known = target !== undefined && (fnId.has(target) || graph.functions?.[target] !== undefined);
248
+ if (!known) {
249
+ dropped.push(`A Subgraph node was left out: the function it calls did not come with it.`);
250
+ continue;
251
+ }
252
+ }
253
+ survives.set(node.id, node);
254
+ }
255
+
256
+ // 3. Mint the new ids, now that the surviving set is known.
257
+ const nodeId = new Map<string, string>();
258
+ for (const id of [...survives.keys()].sort()) nodeId.set(id, mint(MATERIAL_GRAPH_NODE_ID_PREFIX));
259
+
260
+ // 4. Exposed controls, resolved by type against the graph the paste lands in.
261
+ const paramId = new Map<string, string>();
262
+ const parameters: Record<string, MaterialGraphParameter> = {};
263
+ for (const id of Object.keys(clip.parameters).sort()) {
264
+ const parameter = clip.parameters[id]!;
265
+ const existing = graph.parameters[id];
266
+ if (existing === undefined) {
267
+ paramId.set(id, id);
268
+ parameters[id] = parameter;
269
+ continue;
270
+ }
271
+ if (existing.type === parameter.type) {
272
+ paramId.set(id, id);
273
+ continue;
274
+ }
275
+ const fresh = mint(MATERIAL_GRAPH_PARAMETER_ID_PREFIX);
276
+ paramId.set(id, fresh);
277
+ parameters[fresh] = { ...parameter, id: fresh };
278
+ dropped.push(
279
+ `"${parameter.label}" was pasted as a second control: this material already has a ${existing.type} control under that name.`,
280
+ );
281
+ }
282
+
283
+ const offset = options.offset ?? [0, 0];
284
+ const nodes: MaterialGraphNode[] = [];
285
+ for (const id of [...survives.keys()].sort()) {
286
+ const node = survives.get(id)!;
287
+ const body = node.scope !== undefined && fnId.has(node.scope);
288
+ const scope = body ? fnId.get(node.scope!) : options.scope;
289
+ // A GROUP THAT DID NOT TRAVEL leaves its members loose rather than pointing
290
+ // at a box the paste never created — which the validator would refuse.
291
+ const group = node.groupId === undefined ? undefined : nodeId.get(node.groupId);
292
+ if (node.groupId !== undefined && group === undefined) {
293
+ dropped.push(`One node was pasted outside its group: the group box was not part of the copy.`);
294
+ }
295
+ const position: [number, number] = body
296
+ ? [node.position[0], node.position[1]]
297
+ : [node.position[0] + offset[0], node.position[1] + offset[1]];
298
+ const next: MaterialGraphNode = {
299
+ ...node,
300
+ id: nodeId.get(id)!,
301
+ position,
302
+ ...(scope === undefined ? {} : { scope }),
303
+ ...(group === undefined ? {} : { groupId: group }),
304
+ ...(node.parameterId === undefined ? {} : { parameterId: paramId.get(node.parameterId) ?? node.parameterId }),
305
+ ...(node.functionId === undefined ? {} : { functionId: fnId.get(node.functionId) ?? node.functionId }),
306
+ };
307
+ if (scope === undefined) delete next.scope;
308
+ if (group === undefined) delete next.groupId;
309
+ nodes.push(next);
310
+ }
311
+
312
+ const edges: MaterialGraphEdge[] = [];
313
+ for (const edge of clip.edges) {
314
+ const source = nodeId.get(edge.source.nodeId);
315
+ const target = nodeId.get(edge.target.nodeId);
316
+ if (source === undefined || target === undefined) continue;
317
+ edges.push({
318
+ id: mint(MATERIAL_GRAPH_EDGE_ID_PREFIX),
319
+ source: { nodeId: source, socket: edge.source.socket },
320
+ target: { nodeId: target, socket: edge.target.socket },
321
+ });
322
+ }
323
+
324
+ const functions: Record<string, MaterialGraphFunction> = {};
325
+ for (const [id, fn] of Object.entries(clip.functions)) {
326
+ if (reusedFn.has(id)) continue;
327
+ const fresh = fnId.get(id)!;
328
+ const outputNodeId = nodeId.get(fn.outputNodeId);
329
+ if (outputNodeId === undefined) {
330
+ dropped.push(`Function "${fn.name}" was left out: its Function Output node did not come with it.`);
331
+ continue;
332
+ }
333
+ functions[fresh] = { ...fn, id: fresh, outputNodeId, inputs: fn.inputs.map((input) => ({ ...input })) };
334
+ }
335
+ // A function that lost its output takes its call sites with it, or the paste
336
+ // would land a call to a function the graph does not have.
337
+ const orphan = new Set(
338
+ [...fnId.entries()]
339
+ .filter(([id, fresh]) => !reusedFn.has(id) && functions[fresh] === undefined)
340
+ .map(([, fresh]) => fresh),
341
+ );
342
+ if (orphan.size > 0) {
343
+ const dead = new Set(
344
+ nodes.filter((node) => node.functionId !== undefined && orphan.has(node.functionId)).map((n) => n.id),
345
+ );
346
+ return {
347
+ nodes: nodes.filter((node) => !dead.has(node.id) && !(node.scope !== undefined && orphan.has(node.scope))),
348
+ edges: edges.filter((edge) => !dead.has(edge.source.nodeId) && !dead.has(edge.target.nodeId)),
349
+ parameters,
350
+ functions,
351
+ // DEDUPED, because a group box left behind is one fact however many of
352
+ // its members noticed. Twelve copies of one sentence in the error strip
353
+ // reads as twelve problems.
354
+ dropped: [...new Set(dropped)],
355
+ };
356
+ }
357
+
358
+ return { nodes, edges, parameters, functions, dropped: [...new Set(dropped)] };
359
+ }
360
+
361
+ function asRecord(value: unknown): Record<string, unknown> | null {
362
+ return typeof value === "object" && value !== null && !Array.isArray(value)
363
+ ? (value as Record<string, unknown>)
364
+ : null;
365
+ }
366
+
367
+ function finitePair(value: unknown): boolean {
368
+ return (
369
+ Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number" && Number.isFinite(n))
370
+ );
371
+ }
372
+
373
+ function isEdge(value: unknown): value is MaterialGraphEdge {
374
+ const edge = asRecord(value);
375
+ if (edge === null || typeof edge["id"] !== "string") return false;
376
+ return isEndpoint(edge["source"]) && isEndpoint(edge["target"]);
377
+ }
378
+
379
+ function isEndpoint(value: unknown): boolean {
380
+ const end = asRecord(value);
381
+ return end !== null && typeof end["nodeId"] === "string" && typeof end["socket"] === "string";
382
+ }
383
+
384
+ function isParameter(value: unknown): value is MaterialGraphParameter {
385
+ const parameter = asRecord(value);
386
+ return (
387
+ parameter !== null &&
388
+ typeof parameter["id"] === "string" &&
389
+ typeof parameter["label"] === "string" &&
390
+ (["float", "color", "vec2", "vec3"] as unknown[]).includes(parameter["type"])
391
+ );
392
+ }
393
+
394
+ function isFunction(value: unknown): value is MaterialGraphFunction {
395
+ const fn = asRecord(value);
396
+ return (
397
+ fn !== null &&
398
+ typeof fn["id"] === "string" &&
399
+ typeof fn["name"] === "string" &&
400
+ typeof fn["outputNodeId"] === "string" &&
401
+ Array.isArray(fn["inputs"]) &&
402
+ asRecord(fn["output"]) !== null
403
+ );
404
+ }
405
+
406
+ function recordOfShape<T>(value: unknown, is: (candidate: unknown) => candidate is T): Record<string, T> {
407
+ const record = asRecord(value);
408
+ if (record === null) return {};
409
+ const out: Record<string, T> = {};
410
+ for (const [key, candidate] of Object.entries(record)) {
411
+ if (is(candidate)) out[key] = candidate;
412
+ }
413
+ return out;
414
+ }
@@ -0,0 +1,216 @@
1
+ // SMALL PICTURES OF PROJECT IMAGES: a tile that shows an image at 100px
2
+ // should not decode and hold the 4000px original. Each image is fetched once,
3
+ // decoded with `createImageBitmap`, resized, drawn to a canvas and kept as a
4
+ // webp object URL in this page. Never stored as files.
5
+ //
6
+ // KEYED ON THE ASSET'S CONTENT HASH AND A SIZE BUCKET (a power of two from
7
+ // `IMAGE_THUMB_MIN_PX`), so the board, the shot board and the Assets tab share
8
+ // one picture per image and size, and a re-upload under the same name (a new
9
+ // hash) asks for a new one.
10
+ //
11
+ // THE ORIGINAL WHEN THERE IS NO GAIN OR NO WAY: no `createImageBitmap` (a
12
+ // suite, an old browser), an image already within the bucket, a bucket past
13
+ // `IMAGE_THUMB_MAX_PX`, or a decode that fails (some browsers refuse an SVG
14
+ // blob). A thumbnail is decoration: a failure is never a thrown error.
15
+ //
16
+ // ONE DECODE AT A TIME, so a board of fifty pictures never holds fifty
17
+ // full-size bitmaps at once.
18
+ // ponytail: a fixed LRU of 128 pictures; size it from the views if a board
19
+ // shows more than that at once.
20
+ import { useState, useSyncExternalStore } from "react";
21
+
22
+ /** The smallest bucket: a 20px pinned-row thumb still asks for 64. */
23
+ export const IMAGE_THUMB_MIN_PX = 64;
24
+ /** Past this the original is shown: a thumbnail this large saves little. */
25
+ export const IMAGE_THUMB_MAX_PX = 2048;
26
+ export const IMAGE_THUMB_ENTRIES = 128;
27
+
28
+ /** The bucket a longest side of `px` asks for: the next power of two, at
29
+ * least `IMAGE_THUMB_MIN_PX`. Null past `IMAGE_THUMB_MAX_PX`. */
30
+ export function thumbBucket(px: number): number | null {
31
+ const want = Math.max(IMAGE_THUMB_MIN_PX, Math.ceil(Number.isFinite(px) ? px : IMAGE_THUMB_MAX_PX + 1));
32
+ const bucket = 2 ** Math.ceil(Math.log2(want));
33
+ return bucket > IMAGE_THUMB_MAX_PX ? null : bucket;
34
+ }
35
+
36
+ /** What a thumbnail answers: a picture to show, or null while it is drawn. */
37
+ export type ImageThumb = string | null;
38
+
39
+ export interface ImageThumbsDeps {
40
+ /** False skips every draw and answers the original. Default: whether
41
+ * `createImageBitmap` and a canvas exist. */
42
+ supported?: boolean;
43
+ /** The image's bytes. Default `fetch`. */
44
+ load?: (url: string) => Promise<Blob>;
45
+ /** The resized picture whose longest side is at most `bucket`, or null when
46
+ * the image is already that small. Default `resizeToWebp`. */
47
+ resize?: (blob: Blob, bucket: number) => Promise<Blob | null>;
48
+ createObjectURL?: (blob: Blob) => string;
49
+ revokeObjectURL?: (url: string) => void;
50
+ maxEntries?: number;
51
+ }
52
+
53
+ export interface ImageThumbs {
54
+ /** The picture of `url` (content `hash`) for a longest side of `maxPx`:
55
+ * the thumbnail, the original when there is no gain or no way, or null
56
+ * while it is drawn. Queues a draw when it has none. */
57
+ thumb(url: string, hash: string, maxPx: number): ImageThumb;
58
+ /** Called after each change to what `thumb` answers. Returns the
59
+ * unsubscribe. */
60
+ subscribe(listener: () => void): () => void;
61
+ dispose(): void;
62
+ }
63
+
64
+ interface Entry {
65
+ readonly url: string;
66
+ /** The thumbnail's object URL, or the original's URL; absent while drawn. */
67
+ shown?: string;
68
+ /** True when `shown` is an object URL this cache made and must revoke. */
69
+ owned?: boolean;
70
+ }
71
+
72
+ export function createImageThumbs(deps: ImageThumbsDeps = {}): ImageThumbs {
73
+ const supported =
74
+ deps.supported ?? (typeof createImageBitmap === "function" && (typeof OffscreenCanvas === "function" || typeof document !== "undefined"));
75
+ const load = deps.load ?? ((url: string) => fetch(url).then((r) => (r.ok ? r.blob() : Promise.reject(new Error(`${r.status}`)))));
76
+ const resize = deps.resize ?? resizeToWebp;
77
+ const createUrl = deps.createObjectURL ?? ((blob: Blob) => URL.createObjectURL(blob));
78
+ const revokeUrl = deps.revokeObjectURL ?? ((url: string) => URL.revokeObjectURL(url));
79
+ const maxEntries = deps.maxEntries ?? IMAGE_THUMB_ENTRIES;
80
+
81
+ /** Insertion order is recency: `thumb` moves an entry to the end. */
82
+ const entries = new Map<string, Entry>();
83
+ const listeners = new Set<() => void>();
84
+ /** The one decode chain. */
85
+ let chain: Promise<void> = Promise.resolve();
86
+ let disposed = false;
87
+
88
+ const notify = (): void => {
89
+ for (const listener of [...listeners]) listener();
90
+ };
91
+ const drop = (entry: Entry): void => {
92
+ if (entry.owned === true && entry.shown !== undefined) revokeUrl(entry.shown);
93
+ };
94
+ const evict = (): void => {
95
+ for (const [key, entry] of entries) {
96
+ if (entries.size <= maxEntries) return;
97
+ entries.delete(key);
98
+ drop(entry);
99
+ }
100
+ };
101
+
102
+ const draw = (key: string, entry: Entry, bucket: number): void => {
103
+ chain = chain.then(async () => {
104
+ if (disposed || entries.get(key) !== entry) return;
105
+ let shown = entry.url;
106
+ let owned = false;
107
+ try {
108
+ const small = await resize(await load(entry.url), bucket);
109
+ if (small !== null) {
110
+ shown = createUrl(small);
111
+ owned = true;
112
+ }
113
+ } catch {
114
+ // The original stands in.
115
+ }
116
+ if (disposed || entries.get(key) !== entry) {
117
+ if (owned) revokeUrl(shown);
118
+ return;
119
+ }
120
+ entry.shown = shown;
121
+ if (owned) entry.owned = true;
122
+ notify();
123
+ })
124
+ // A link that threw (a listener, say) must not stop every later draw:
125
+ // `then` on a rejected chain runs nothing.
126
+ .catch(() => {});
127
+ };
128
+
129
+ return {
130
+ thumb(url, hash, maxPx) {
131
+ const bucket = thumbBucket(maxPx);
132
+ if (!supported || bucket === null || hash === "") return url;
133
+ const key = `${hash}@${bucket}`;
134
+ // The same bytes under a new URL (a rename) keep their picture.
135
+ let entry = entries.get(key);
136
+ if (entry === undefined) {
137
+ entry = { url };
138
+ entries.set(key, entry);
139
+ evict();
140
+ draw(key, entry, bucket);
141
+ } else {
142
+ entries.delete(key);
143
+ entries.set(key, entry);
144
+ }
145
+ return entry.shown ?? null;
146
+ },
147
+ subscribe(listener) {
148
+ listeners.add(listener);
149
+ return () => listeners.delete(listener);
150
+ },
151
+ dispose() {
152
+ disposed = true;
153
+ for (const entry of entries.values()) drop(entry);
154
+ entries.clear();
155
+ listeners.clear();
156
+ },
157
+ };
158
+ }
159
+
160
+ /**
161
+ * The browser's resize: decode, scale the longest side to `bucket` with a
162
+ * high-quality resample, draw to a canvas and encode webp. Null when the image
163
+ * is already within the bucket, so the original shows.
164
+ */
165
+ export async function resizeToWebp(blob: Blob, bucket: number): Promise<Blob | null> {
166
+ const full = await createImageBitmap(blob);
167
+ try {
168
+ const longest = Math.max(full.width, full.height);
169
+ if (longest <= bucket) return null;
170
+ const width = Math.max(1, Math.round((full.width * bucket) / longest));
171
+ const height = Math.max(1, Math.round((full.height * bucket) / longest));
172
+ const small = await createImageBitmap(full, { resizeWidth: width, resizeHeight: height, resizeQuality: "high" });
173
+ try {
174
+ if (typeof OffscreenCanvas === "function") {
175
+ const canvas = new OffscreenCanvas(width, height);
176
+ canvas.getContext("2d")?.drawImage(small, 0, 0);
177
+ return await canvas.convertToBlob({ type: "image/webp", quality: 0.85 });
178
+ }
179
+ const canvas = document.createElement("canvas");
180
+ canvas.width = width;
181
+ canvas.height = height;
182
+ canvas.getContext("2d")?.drawImage(small, 0, 0);
183
+ return await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, "image/webp", 0.85));
184
+ } finally {
185
+ small.close();
186
+ }
187
+ } finally {
188
+ full.close();
189
+ }
190
+ }
191
+
192
+ let shared: ImageThumbs | null = null;
193
+ function sharedThumbs(): ImageThumbs {
194
+ shared ??= createImageThumbs();
195
+ return shared;
196
+ }
197
+
198
+ /** For the suite: forget every picture and start a fresh cache. */
199
+ export function resetImageThumbs(next?: ImageThumbs): void {
200
+ shared?.dispose();
201
+ shared = next ?? null;
202
+ }
203
+
204
+ /**
205
+ * The picture to show for an image of content `hash` at `url`, in a box whose
206
+ * longest side is `maxPx` CSS px (scaled by the device pixel ratio here). The
207
+ * original where there is no thumbnail to make, null while one is drawn.
208
+ */
209
+ export function useImageThumb(url: string, hash: string, maxPx: number): ImageThumb {
210
+ const [dpr] = useState(() => (typeof window === "undefined" ? 1 : window.devicePixelRatio || 1));
211
+ const thumbs = sharedThumbs();
212
+ const px = Math.ceil(maxPx * dpr);
213
+ // `thumb` queues the draw on its first ask and answers the same value until
214
+ // the draw lands and notifies, which is what a snapshot must do.
215
+ return useSyncExternalStore(thumbs.subscribe, () => thumbs.thumb(url, hash, px));
216
+ }