@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
@@ -2,9 +2,13 @@
2
2
  //
3
3
  // Mounted INSIDE `ShotRuntime`'s canvas, and only while a second pane is on
4
4
  // screen. `renderControl: "external"` is what makes that legal — see the
5
- // contract in packages/shot/src/ShotRuntime.tsx. `useFrame(…, 1)` is what hands
6
- // the present over: r3f skips its own render whenever any subscriber has a
7
- // priority above zero, which is the identical mechanism `PostPipeline` uses.
5
+ // contract in packages/shot/src/ShotRuntime.tsx. `useFrame(…, { phase:
6
+ // "render" })` is what hands the present over: fiber's own render job stands
7
+ // down whenever a user job occupies the render phase (`hasUserJobsInPhase`),
8
+ // the same mechanism ScenePresenter uses. The phase also guarantees the
9
+ // update-phase subscribers (camera write, user `useFrame` code) ran first —
10
+ // the v9 ordering that a legacy `useFrame(cb, 1)` no longer gives, because
11
+ // the v10 scheduler sorts priority DESCENDING within a phase.
8
12
  //
9
13
  // ── WHY THE CAMERA PANE'S POST GOES THROUGH A RENDER TARGET ──────────────
10
14
  //
@@ -78,82 +82,171 @@
78
82
  //
79
83
  // ── BINS ─────────────────────────────────────────────────────────────────
80
84
  //
81
- // A scene with a `mesh` layer draws in several passes (packages/shot/bins.ts).
82
- // EVERY render here goes through `renderBins` or a `SceneSource` rather than
83
- // `gl.render` / `pass()` directly, so both panes agree with each other and with
84
- // the export about what covers what. `renderBins` degenerates to exactly one
85
+ // A scene with a `mesh` layer draws in several passes, and a mask run draws
86
+ // as a nested matte (packages/shot/bins.ts). EVERY render here goes through
87
+ // `renderSteps` or a `SceneSource` rather than `gl.render` / `pass()`
88
+ // directly, so both panes agree with each other and with the export about what
89
+ // covers what. `renderSteps` degenerates to exactly one
85
90
  // `gl.render` when there is nothing to split, so a scene with no model takes
86
91
  // the identical call this file always made.
87
92
  //
93
+ // ── THE PLAYBACK RENDER SCALE REACHES THE CAMERA PANE ALONE ──────────────
94
+ //
95
+ // While the transport plays and the stage's controller answers a step below 1
96
+ // (`presentationScale`, packages/shot/src/presentationScale.ts), the camera
97
+ // pane's offscreen draws (the transition capture, the post chain, and for a
98
+ // Shot with no post a plain pane target) run inside `withPresentationScale`,
99
+ // so every target they size from the renderer comes out scaled, and the blit
100
+ // into the pane upscales them. The working pane and its furniture keep the
101
+ // canvas's density. At 1 every draw is the one this file always made.
102
+ //
88
103
  // ── NOTHING IS ALLOCATED PER FRAME ───────────────────────────────────────
89
104
  //
90
- // The rig, the clay, the blit quad and the scratch colour are all hoisted. The
91
- // hot path is scissor rects and one uniform write.
92
- import { useEffect, useMemo } from "react";
93
- import { useFrame, useThree } from "@react-three/fiber";
105
+ // The rig, appearance-keyed clay, blit quad and scratch colour are all stable
106
+ // between changes. The hot path is scissor rects and one uniform write.
107
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
108
+ import { useFrame, useStore, useThree } from "@react-three/fiber";
94
109
  import { Color, ColorManagement, HalfFloatType, Layers, RenderTarget } from "three";
110
+ import { isInsideOutShell } from "./sceneExtent.js";
95
111
  import { MeshBasicNodeMaterial, QuadMesh, RenderPipeline } from "three/webgpu";
96
- import type { ColorSpace } from "three";
112
+ import type { Camera, ColorSpace, Material, Object3D, Scene } from "three";
97
113
  import type { WebGPURenderer } from "three/webgpu";
98
114
  import { texture, uv } from "three/tsl";
99
115
  import {
116
+ PRESENTER_RIGS_MAX,
117
+ PresentationSchedule,
100
118
  binSceneSource,
101
119
  buildPostChain,
120
+ compileObjectForTarget,
121
+ compileRenderPipelineForTarget,
122
+ devicePixels,
123
+ drawScaleOf,
102
124
  hasPostChain,
103
125
  passSceneSource,
126
+ pendingFrameError,
127
+ pieceOwner,
104
128
  postSignature,
105
- renderBins,
129
+ presenterShapesOf,
130
+ registerSupplementalFrameCompile,
131
+ renderSteps,
106
132
  transitionTap,
133
+ usePieceFailures,
134
+ withPresentationScale,
107
135
  } from "@dailies-studio/shot";
108
- import type { RenderBin, SceneEffects } from "@dailies-studio/shot/doc";
136
+ import type { PieceFailureInput, PresentationScaleSource, RenderStep, SceneEffects, SceneSource } from "@dailies-studio/shot";
137
+ import { componentDefsOfCached, isCompositeStep } from "@dailies-studio/shot/doc";
138
+ import type { Doc3D } from "@dailies-studio/shot/doc";
109
139
  import type { StageFeed } from "./feed.js";
110
140
  import type { PaneRect } from "./panes.js";
111
141
  import { workingDrawPlan } from "./shading.js";
112
142
  import type { ShadingMode } from "./shading.js";
113
143
  import { EDITOR_LAYER } from "./StageTools.js";
144
+ import { tokenColor } from "./tokenColor.js";
145
+
146
+ /** A CSS colour as a three `Color`, or `fallback` when three cannot parse it. */
147
+ function tokenColorOf(css: string, fallback: Color): Color {
148
+ try {
149
+ return new Color(css);
150
+ } catch {
151
+ return fallback;
152
+ }
153
+ }
154
+ import type { StageTheme } from "./stageTheme.js";
114
155
 
115
156
  /**
116
157
  * The working pane's backdrop and its clay.
117
158
  *
118
- * NOT tokens. These are a GL clear colour and a material colour, so they cannot
119
- * read the CSS custom properties the overlays use; they are picked to sit just
120
- * off `--panel` so the pane reads as a workspace rather than as the shot.
159
+ * They are GL colours, so each is resolved from a CONCRETE CSS token when the
160
+ * appearance changes. The camera pane never reads them: it keeps the shot's
161
+ * authored clear colour and remains identical to export.
121
162
  */
122
- export const CLAY_BACKDROP = new Color(0x141417);
123
- const CLAY_FILL = new Color(0x8a8a93);
124
- /** The wireframe's line colour. Brighter than the clay so edges read as lines
125
- * on the same backdrop rather than as a dimmer clay. */
126
- const WIRE_FILL = new Color(0xb8b8c2);
127
-
128
163
  /** Scratch for saving the shot's own clear colour across the working pane's. */
129
164
  const _prevClear = new Color();
165
+ const NORMAL_WORKING_COMPILE_MODE = {};
166
+ /** No steps: `renderSteps`' single plain render. */
167
+ const NO_STEPS: readonly RenderStep[] = Object.freeze([]);
130
168
 
131
169
  export interface SplitRendererProps {
170
+ /** Resolved editor theme. It rebuilds only the working-view palette. */
171
+ theme: StageTheme;
132
172
  feed: StageFeed;
173
+ /** The document on stage: how many post shapes its Shots use sizes the rig
174
+ * cache. */
175
+ doc: Doc3D;
133
176
  panes: { working: PaneRect | null; camera: PaneRect | null };
134
177
  /** The active scene's effects, or null when nothing is on screen. */
135
178
  effects: SceneEffects | null;
136
179
  /** The active scene's render bins at this frame. Empty or single = one draw,
137
180
  * which is every scene without a live mesh layer. */
138
- bins: readonly RenderBin[];
139
- /** Whether the scene contains a mesh layer AT ALL. Static (lifetimes
140
- * ignored), because it picks the post chain's SHAPE — see ShotRuntime. */
141
- hasMesh: boolean;
181
+ bins: readonly RenderStep[];
182
+ /** Whether the scene contains a mesh or a mask layer AT ALL
183
+ * (`sceneNeedsBins`). Static (lifetimes ignored), because it picks the post
184
+ * chain's SHAPE — see ShotRuntime. */
185
+ needsBins: boolean;
142
186
  /** MASTER frame — the grain's seed, and the only clock the chain sees. */
143
187
  frame: number;
188
+ /** The frame-exact readers (StageApp's `playhead`): the post values, the
189
+ * bins and the master frame at DRAW time. `effects`, `bins` and `frame`
190
+ * above are the render's snapshot, which is right about the post chain's
191
+ * SHAPE and the bins' kind and may be a frame behind on their values;
192
+ * absent, the snapshot is the frame (the test harness). */
193
+ live?: {
194
+ at(): { masterFrame: number; effects: SceneEffects | null; bins: readonly RenderStep[]; sceneId?: string | null };
195
+ };
144
196
  /** How the WORKING pane draws. The camera pane is not addressable: it is
145
197
  * always the graded render, whatever this says. */
146
198
  shading: ShadingMode;
199
+ /** The Shot the effects belong to, for a post chain that fails to build or
200
+ * to draw: it is reported as that Shot's post stack (pieceFailures.ts). */
201
+ sceneId?: string | null;
202
+ /** The component preview's backdrop (bridge v45), a CSS colour the working
203
+ * pane clears with in place of `--working-bg`. Absent or null = the
204
+ * token. */
205
+ backdrop?: string | null;
206
+ /** The playback render scale's source (the stage's controller), asked once
207
+ * per played frame for the camera pane: see the header. Absent, nothing
208
+ * scales. */
209
+ presentationScale?: PresentationScaleSource;
210
+ /** The transport plays and no preparing pass draws: the only frames that
211
+ * scale. */
212
+ playing?: boolean;
147
213
  }
148
214
 
149
- export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shading }: SplitRendererProps): null {
215
+ export function SplitRenderer({
216
+ theme,
217
+ feed,
218
+ doc,
219
+ panes,
220
+ effects,
221
+ bins: snapshotBins,
222
+ needsBins,
223
+ frame,
224
+ live,
225
+ shading,
226
+ sceneId = null,
227
+ backdrop = null,
228
+ presentationScale,
229
+ playing = false,
230
+ }: SplitRendererProps): null {
150
231
  // The runtime class IS `WebGPURenderer` (createStageGl builds one, WebGL2 is
151
232
  // a BACKEND of it, not a different class); r3f types `gl` as the classic
152
233
  // renderer, so the cast happens once here rather than at five call sites.
153
234
  const gl = useThree((s) => s.gl) as unknown as WebGPURenderer;
235
+ const store = useStore();
154
236
  const scene = useThree((s) => s.scene);
155
237
  const shotCamera = useThree((s) => s.camera);
156
238
  const size = useThree((s) => s.size);
239
+ const workingPalette = useMemo(
240
+ () => ({
241
+ backdrop: tokenColor("--working-bg", "#141414"),
242
+ clay: tokenColor("--working-clay", "#8a8a93"),
243
+ wire: tokenColor("--working-wire", "#b8b8c2"),
244
+ }),
245
+ [theme],
246
+ );
247
+ // The preview's backdrop over the token, or the token when the colour is
248
+ // not one three can parse (`tokenColor`'s rule).
249
+ const clearBackdrop = useMemo(() => (backdrop === null ? workingPalette.backdrop : tokenColorOf(backdrop, workingPalette.backdrop)), [backdrop, workingPalette]);
157
250
 
158
251
  /**
159
252
  * The clay override, ONE dedicated instance and never shared.
@@ -161,7 +254,8 @@ export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shad
161
254
  * `depthTest`/`depthWrite` stay OFF, matching every layer material. It is
162
255
  * tempting to turn them on "because the working pane is the 3D one", and it
163
256
  * would be a second truth: this lane's paint order is `renderOrder =
164
- * layer.order` and nothing else (see ShotRuntime's header), so a depth-tested
257
+ * layer.order`, the layer lists' walk, and nothing else (see ShotRuntime's
258
+ * header), so a depth-tested
165
259
  * working pane would disagree with the camera pane about what covers what —
166
260
  * and, because layers default to z = 0, would z-fight the common case.
167
261
  *
@@ -172,7 +266,7 @@ export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shad
172
266
  */
173
267
  const overrides = useMemo(() => {
174
268
  const clay = new MeshBasicNodeMaterial();
175
- clay.color.copy(CLAY_FILL);
269
+ clay.color.copy(workingPalette.clay);
176
270
  clay.depthTest = false;
177
271
  clay.depthWrite = false;
178
272
  clay.toneMapped = false;
@@ -180,13 +274,13 @@ export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shad
180
274
  // depth/tone rationale above holds unchanged, because a wireframe pane's
181
275
  // paint order is still `renderOrder` and nothing else.
182
276
  const wire = new MeshBasicNodeMaterial();
183
- wire.color.copy(WIRE_FILL);
277
+ wire.color.copy(workingPalette.wire);
184
278
  wire.wireframe = true;
185
279
  wire.depthTest = false;
186
280
  wire.depthWrite = false;
187
281
  wire.toneMapped = false;
188
282
  return { clay, wire };
189
- }, []);
283
+ }, [workingPalette]);
190
284
  useEffect(
191
285
  () => () => {
192
286
  overrides.clay.dispose();
@@ -195,66 +289,88 @@ export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shad
195
289
  [overrides],
196
290
  );
197
291
 
198
- const signature = effects === null ? "" : `${postSignature(effects)}::${hasMesh}`;
199
- const post = useMemo(() => {
200
- if (effects === null || !hasPostChain(effects)) return null;
201
- // Layer 0 only. This source is what feeds the graded image, so pinning it is
202
- // what makes "an editor-only object can never appear in the camera pane"
203
- // true by construction rather than by everyone remembering to set a mask.
204
- // Both sources honour it identically.
205
- const source = hasMesh
206
- ? binSceneSource(scene, shotCamera, new Layers())
207
- : passSceneSource(scene, shotCamera, new Layers());
208
- const chain = buildPostChain(source.textureNode, effects);
209
- const pipeline = new RenderPipeline(
210
- gl as unknown as ConstructorParameters<typeof RenderPipeline>[0],
211
- );
212
- pipeline.outputNode = chain.outputNode;
213
- pipeline.outputColorTransform = false; // see the colour note in the header
214
- // Half float, working space: the chain's output is scene-referred light and
215
- // an 8-bit sRGB hop here would band every gradient the bloom touches.
216
- const target = new RenderTarget(1, 1, {
217
- type: HalfFloatType,
218
- // Read off ColorManagement rather than written as `LinearSRGBColorSpace`:
219
- // the pipeline forces the WORKING space for the chain, so this target has
220
- // to be whatever that is, not whatever it is today.
221
- colorSpace: ColorManagement.workingColorSpace as ColorSpace,
222
- });
223
- const material = new MeshBasicNodeMaterial();
224
- material.colorNode = texture(target.texture, uv());
225
- material.depthTest = false;
226
- material.depthWrite = false;
227
- material.toneMapped = false;
228
- // THREE'S OWN full-screen primitive, not a hand-rolled plane. Its geometry
229
- // carries the v-FLIPPED uv three uses everywhere it blits a render target
230
- // (`QuadGeometry`: the bottom-left vertex is uv 0,1), and its camera covers
231
- // NDC exactly — so uv 0..1 lands on the current VIEWPORT, whatever pane
232
- // that is, and the image comes out the same way up on both backends. A
233
- // `PlaneGeometry` looks equivalent and is upside down.
234
- const blit = new QuadMesh(material);
235
- return { source, chain, pipeline, target, material, blit };
236
- // `effects` is deliberately not a dependency: its VALUES are uniforms
237
- // written every frame, and only the SIGNATURE changes the graph's shape.
238
- // eslint-disable-next-line react-hooks/exhaustive-deps
239
- }, [gl, scene, shotCamera, signature]);
240
-
292
+ const signature = effects === null ? "" : `${postSignature(effects)}::${needsBins}`;
293
+ // ── THE RIGS ARE KEPT BY SHAPE ──────────────────────────────────────────
294
+ //
295
+ // `PresenterRigCache`'s reason (ShotRuntime.tsx), in the layouts it does
296
+ // not reach: a cut to another post shape built a new chain and its
297
+ // pipelines on the cut's first frame and disposed the old one, so every
298
+ // cut back paid again, on every loop. Kept here, a shape that comes back
299
+ // finds its rig, and with it the registration identity ClockDriver already
300
+ // compiled. Sized to the film's shapes and capped as the presenter's is:
301
+ // each rig holds pane- and canvas-sized targets. One cache per renderer,
302
+ // scene and camera, since a rig is built against all three.
303
+ const rigs = useMemo(
304
+ () => ({ camera: new SplitRigCache(releaseCameraPost), working: new SplitRigCache(releaseWorkingPost) }),
305
+ [gl, scene, shotCamera],
306
+ );
241
307
  useEffect(
242
308
  () => () => {
243
- if (post === null) return;
244
- post.chain.dispose();
245
- post.pipeline.dispose();
246
- post.source.dispose();
247
- post.target.dispose();
248
- post.material.dispose();
309
+ rigs.camera.dispose();
310
+ rigs.working.dispose();
249
311
  },
250
- [post],
312
+ [rigs],
313
+ );
314
+ const rigCapacity = useMemo(
315
+ () => Math.min(PRESENTER_RIGS_MAX, Math.max(1, presenterShapesOf(doc, componentDefsOfCached(doc)))),
316
+ [doc],
251
317
  );
318
+ useEffect(() => {
319
+ rigs.camera.resize(rigCapacity);
320
+ rigs.working.resize(rigCapacity);
321
+ }, [rigs, rigCapacity]);
322
+ // ── A POST STACK THAT FAILS IS ONE PIECE ────────────────────────────────
323
+ //
324
+ // With the runtime's isolation log (pieceFailures.ts), a chain that cannot
325
+ // be built leaves its pane ungraded and a pass that throws while drawing
326
+ // leaves that frame ungraded; both are reported as the Shot's post stack.
327
+ // Without one, the throw is the stage's, as before. A rig that threw is
328
+ // never kept.
329
+ const pieces = usePieceFailures();
330
+ const postBuild = useMemo((): { rig: CameraPost | null; error: unknown } => {
331
+ if (effects === null || !hasPostChain(effects)) return { rig: null, error: null };
332
+ const build = (): CameraPost => rigs.camera.acquire(signature, () => buildCameraPost(gl, scene, shotCamera, needsBins, effects));
333
+ if (pieces === null) return { rig: build(), error: null };
334
+ try {
335
+ return { rig: build(), error: null };
336
+ } catch (error) {
337
+ return { rig: null, error };
338
+ }
339
+ // `effects` is deliberately not a dependency: its VALUES are uniforms
340
+ // written every frame, and only the SIGNATURE changes the graph's shape.
341
+ // eslint-disable-next-line react-hooks/exhaustive-deps
342
+ }, [gl, scene, shotCamera, signature, pieces, rigs]);
343
+ const post = postBuild.rig;
344
+
345
+ useLayoutEffect(() => {
346
+ if (post === null) return;
347
+ const source = post.source;
348
+ return registerSupplementalFrameCompile(
349
+ gl,
350
+ post,
351
+ async () => {
352
+ await source.compile(gl);
353
+ await compileRenderPipelineForTarget(gl, post.pipeline, post.target);
354
+ await compileObjectForTarget(gl, post.blit, post.blit.camera, null);
355
+ },
356
+ [post.blit],
357
+ // THE UPCOMING SCENE, AND A CHANGED OBJECT, UNDER THIS SOURCE'S OWN
358
+ // TARGET, as ScenePresenter registers its source. Without it a cut's
359
+ // warm-up compiled nothing for this target, and ClockDriver's targeted
360
+ // path compiled a registration with no warm WHOLE, the live scene,
361
+ // while the played frame waited.
362
+ source.compileWarm === undefined
363
+ ? undefined
364
+ : (warmScene, warmCamera, subject, side) =>
365
+ side === "outgoing" ? Promise.resolve() : source.compileWarm!(gl, warmScene, warmCamera, subject),
366
+ );
367
+ }, [gl, post]);
252
368
 
253
369
  /**
254
370
  * The WORKING pane's own post rig, for "rendered" shading. Mirrors `post`
255
371
  * with two deliberate differences:
256
372
  *
257
- * 1. Its OWN source target, drawn by `renderBins` at FRAME time with
373
+ * 1. Its OWN source target, drawn by `renderSteps` at FRAME time with
258
374
  * `feed.workingCamera` — never `pass()`/`binSceneSource`, which bake a
259
375
  * camera at build time. The working slot's camera OBJECT is swapped by
260
376
  * isolation (StageTools puts an orthographic head-on rig in it), so the
@@ -270,62 +386,199 @@ export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shad
270
386
  * the source to the pane does not reach that, and no call here can — it
271
387
  * needs a pass whose target aspect may differ from the canvas's.
272
388
  *
273
- * Keyed on the mode so the rig exists only while rendered shading is on:
274
- * solid users pay nothing, and leaving the mode disposes the pipeline. It
275
- * is keyed on the EFFECTS signature alone and not on `signature`, which
276
- * carries `hasMesh`: that bit picks the camera pane's SOURCE KIND (bin vs
277
- * pass) and this rig has neither — it always draws its own target with
278
- * `renderBins` — so folding it in would tear down and recompile the whole
279
- * working pipeline every time a mesh layer entered or left the scene.
389
+ * Kept only while rendered shading is on: solid users pay nothing, and
390
+ * leaving the mode disposes its rigs. Kept by the EFFECTS signature alone
391
+ * and not by `signature`, which carries `needsBins`: that bit picks the
392
+ * camera pane's SOURCE KIND (bin vs pass) and this rig has neither (it
393
+ * always draws its own target with `renderSteps`), so folding it in would
394
+ * build and compile another working pipeline every time a mesh or mask
395
+ * layer entered or left the scene.
280
396
  */
281
- const wantWorkingGrade = shading === "rendered";
397
+ // THROUGH THE PLAN, never a second `=== "rendered"`: what a mode means is
398
+ // shading.ts's to say, and the draw below already asks it there. `hasChain`
399
+ // is true here because whether a chain EXISTS is the next line's question,
400
+ // not this one's.
401
+ const wantWorkingGrade = workingDrawPlan(shading, true).graded;
282
402
  const workingSignature = effects === null ? "" : postSignature(effects);
283
- const wpost = useMemo(() => {
284
- if (!wantWorkingGrade || effects === null || !hasPostChain(effects)) return null;
285
- const src = new RenderTarget(1, 1, { type: HalfFloatType });
286
- const chain = buildPostChain(texture(src.texture, uv()), effects);
287
- const pipeline = new RenderPipeline(
288
- gl as unknown as ConstructorParameters<typeof RenderPipeline>[0],
289
- );
290
- pipeline.outputNode = chain.outputNode;
291
- pipeline.outputColorTransform = false; // one encode, at the blit — header
292
- const target = new RenderTarget(1, 1, {
293
- type: HalfFloatType,
294
- colorSpace: ColorManagement.workingColorSpace as ColorSpace,
295
- });
296
- const material = new MeshBasicNodeMaterial();
297
- material.colorNode = texture(target.texture, uv());
298
- material.depthTest = false;
299
- material.depthWrite = false;
300
- material.toneMapped = false;
301
- const blit = new QuadMesh(material);
302
- return { src, chain, pipeline, target, material, blit };
403
+ const wpostBuild = useMemo((): { rig: WorkingPost | null; error: unknown } => {
404
+ if (!wantWorkingGrade || effects === null || !hasPostChain(effects)) return { rig: null, error: null };
405
+ const build = (): WorkingPost => rigs.working.acquire(workingSignature, () => buildWorkingPost(gl, effects));
406
+ if (pieces === null) return { rig: build(), error: null };
407
+ try {
408
+ return { rig: build(), error: null };
409
+ } catch (error) {
410
+ return { rig: null, error };
411
+ }
303
412
  // Same rule as `post`: `effects` values are uniforms written per frame,
304
413
  // only the SIGNATURE changes the graph's shape.
305
414
  // eslint-disable-next-line react-hooks/exhaustive-deps
306
- }, [gl, workingSignature, wantWorkingGrade]);
307
-
415
+ }, [gl, workingSignature, wantWorkingGrade, pieces, rigs]);
416
+ const wpost = wpostBuild.rig;
417
+ useEffect(() => {
418
+ if (!wantWorkingGrade) rigs.working.dispose();
419
+ }, [rigs, wantWorkingGrade]);
420
+ // A chain that could not be built, reported once for the two panes (they
421
+ // build the same effects): the pane that failed draws ungraded.
422
+ const buildOwner = useMemo(() => pieceOwner("split-post"), []);
423
+ const drawOwner = useMemo(() => pieceOwner("split-post-draw"), []);
424
+ const buildError = postBuild.error ?? wpostBuild.error;
425
+ useLayoutEffect(() => {
426
+ if (pieces === null) return;
427
+ pieces.set(buildOwner, buildError === null ? [] : [postPiece(sceneId, buildError)]);
428
+ }, [pieces, buildOwner, buildError, sceneId]);
429
+ useLayoutEffect(
430
+ () => () => {
431
+ pieces?.set(buildOwner);
432
+ pieces?.set(drawOwner);
433
+ },
434
+ [pieces, buildOwner, drawOwner],
435
+ );
436
+ /** A pass that threw while drawing, until a frame draws clean. */
437
+ const drawFailed = useRef(false);
438
+ /** When the camera pane's scale may move (cuts, transition windows). */
439
+ const [schedule] = useState(() => new PresentationSchedule());
440
+ /** The camera pane's plain draw below 1 (step 1a), made with a scale source
441
+ * so its blit compiles ahead, as the grade's blit does (below): built at
442
+ * the first scaled frame instead, nothing else had built that blit's
443
+ * pipeline on a film with no post, and the frame built it synchronously.
444
+ * Its target holds memory only while the scale is below 1. */
445
+ const scales = presentationScale !== undefined;
446
+ const plainPane = useMemo(() => (scales ? buildPlainPane() : null), [gl, scales]);
308
447
  useEffect(
309
448
  () => () => {
310
- if (wpost === null) return;
311
- wpost.chain.dispose();
312
- wpost.pipeline.dispose();
313
- wpost.src.dispose();
314
- wpost.target.dispose();
315
- wpost.material.dispose();
449
+ if (plainPane !== null) releasePlainPane(plainPane);
316
450
  },
317
- [wpost],
451
+ [plainPane],
318
452
  );
453
+ useLayoutEffect(() => {
454
+ if (plainPane === null) return;
455
+ return registerSupplementalFrameCompile(
456
+ gl,
457
+ plainPane,
458
+ () => compileObjectForTarget(gl, plainPane.blit, plainPane.blit.camera, null),
459
+ [plainPane.blit],
460
+ );
461
+ }, [gl, plainPane]);
462
+
463
+ useLayoutEffect(() => {
464
+ if (wpost === null) return;
465
+ return registerSupplementalFrameCompile(
466
+ gl,
467
+ wpost,
468
+ async () => {
469
+ await compileRenderPipelineForTarget(gl, wpost.pipeline, wpost.target);
470
+ await compileObjectForTarget(gl, wpost.blit, wpost.blit.camera, null);
471
+ },
472
+ [wpost.blit],
473
+ );
474
+ }, [gl, wpost]);
475
+
476
+ // ONE READING OF THE PLAN for both the compile mode and the compile's own
477
+ // override below. They were two hand-written copies of shading.ts's
478
+ // `override` arm, so a fifth mode (or moving an existing one's override)
479
+ // compiled clean here and left the warm path disagreeing with the draw path,
480
+ // which shows up as a first-frame stall rather than as an error.
481
+ const workingOverride = ((tag) =>
482
+ tag === "wire" ? overrides.wire : tag === "clay" ? overrides.clay : null)(
483
+ workingDrawPlan(shading, wpost !== null).override,
484
+ );
485
+ const workingCompileMode: object = workingOverride ?? wpost ?? NORMAL_WORKING_COMPILE_MODE;
486
+ const workingCompileIdentity = useRef<{
487
+ camera: Camera | null;
488
+ mode: object;
489
+ identity: object;
490
+ }>({ camera: null, mode: NORMAL_WORKING_COMPILE_MODE, identity: {} });
491
+ useLayoutEffect(() => {
492
+ if (panes.working === null) return;
493
+ const workingCamera = feed.prepareWorkingCamera?.() ?? feed.workingCamera;
494
+ if (workingCamera === null) return;
495
+ if (
496
+ workingCompileIdentity.current.camera !== workingCamera ||
497
+ workingCompileIdentity.current.mode !== workingCompileMode
498
+ ) {
499
+ workingCompileIdentity.current = {
500
+ camera: workingCamera,
501
+ mode: workingCompileMode,
502
+ identity: {},
503
+ };
504
+ }
505
+ const identity = workingCompileIdentity.current.identity;
506
+ const override = workingOverride;
507
+ const source = wpost;
508
+ // One compile shape for the live scene and for the upcoming one: the same
509
+ // targets, the same masks, the same override, so a cut finds every
510
+ // working-pane pipeline resident (see `WarmCompile`).
511
+ const compileScene = async (target: Scene, camera: Camera, subject: Object3D): Promise<void> => {
512
+ if (source !== null) {
513
+ // This is the target the priority-1 draw actually fills before the
514
+ // working post chain reads it. A canvas compile is not equivalent:
515
+ // target format and samples are part of Three's pipeline context.
516
+ source.src.samples = gl.samples;
517
+ source.src.texture.type = gl.getOutputBufferType();
518
+ await compileObjectForTarget(
519
+ gl,
520
+ subject,
521
+ camera,
522
+ source.src,
523
+ target,
524
+ () => configureWorkingSceneCompile(target, camera, 1, null),
525
+ );
526
+ // Rendered shading overpaints the editor layer after the graded blit.
527
+ // It is a separate canvas draw and therefore a separate compile
528
+ // context from the half-float source above.
529
+ await compileObjectForTarget(
530
+ gl,
531
+ subject,
532
+ camera,
533
+ null,
534
+ target,
535
+ () => configureWorkingSceneCompile(target, camera, 1 << EDITOR_LAYER, null),
536
+ );
537
+ return;
538
+ }
539
+ const mask = workingCamera.layers.mask;
540
+ await compileObjectForTarget(
541
+ gl,
542
+ subject,
543
+ camera,
544
+ null,
545
+ target,
546
+ () => configureWorkingSceneCompile(target, camera, mask, override),
547
+ );
548
+ };
549
+ return registerSupplementalFrameCompile(
550
+ gl,
551
+ identity,
552
+ () => compileScene(scene, workingCamera, scene),
553
+ [],
554
+ (warmScene, warmCamera, subject, side) =>
555
+ side === "outgoing" ? Promise.resolve() : compileScene(warmScene, warmCamera, subject),
556
+ );
557
+ // Deliberately every commit. StageTools publishes the current camera in a
558
+ // layout effect rather than React state. Re-registering the SAME identity
559
+ // updates this closure without forcing a compile; a camera or draw-mode
560
+ // change mints an identity above and makes ClockDriver compile once.
561
+ });
319
562
 
320
563
  // The props are read straight out of the closure. r3f re-points `useFrame`'s
321
564
  // callback ref in a layout effect on every render, and `ClockDriver` — the
322
565
  // LAST child of the scene — is what calls `advance`, so the callback that
323
566
  // draws is always the one this render produced.
324
567
  useFrame(() => {
568
+ // A subscriber in this tick threw (the error is parked, ClockDriver
569
+ // consumes it right after advance): do NOT present the crashed frame.
570
+ // The canvas keeps the last good picture, matching the v9 latch.
571
+ // Isolating, the throw is one piece (ClockDriver records it) and the
572
+ // frame draws around it, so the rest of the Shot keeps moving.
573
+ if (pieces === null && pendingFrameError(store)) return;
325
574
  const working = panes.working;
326
575
  const camera = panes.camera;
327
- const fx = effects;
328
- const at = frame;
576
+ // The values of THIS draw's frame, when the stage drives by clock; the
577
+ // props' otherwise. The chain's shape came from the props either way.
578
+ const now = live?.at();
579
+ const fx = now === undefined ? effects : effects === null ? null : now.effects;
580
+ const at = now === undefined ? frame : now.masterFrame;
581
+ const bins = now === undefined ? snapshotBins : now.bins;
329
582
  const workingCamera = feed.workingCamera;
330
583
 
331
584
  // The shot's own clear colour is ShotBackground's, written on a layout
@@ -341,33 +594,59 @@ export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shad
341
594
  // for the same reason, at the PANE's size so the blend is not stretched
342
595
  // across a frustum it was not rendered for. Without this the editor
343
596
  // would show hard cuts in split view where the export dissolves.
344
- const blend = camera === null ? null : transitionTap();
597
+ // A capture that threw answers false (the compositor reported it as
598
+ // the transition): the pane shows the incoming Shot, a hard cut.
599
+ let blend = camera === null ? null : transitionTap();
600
+ // The camera pane's playback scale (header): asked once per played frame,
601
+ // and held through a transition window by the schedule.
602
+ let scale = 1;
603
+ if (presentationScale !== undefined && playing && camera !== null) {
604
+ scale = schedule.next(presentationScale, now?.sceneId ?? sceneId, blend !== null, at);
605
+ } else schedule.stop();
345
606
  if (blend !== null && camera !== null) {
346
607
  gl.setScissorTest(false);
347
- blend.capture(camera.width, camera.height);
608
+ const tap = blend;
609
+ let captured = true;
610
+ withPresentationScale(gl, scale, () => {
611
+ captured = tap.capture(camera.width, camera.height);
612
+ });
613
+ if (!captured) blend = null;
348
614
  }
349
615
 
616
+ // A post pass that throws while drawing leaves its pane ungraded for this
617
+ // frame, and is reported (see `postBuild`). Without the log it throws.
618
+ // Inline try blocks and module functions, not closures: this runs every
619
+ // frame. The one closure per offscreen draw is the scale's seam
620
+ // (`withPresentationScale`), which at 1 only calls it.
621
+ let drawError: unknown = null;
622
+
350
623
  // 1. The camera pane's post chain, OFFSCREEN and UNSCISSORED. See the
351
624
  // header for why this cannot go straight to the canvas. Skipped inside a
352
625
  // window: the compositor rendered this scene into its own operand target
353
626
  // already, with the same chain.
627
+ let graded = post !== null && fx !== null;
354
628
  if (blend === null && camera !== null && post !== null && fx !== null) {
355
- // The target is PANE sized, in device px: the pane gets its own
356
- // resolution for free, and `setSize` already no-ops when it matches.
357
- const dpr = gl.getPixelRatio();
358
- post.target.setSize(
359
- Math.max(1, Math.round(camera.width * dpr)),
360
- Math.max(1, Math.round(camera.height * dpr)),
361
- );
362
- post.chain.write(fx, at);
363
- gl.setScissorTest(false);
364
- // The bins first, into whatever the source owns. A `pass()`-backed source
365
- // no-ops here and renders itself from inside `pipeline.render()` below,
366
- // which is the untouched single-render path.
367
- post.source.draw(gl, bins);
368
- gl.setRenderTarget(post.target);
369
- post.pipeline.render();
370
- gl.setRenderTarget(null);
629
+ try {
630
+ withPresentationScale(gl, scale, () => drawCameraPost(gl, post, camera, fx, at, bins));
631
+ } catch (error) {
632
+ if (pieces === null) throw error;
633
+ drawError ??= error;
634
+ gl.setRenderTarget(null);
635
+ graded = false;
636
+ }
637
+ }
638
+
639
+ // 1a. A pane with no grade to blit, while the scale is below 1: the plain
640
+ // draw goes offscreen too, into a pane-sized target at the scale, and
641
+ // step 3 blits it up, because a canvas pass cannot draw below the
642
+ // canvas's density. At 1 it stays step 3's direct draw.
643
+ let plain: PlainPane | null = null;
644
+ if (scale < 1 && blend === null && camera !== null && !graded && plainPane !== null) {
645
+ withPresentationScale(gl, scale, () => drawPlainPane(gl, plainPane, camera, scene, shotCamera, bins));
646
+ plain = plainPane;
647
+ } else if (scale === 1 && plainPane?.held === true) {
648
+ plainPane.held = false;
649
+ plainPane.target.dispose();
371
650
  }
372
651
 
373
652
  // 1b. The WORKING pane's grade, offscreen and unscissored — rendered
@@ -379,28 +658,29 @@ export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shad
379
658
  // this camera's mask is per-draw state we restore. The clear colour is
380
659
  // still the shot's own (`_prevClear`): a graded working pane previews
381
660
  // the export's image, so it sits on the export's background.
661
+ let workingGraded = wpost !== null;
382
662
  if (working !== null && workingCamera !== null && wpost !== null && fx !== null) {
383
- const dpr = gl.getPixelRatio();
384
- const w = Math.max(1, Math.round(working.width * dpr));
385
- const h = Math.max(1, Math.round(working.height * dpr));
386
- // Mirrors binSceneSource's target discipline: the renderer's own sample
387
- // count and output buffer type, so the image is scene-referred with MSAA
388
- // on the model silhouettes.
389
- wpost.src.samples = gl.samples;
390
- wpost.src.texture.type = gl.getOutputBufferType();
391
- wpost.src.setSize(w, h);
392
- wpost.target.setSize(w, h);
393
- wpost.chain.write(fx, at);
394
- gl.setScissorTest(false);
395
663
  const prevMask = workingCamera.layers.mask;
396
- workingCamera.layers.mask = 1; // bit 0 only
397
- gl.setRenderTarget(wpost.src);
398
- renderBins(gl, scene, workingCamera, bins);
399
- gl.setRenderTarget(null);
664
+ try {
665
+ drawWorkingPost(gl, wpost, working, workingCamera, fx, at, scene, bins);
666
+ } catch (error) {
667
+ if (pieces === null) throw error;
668
+ drawError ??= error;
669
+ gl.setRenderTarget(null);
670
+ workingGraded = false;
671
+ }
400
672
  workingCamera.layers.mask = prevMask;
401
- gl.setRenderTarget(wpost.target);
402
- wpost.pipeline.render();
403
- gl.setRenderTarget(null);
673
+ }
674
+
675
+ // What this frame's grade did, reported once for both panes.
676
+ if (pieces !== null) {
677
+ if (drawError !== null) {
678
+ drawFailed.current = true;
679
+ pieces.set(drawOwner, [postPiece(now?.sceneId ?? sceneId, drawError)]);
680
+ } else if (drawFailed.current) {
681
+ drawFailed.current = false;
682
+ pieces.set(drawOwner);
683
+ }
404
684
  }
405
685
 
406
686
  gl.setScissorTest(true);
@@ -430,9 +710,9 @@ export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shad
430
710
  // plan, which is what `workingDrawPlan` returns for it.
431
711
  // The camera pane is always the one that matches the export.
432
712
  if (working !== null && workingCamera !== null) {
433
- const plan = workingDrawPlan(shading, wpost !== null);
713
+ const plan = workingDrawPlan(shading, wpost !== null && workingGraded);
434
714
  applyRect(gl, working);
435
- if (plan.graded && wpost !== null) {
715
+ if (plan.graded && wpost !== null && workingGraded) {
436
716
  // The graded image already carries the shot's own background, so the
437
717
  // pane clears with it rather than the clay backdrop.
438
718
  gl.setClearColor(_prevClear, prevAlpha);
@@ -449,9 +729,25 @@ export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shad
449
729
  gl.autoClear = prevAutoClear;
450
730
  workingCamera.layers.mask = prevMask;
451
731
  } else if (plan.override !== null) {
452
- gl.setClearColor(CLAY_BACKDROP, 1);
732
+ gl.setClearColor(clearBackdrop, 1);
453
733
  scene.overrideMaterial = plan.override === "wire" ? overrides.wire : overrides.clay;
454
- renderBins(gl, scene, workingCamera, bins);
734
+ // An override draws every object with ITS side (three 0.185.1 copies
735
+ // the source material's side onto an override only for shadow
736
+ // passes), so a `BackSide` sky dome becomes a front-faced clay ball
737
+ // around the whole world. It is hidden for this draw and put back.
738
+ const insideOut = hideInsideOut(scene);
739
+ try {
740
+ // ONE PASS FOR ONE COLOUR. Clay and wire are depth-off and the
741
+ // same colour everywhere, so drawing the bins in order paints
742
+ // nothing one pass of all of them does not (one colour blended over
743
+ // itself commutes), and each bin was a whole render: a projection
744
+ // walk, a canvas-size MSAA pass and an output quad. A matte or a
745
+ // blur still cuts what it covers, so a plan with one keeps its steps.
746
+ renderSteps(gl, scene, workingCamera, bins.some(isCompositeStep) ? bins : NO_STEPS);
747
+ } finally {
748
+ for (const o of insideOut) o.visible = true;
749
+ insideOut.length = 0;
750
+ }
455
751
  scene.overrideMaterial = null;
456
752
  } else {
457
753
  // RENDERED WITHOUT A CHAIN keeps the SHOT's backdrop, not the clay
@@ -463,22 +759,23 @@ export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shad
463
759
  // Material mode keeps the clay backdrop on purpose: it previews the
464
760
  // MATERIALS against the workspace, not the shot.
465
761
  if (shading === "rendered") gl.setClearColor(_prevClear, prevAlpha);
466
- else gl.setClearColor(CLAY_BACKDROP, 1);
467
- renderBins(gl, scene, workingCamera, bins);
762
+ else gl.setClearColor(clearBackdrop, 1);
763
+ renderSteps(gl, scene, workingCamera, bins);
468
764
  }
469
765
  }
470
766
 
471
767
  // 3. The camera pane: the blit, or a plain render when the scene has no
472
768
  // post at all (no pass, no target, no cost — the same call ShotRuntime
473
- // would have made).
769
+ // would have made), or below 1 the plain draw's blit (step 1a).
474
770
  if (camera !== null) {
475
771
  applyRect(gl, camera);
476
772
  gl.setClearColor(_prevClear, prevAlpha);
477
773
  // `QuadMesh.render` is `renderer.render(quad, itsOwnCamera)` — the same
478
774
  // call, through the object that owns the right camera for it.
479
775
  if (blend !== null) blend.quad.render(gl);
480
- else if (post !== null && fx !== null) post.blit.render(gl);
481
- else renderBins(gl, scene, shotCamera, bins);
776
+ else if (post !== null && fx !== null && graded) post.blit.render(gl);
777
+ else if (plain !== null) plain.blit.render(gl);
778
+ else renderSteps(gl, scene, shotCamera, bins);
482
779
  }
483
780
 
484
781
  // 4. Hand the renderer back exactly as it was found. Every one of these is
@@ -489,11 +786,300 @@ export function SplitRenderer({ feed, panes, effects, bins, hasMesh, frame, shad
489
786
  gl.setViewport(0, 0, size.width, size.height);
490
787
  gl.setScissor(0, 0, size.width, size.height);
491
788
  gl.setClearColor(_prevClear, prevAlpha);
492
- }, 1);
789
+ }, { phase: "render" });
493
790
 
494
791
  return null;
495
792
  }
496
793
 
794
+ interface CameraPost {
795
+ source: SceneSource;
796
+ chain: ReturnType<typeof buildPostChain>;
797
+ pipeline: RenderPipeline;
798
+ target: RenderTarget;
799
+ material: MeshBasicNodeMaterial;
800
+ blit: QuadMesh;
801
+ }
802
+
803
+ /**
804
+ * The CAMERA pane's post rig: its scene source, the chain, the pipeline that
805
+ * renders it offscreen and the blit that brings it into the pane.
806
+ *
807
+ * Layer 0 only. This source is what feeds the graded image, so pinning it is
808
+ * what makes "an editor-only object can never appear in the camera pane" true
809
+ * by construction rather than by everyone remembering to set a mask. Both
810
+ * sources honour it identically. A build that throws part-way disposes the
811
+ * source it made (the caller isolates the throw, see `postBuild`).
812
+ */
813
+ function buildCameraPost(
814
+ gl: WebGPURenderer,
815
+ scene: Scene,
816
+ shotCamera: Camera,
817
+ needsBins: boolean,
818
+ effects: SceneEffects,
819
+ ): CameraPost {
820
+ const source = needsBins
821
+ ? binSceneSource(scene, shotCamera, new Layers())
822
+ : passSceneSource(scene, shotCamera, new Layers());
823
+ let chain: ReturnType<typeof buildPostChain>;
824
+ try {
825
+ chain = buildPostChain(source.textureNode, effects);
826
+ } catch (error) {
827
+ source.dispose();
828
+ throw error;
829
+ }
830
+ const pipeline = new RenderPipeline(gl as unknown as ConstructorParameters<typeof RenderPipeline>[0]);
831
+ pipeline.outputNode = chain.outputNode;
832
+ pipeline.outputColorTransform = false; // see the colour note in the header
833
+ // Half float, working space: the chain's output is scene-referred light and
834
+ // an 8-bit sRGB hop here would band every gradient the bloom touches.
835
+ const target = new RenderTarget(1, 1, {
836
+ type: HalfFloatType,
837
+ // Read off ColorManagement rather than written as `LinearSRGBColorSpace`:
838
+ // the pipeline forces the WORKING space for the chain, so this target has
839
+ // to be whatever that is, not whatever it is today.
840
+ colorSpace: ColorManagement.workingColorSpace as ColorSpace,
841
+ });
842
+ return { source, chain, pipeline, target, ...blitOf(target) };
843
+ }
844
+
845
+ /** A target brought into the current viewport: drawn with no depth and no
846
+ * tone mapping of its own, so the canvas's one output pass encodes it. */
847
+ function blitOf(target: RenderTarget): { material: MeshBasicNodeMaterial; blit: QuadMesh } {
848
+ const material = new MeshBasicNodeMaterial();
849
+ material.colorNode = texture(target.texture, uv());
850
+ material.depthTest = false;
851
+ material.depthWrite = false;
852
+ material.toneMapped = false;
853
+ // THREE'S OWN full-screen primitive, not a hand-rolled plane. Its geometry
854
+ // carries the v-FLIPPED uv three uses everywhere it blits a render target
855
+ // (`QuadGeometry`: the bottom-left vertex is uv 0,1), and its camera covers
856
+ // NDC exactly — so uv 0..1 lands on the current VIEWPORT, whatever pane that
857
+ // is, and the image comes out the same way up on both backends. A
858
+ // `PlaneGeometry` looks equivalent and is upside down.
859
+ return { material, blit: new QuadMesh(material) };
860
+ }
861
+
862
+ interface WorkingPost {
863
+ src: RenderTarget;
864
+ chain: ReturnType<typeof buildPostChain>;
865
+ pipeline: RenderPipeline;
866
+ target: RenderTarget;
867
+ material: MeshBasicNodeMaterial;
868
+ blit: QuadMesh;
869
+ }
870
+
871
+ /** The WORKING pane's post rig (rendered shading): its own source target,
872
+ * the chain over it, the pipeline and the blit. See `wpost`. */
873
+ function buildWorkingPost(gl: WebGPURenderer, effects: SceneEffects): WorkingPost {
874
+ // In the WORKING space, the canvas frame-buffer target's own label: WebGPU
875
+ // keys a pipeline on its target's colour space, so a material drawn here
876
+ // and on the canvas builds one pipeline instead of two. The format stays
877
+ // half float, and sampling a working-space texture into the working space
878
+ // converts nothing, so no byte of the pane moves.
879
+ const src = new RenderTarget(1, 1, {
880
+ type: HalfFloatType,
881
+ colorSpace: ColorManagement.workingColorSpace as ColorSpace,
882
+ });
883
+ let chain: ReturnType<typeof buildPostChain>;
884
+ try {
885
+ chain = buildPostChain(texture(src.texture, uv()), effects);
886
+ } catch (error) {
887
+ src.dispose();
888
+ throw error;
889
+ }
890
+ const pipeline = new RenderPipeline(gl as unknown as ConstructorParameters<typeof RenderPipeline>[0]);
891
+ pipeline.outputNode = chain.outputNode;
892
+ pipeline.outputColorTransform = false; // one encode, at the blit — header
893
+ const target = new RenderTarget(1, 1, {
894
+ type: HalfFloatType,
895
+ colorSpace: ColorManagement.workingColorSpace as ColorSpace,
896
+ });
897
+ return { src, chain, pipeline, target, ...blitOf(target) };
898
+ }
899
+
900
+ /** The camera pane's plain draw below 1 (the draw's step 1a): its target and
901
+ * the blit that brings it into the pane. */
902
+ interface PlainPane {
903
+ target: RenderTarget;
904
+ material: MeshBasicNodeMaterial;
905
+ blit: QuadMesh;
906
+ /** The target holds memory: drawn since the scale was last at 1. */
907
+ held: boolean;
908
+ }
909
+
910
+ /** Of the canvas frame-buffer target's own kind (half float, the working
911
+ * space, depth, and the renderer's samples at the draw), so every pipeline
912
+ * the scene built for the canvas draws into it too. */
913
+ function buildPlainPane(): PlainPane {
914
+ const target = new RenderTarget(1, 1, {
915
+ type: HalfFloatType,
916
+ colorSpace: ColorManagement.workingColorSpace as ColorSpace,
917
+ });
918
+ return { target, ...blitOf(target), held: false };
919
+ }
920
+
921
+ function releasePlainPane(pane: PlainPane): void {
922
+ pane.target.dispose();
923
+ pane.material.dispose();
924
+ }
925
+
926
+ function releaseCameraPost(post: CameraPost): void {
927
+ post.chain.dispose();
928
+ post.pipeline.dispose();
929
+ post.source.dispose();
930
+ post.target.dispose();
931
+ post.material.dispose();
932
+ }
933
+
934
+ function releaseWorkingPost(wpost: WorkingPost): void {
935
+ wpost.chain.dispose();
936
+ wpost.pipeline.dispose();
937
+ wpost.src.dispose();
938
+ wpost.target.dispose();
939
+ wpost.material.dispose();
940
+ }
941
+
942
+ /**
943
+ * The split rigs by shape, least recently used first: `PresenterRigCache`
944
+ * (ShotRuntime.tsx) for this file's two rigs, with the release passed in.
945
+ * The mounted rig is always the most recent acquire, so it is never the one
946
+ * evicted while the cache holds two or more.
947
+ *
948
+ * ponytail: the third copy of this LRU (with `TransitionRigCache` and
949
+ * `PresenterRigCache`), because the runtime's are typed to their own rigs
950
+ * and not exported. One generic cache in the shot package retires all three.
951
+ */
952
+ class SplitRigCache<R> {
953
+ readonly #rigs = new Map<string, R>();
954
+ readonly #release: (rig: R) => void;
955
+ #capacity = 1;
956
+
957
+ constructor(release: (rig: R) => void) {
958
+ this.#release = release;
959
+ }
960
+
961
+ resize(capacity: number): void {
962
+ this.#capacity = Math.max(1, capacity);
963
+ this.#evict();
964
+ }
965
+
966
+ acquire(key: string, make: () => R): R {
967
+ const hit = this.#rigs.get(key);
968
+ if (hit !== undefined) {
969
+ this.#rigs.delete(key);
970
+ this.#rigs.set(key, hit);
971
+ return hit;
972
+ }
973
+ const rig = make();
974
+ this.#rigs.set(key, rig);
975
+ this.#evict();
976
+ return rig;
977
+ }
978
+
979
+ dispose(): void {
980
+ for (const rig of this.#rigs.values()) this.#release(rig);
981
+ this.#rigs.clear();
982
+ }
983
+
984
+ #evict(): void {
985
+ while (this.#rigs.size > this.#capacity) {
986
+ const oldest = this.#rigs.keys().next().value as string;
987
+ const victim = this.#rigs.get(oldest)!;
988
+ this.#rigs.delete(oldest);
989
+ this.#release(victim);
990
+ }
991
+ }
992
+ }
993
+
994
+ /** A Shot's post stack, as a failed piece. */
995
+ /** The camera pane's grade, offscreen (see the draw's step 1). */
996
+ function drawCameraPost(
997
+ gl: WebGPURenderer,
998
+ post: CameraPost,
999
+ camera: PaneRect,
1000
+ fx: SceneEffects,
1001
+ at: number,
1002
+ bins: readonly RenderStep[],
1003
+ ): void {
1004
+ // The target is PANE sized, in device px: the pane gets its own
1005
+ // resolution for free, and `setSize` already no-ops when it matches. Sized
1006
+ // as three sizes the pane's viewport (`devicePixels`), so the blit draws it
1007
+ // back one texel to a pixel; rounded, it was a pixel wider than the pane at
1008
+ // a fractional density.
1009
+ const dpr = gl.getPixelRatio();
1010
+ post.target.setSize(devicePixels(camera.width, dpr), devicePixels(camera.height, dpr));
1011
+ // The pixel-sized knobs at the pane's playback scale (`drawScaleOf`).
1012
+ post.chain.write(fx, at, drawScaleOf(gl));
1013
+ gl.setScissorTest(false);
1014
+ // The bins first, into whatever the source owns. A `pass()`-backed
1015
+ // source no-ops here and renders itself from inside
1016
+ // `pipeline.render()` below, which is the untouched single-render path.
1017
+ post.source.draw(gl, bins);
1018
+ gl.setRenderTarget(post.target);
1019
+ post.pipeline.render();
1020
+ gl.setRenderTarget(null);
1021
+ }
1022
+
1023
+ /** The camera pane's plain draw, offscreen and pane sized like the grade's
1024
+ * (see the draw's step 1a). The clear colour is still the Shot's. */
1025
+ function drawPlainPane(
1026
+ gl: WebGPURenderer,
1027
+ pane: PlainPane,
1028
+ camera: PaneRect,
1029
+ scene: Scene,
1030
+ shotCamera: Camera,
1031
+ bins: readonly RenderStep[],
1032
+ ): void {
1033
+ const dpr = gl.getPixelRatio();
1034
+ pane.target.samples = gl.samples;
1035
+ pane.target.texture.type = gl.getOutputBufferType();
1036
+ pane.target.setSize(devicePixels(camera.width, dpr), devicePixels(camera.height, dpr));
1037
+ pane.held = true;
1038
+ gl.setScissorTest(false);
1039
+ gl.setRenderTarget(pane.target);
1040
+ renderSteps(gl, scene, shotCamera, bins);
1041
+ gl.setRenderTarget(null);
1042
+ }
1043
+
1044
+ /** The working pane's grade, offscreen (see the draw's step 1b). The caller
1045
+ * restores the camera's mask, thrown or not. */
1046
+ function drawWorkingPost(
1047
+ gl: WebGPURenderer,
1048
+ wpost: WorkingPost,
1049
+ working: PaneRect,
1050
+ workingCamera: Camera,
1051
+ fx: SceneEffects,
1052
+ at: number,
1053
+ scene: Scene,
1054
+ bins: readonly RenderStep[],
1055
+ ): void {
1056
+ const prevMask = workingCamera.layers.mask;
1057
+ const dpr = gl.getPixelRatio();
1058
+ const w = devicePixels(working.width, dpr);
1059
+ const h = devicePixels(working.height, dpr);
1060
+ // Mirrors binSceneSource's target discipline: the renderer's own sample
1061
+ // count and output buffer type, so the image is scene-referred with
1062
+ // MSAA on the model silhouettes.
1063
+ wpost.src.samples = gl.samples;
1064
+ wpost.src.texture.type = gl.getOutputBufferType();
1065
+ wpost.src.setSize(w, h);
1066
+ wpost.target.setSize(w, h);
1067
+ wpost.chain.write(fx, at);
1068
+ gl.setScissorTest(false);
1069
+ workingCamera.layers.mask = 1; // bit 0 only
1070
+ gl.setRenderTarget(wpost.src);
1071
+ renderSteps(gl, scene, workingCamera, bins);
1072
+ gl.setRenderTarget(null);
1073
+ workingCamera.layers.mask = prevMask;
1074
+ gl.setRenderTarget(wpost.target);
1075
+ wpost.pipeline.render();
1076
+ gl.setRenderTarget(null);
1077
+ }
1078
+
1079
+ function postPiece(sceneId: string | null, error: unknown): PieceFailureInput {
1080
+ return { key: "post", kind: "post", sceneId: sceneId ?? "", fieldPath: "effects", error };
1081
+ }
1082
+
497
1083
  /**
498
1084
  * Point the renderer at one pane.
499
1085
  *
@@ -509,3 +1095,64 @@ function applyRect(gl: WebGPURenderer, rect: PaneRect): void {
509
1095
  gl.setViewport(rect.x, rect.y, rect.width, rect.height);
510
1096
  gl.setScissor(rect.x, rect.y, rect.width, rect.height);
511
1097
  }
1098
+
1099
+ /**
1100
+ * Make compileAsync see the working draw's exact scene state, then hand back a
1101
+ * synchronous restore. `compileObjectForTarget` invokes both around its
1102
+ * synchronous scene walk, so no camera, scene, object, or renderer state is
1103
+ * held while shader compilation awaits the backend.
1104
+ *
1105
+ * Frustum culling is disabled for the walk on purpose. An orbit can reveal an
1106
+ * already-mounted object without changing its material signature, and that
1107
+ * later first draw must not be where its pipeline is created.
1108
+ */
1109
+ /** Scratch for `hideInsideOut`: the meshes hidden for one override draw. */
1110
+ const _insideOut: Object3D[] = [];
1111
+
1112
+ /**
1113
+ * Hide every showing mesh whose material(s) are ALL `BackSide` and return
1114
+ * them, so the caller can put them back after the draw. Such a mesh is a
1115
+ * shell meant to be seen from inside (a sky dome, an environment sphere);
1116
+ * under a single-sided override it is a wall around everything else. The same
1117
+ * predicate the camera body's bounds walk skips by (sceneExtent).
1118
+ *
1119
+ * WALKED AT EVERY DRAW, over what shows (a shell under a hidden parent draws
1120
+ * nothing anyway). A list kept between draws went stale on changes no key it
1121
+ * had could see: a cut or a seek into a resident Shot only re-parents that
1122
+ * Shot's group, and a code component's module, its own Suspense or a child it
1123
+ * renders on a prop commits a subtree with no layer mounting. A shell the
1124
+ * list missed drew as a clay ball over the whole pane.
1125
+ */
1126
+ export function hideInsideOut(scene: Scene): Object3D[] {
1127
+ _insideOut.length = 0;
1128
+ scene.traverseVisible((o) => {
1129
+ if (!isInsideOutShell(o)) return;
1130
+ o.visible = false;
1131
+ _insideOut.push(o);
1132
+ });
1133
+ return _insideOut;
1134
+ }
1135
+
1136
+ export function configureWorkingSceneCompile(
1137
+ scene: Scene,
1138
+ camera: Camera,
1139
+ mask: number,
1140
+ override: Material | null,
1141
+ ): () => void {
1142
+ const previousMask = camera.layers.mask;
1143
+ const previousOverride = scene.overrideMaterial;
1144
+ const culled: Array<{ object: Object3D & { frustumCulled: boolean }; value: boolean }> = [];
1145
+ camera.layers.mask = mask;
1146
+ scene.overrideMaterial = override;
1147
+ scene.traverse((object) => {
1148
+ if (!("frustumCulled" in object) || typeof object.frustumCulled !== "boolean") return;
1149
+ const renderable = object as Object3D & { frustumCulled: boolean };
1150
+ culled.push({ object: renderable, value: renderable.frustumCulled });
1151
+ renderable.frustumCulled = false;
1152
+ });
1153
+ return () => {
1154
+ for (const entry of culled) entry.object.frustumCulled = entry.value;
1155
+ scene.overrideMaterial = previousOverride;
1156
+ camera.layers.mask = previousMask;
1157
+ };
1158
+ }