@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
@@ -23,9 +23,21 @@
23
23
  //
24
24
  // The frustum is sized from the subtree's own bounds, measured in the ROOT's
25
25
  // frame, so entering a subtree frames it whatever the scene camera was doing.
26
- import { toLocal, worldStateAt } from "@dailies-studio/shot/doc";
27
- import type { Mat3, Scene3D, Vec3 } from "@dailies-studio/shot/doc";
26
+ import { effectiveTransform, layerAliveAt, layerChildren, toLocal, worldStateAt } from "@dailies-studio/shot/doc";
27
+ import type { LayerLookup, Mat3, Vec3 } from "@dailies-studio/shot/doc";
28
28
  import type { Rational } from "@dailies-studio/engine";
29
+ import type { PaneRect } from "./panes.js";
30
+
31
+ /**
32
+ * Bind-pose box sizes by layer id, as the runtime measured them.
33
+ *
34
+ * A model is contain-fitted INTO its authored box (`applyFit`), so a model whose
35
+ * bind-pose aspect differs from that box occupies only part of it. Framing the
36
+ * box therefore frames empty air on two sides, which is most of why an isolated
37
+ * mesh read small. The stage collects these from `onMeshStats` and they never
38
+ * cross the bridge — this is a viewing decision, not a document one.
39
+ */
40
+ export type MeshBounds = ReadonlyMap<string, readonly [number, number, number]>;
29
41
 
30
42
  /** How much of the pane is margin around the framed subtree, per side. */
31
43
  export const ISOLATION_MARGIN = 0.12;
@@ -34,6 +46,26 @@ export const ISOLATION_MARGIN = 0.12;
34
46
  * a real document, and a zero-width ortho camera projects everything to NaN. */
35
47
  const MIN_HALF_EXTENT = 1;
36
48
 
49
+ /**
50
+ * The COMPONENT CONTEXT's frame (unify P2): a definition's canvas is flat at
51
+ * the origin in its own basis, so its head-on frame is a literal — never a
52
+ * derivation off an instance (`isolationFrame` needs a scene and a placed
53
+ * root, and a def-first entry has neither).
54
+ */
55
+ export function componentFrame(size: readonly [number, number]): IsolationFrame {
56
+ return {
57
+ target: [0, 0, 0],
58
+ rotation: [
59
+ [1, 0, 0],
60
+ [0, 1, 0],
61
+ [0, 0, 1],
62
+ ],
63
+ halfWidth: Math.max(MIN_HALF_EXTENT, size[0] / 2),
64
+ halfHeight: Math.max(MIN_HALF_EXTENT, size[1] / 2),
65
+ halfDepth: MIN_HALF_EXTENT,
66
+ };
67
+ }
68
+
37
69
  export interface IsolationFrame {
38
70
  /** Where to look, in THREE world space. */
39
71
  target: Vec3;
@@ -53,58 +85,284 @@ export interface IsolationFrame {
53
85
  * parent is mid-rotation is looking at where it is now. Returns null when the
54
86
  * layer is not in this scene, which is what "the isolation went stale" looks
55
87
  * like from here.
88
+ *
89
+ * A bare `LayerLookup` and not a `Scene3D`, because a component DEFINITION is
90
+ * also a layer table and a mesh inside one deserves the same head-on frame a
91
+ * mesh in a scene gets. Nothing here ever read anything but `layersById`.
56
92
  */
57
93
  export function isolationFrame(
58
- scene: Scene3D,
94
+ scene: LayerLookup,
59
95
  rootId: string,
60
96
  subtree: ReadonlySet<string>,
61
97
  sceneFrame: number,
62
98
  fps: Rational,
99
+ meshBounds?: MeshBounds,
100
+ ): IsolationFrame | null {
101
+ if (scene.layersById[rootId] === undefined) return null;
102
+ const rootPose = worldStateAt(scene, rootId, sceneFrame, fps);
103
+ const bounds = emptyBounds();
104
+ for (const id of subtree) addMember(bounds, scene, rootPose, id, sceneFrame, fps, meshBounds);
105
+ return frameOf(rootPose, bounds);
106
+ }
107
+
108
+ /** The most frames `isolationFrameOverLife` resolves the subtree at. */
109
+ export const MAX_LIFE_SAMPLES = 240;
110
+
111
+ /**
112
+ * The head-on frame that holds `rootId`'s subtree for its WHOLE life, seen from
113
+ * the root's pose at `anchorFrame`.
114
+ *
115
+ * ── WHY NOT THE FRAME AT THE PLAYHEAD ────────────────────────────────────
116
+ *
117
+ * The frame at one instant is the right size for that instant only. Fitted per
118
+ * frame during playback, the camera re-centred and re-zoomed on every frame, so
119
+ * anything that moved or scaled inside the subtree was cancelled out by the
120
+ * camera chasing it: the picture slid and pulsed while the subject stood still.
121
+ * Isolation is a place the author edits in, so its camera holds still and the
122
+ * motion plays inside it. That needs a box that already contains every pose.
123
+ *
124
+ * The basis is the root's pose at the ANCHOR (the frame the author entered on),
125
+ * not at each sample: a root that rotates over its life would otherwise give a
126
+ * union of boxes measured in different frames, which bounds nothing. Every
127
+ * member at every sample is measured in that one basis.
128
+ *
129
+ * Only what DRAWS is counted: a member outside its lifetime, hidden, or under a
130
+ * parent that is either, paints nothing at that frame, and framing where it
131
+ * would have been is empty margin. Ancestors ABOVE the root are not consulted:
132
+ * when one of them is dead the whole subtree is, and that frame adds nothing.
133
+ * Nothing drawing at any sample falls back to the anchor frame's own box, so a
134
+ * subtree that is never on screen still opens on something.
135
+ *
136
+ * ponytail: past `MAX_LIFE_SAMPLES` frames the life is sampled at an even
137
+ * stride, so an overshoot between two samples can poke past the box. The 12%
138
+ * margin absorbs any ordinary ease. Sample the track keys too if it ever clips.
139
+ */
140
+ export function isolationFrameOverLife(
141
+ scene: LayerLookup,
142
+ rootId: string,
143
+ anchorFrame: number,
144
+ durationFrames: number,
145
+ fps: Rational,
146
+ meshBounds?: MeshBounds,
63
147
  ): IsolationFrame | null {
64
148
  const root = scene.layersById[rootId];
65
149
  if (root === undefined) return null;
66
- const rootPose = worldStateAt(scene, rootId, sceneFrame, fps);
150
+ const rootPose = worldStateAt(scene, rootId, anchorFrame, fps);
151
+ const bounds = emptyBounds();
152
+ const end = Math.max(1, Math.floor(durationFrames));
153
+ const stride = Math.max(1, end / MAX_LIFE_SAMPLES);
154
+ for (let s = 0; s < end; s += stride) {
155
+ const frame = Math.floor(s);
156
+ // Depth-first from the root, carrying "every ancestor down here draws".
157
+ const visit = (id: string): void => {
158
+ const layer = scene.layersById[id];
159
+ if (layer === undefined || !layerAliveAt(layer, frame)) return;
160
+ addMember(bounds, scene, rootPose, id, frame, fps, meshBounds);
161
+ for (const child of layerChildren(layer)) visit(child);
162
+ };
163
+ visit(rootId);
164
+ }
165
+ return Number.isFinite(bounds.minX)
166
+ ? frameOf(rootPose, bounds)
167
+ : isolationFrame(scene, rootId, subtreeOf(scene, rootId), anchorFrame, fps, meshBounds);
168
+ }
67
169
 
68
- let minX = Infinity;
69
- let maxX = -Infinity;
70
- let minY = Infinity;
71
- let maxY = -Infinity;
72
- let minZ = Infinity;
73
- let maxZ = -Infinity;
74
- for (const id of subtree) {
170
+ /** `rootId` and every layer under it, through the one child walk. */
171
+ function subtreeOf(scene: LayerLookup, rootId: string): Set<string> {
172
+ const out = new Set<string>();
173
+ const visit = (id: string): void => {
75
174
  const layer = scene.layersById[id];
76
- if (layer === undefined) continue;
77
- // Each member's centre in the ROOT's frame, with its own axis-aligned box
78
- // around it. The box ignores the member's own tilt and roll, the same
79
- // simplification `groupLayers` makes for the same reason: a rotation-aware
80
- // hull would frame a rolled child looser than the box the author sees.
81
- const local = toLocal(rootPose, worldStateAt(scene, id, sceneFrame, fps));
82
- const [w, h] = layer.transform.size;
83
- minX = Math.min(minX, local.position[0] - w / 2);
84
- maxX = Math.max(maxX, local.position[0] + w / 2);
85
- minY = Math.min(minY, local.position[1] - h / 2);
86
- maxY = Math.max(maxY, local.position[1] + h / 2);
87
- minZ = Math.min(minZ, local.position[2]);
88
- maxZ = Math.max(maxZ, local.position[2]);
89
- }
90
- if (!Number.isFinite(minX)) return null;
175
+ if (layer === undefined || out.has(id)) return;
176
+ out.add(id);
177
+ for (const child of layerChildren(layer)) visit(child);
178
+ };
179
+ visit(rootId);
180
+ return out;
181
+ }
182
+
183
+ interface Bounds {
184
+ minX: number;
185
+ maxX: number;
186
+ minY: number;
187
+ maxY: number;
188
+ minZ: number;
189
+ maxZ: number;
190
+ }
191
+
192
+ function emptyBounds(): Bounds {
193
+ return { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity, minZ: Infinity, maxZ: -Infinity };
194
+ }
91
195
 
196
+ /** Grow `bounds` by one member's box at one frame, measured in `rootPose`. */
197
+ function addMember(
198
+ bounds: Bounds,
199
+ scene: LayerLookup,
200
+ rootPose: ReturnType<typeof worldStateAt>,
201
+ id: string,
202
+ sceneFrame: number,
203
+ fps: Rational,
204
+ meshBounds: MeshBounds | undefined,
205
+ ): void {
206
+ const layer = scene.layersById[id];
207
+ if (layer === undefined) return;
208
+ // Each member's centre in the ROOT's frame, with its own axis-aligned box
209
+ // around it. The box ignores the member's own tilt and roll, the same
210
+ // simplification `groupLayers` makes for the same reason: a rotation-aware
211
+ // hull would frame a rolled child looser than the box the author sees.
212
+ const local = toLocal(rootPose, worldStateAt(scene, id, sceneFrame, fps));
213
+ // The EFFECTIVE size, not the authored one. `worldStateAt` already resolves
214
+ // the chain, so reading `layer.transform.size` beside it framed a box the
215
+ // layer no longer occupies the moment a size track scaled it.
216
+ const [w, h] = effectiveTransform(layer, sceneFrame, fps).size;
217
+ const box = occupiedBox(layer, w, h, meshBounds?.get(id));
218
+ bounds.minX = Math.min(bounds.minX, local.position[0] - box[0] / 2);
219
+ bounds.maxX = Math.max(bounds.maxX, local.position[0] + box[0] / 2);
220
+ bounds.minY = Math.min(bounds.minY, local.position[1] - box[1] / 2);
221
+ bounds.maxY = Math.max(bounds.maxY, local.position[1] + box[1] / 2);
222
+ bounds.minZ = Math.min(bounds.minZ, local.position[2] - box[2] / 2);
223
+ bounds.maxZ = Math.max(bounds.maxZ, local.position[2] + box[2] / 2);
224
+ }
225
+
226
+ function frameOf(rootPose: ReturnType<typeof worldStateAt>, b: Bounds): IsolationFrame | null {
227
+ if (!Number.isFinite(b.minX)) return null;
92
228
  // The bounds' CENTRE, not the root's origin: a group whose children all sit
93
229
  // to one side would otherwise be framed off-centre, which is the state the
94
230
  // grouping arithmetic exists to make impossible to author but which a
95
231
  // hand-written document can still hold.
96
- const centre: Vec3 = [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2];
97
- const world = worldOf(rootPose, centre);
98
-
232
+ const centre: Vec3 = [(b.minX + b.maxX) / 2, (b.minY + b.maxY) / 2, (b.minZ + b.maxZ) / 2];
99
233
  return {
100
- target: world,
234
+ target: worldOf(rootPose, centre),
101
235
  rotation: rootPose.rotation,
102
- halfWidth: Math.max(MIN_HALF_EXTENT, (maxX - minX) / 2),
103
- halfHeight: Math.max(MIN_HALF_EXTENT, (maxY - minY) / 2),
104
- halfDepth: Math.max(MIN_HALF_EXTENT, (maxZ - minZ) / 2),
236
+ halfWidth: Math.max(MIN_HALF_EXTENT, (b.maxX - b.minX) / 2),
237
+ halfHeight: Math.max(MIN_HALF_EXTENT, (b.maxY - b.minY) / 2),
238
+ halfDepth: Math.max(MIN_HALF_EXTENT, (b.maxZ - b.minZ) / 2),
105
239
  };
106
240
  }
107
241
 
242
+ /**
243
+ * THE head-on camera: everything a consumer needs to draw, measure or hand over
244
+ * a derived frame, from one derivation.
245
+ *
246
+ * ── WHY ONE FUNCTION AND NOT THREE CALLERS OF `isolationExtent` ─────────
247
+ *
248
+ * There were three: the camera `StageTools` installs, `paneScreenScale` (which
249
+ * every drag speed and every snap threshold is measured in), and the pose the
250
+ * orbit rig is seeded with on entry. Three derivations of one number is three
251
+ * chances to disagree, and the disagreement is invisible — a drag that runs an
252
+ * order of magnitude fast looks like a bad feel, not like a bug. `pxPerUnit` is
253
+ * that number, answered once.
254
+ */
255
+ export interface HeadOnCamera {
256
+ /** Orthographic half-extents covering the WHOLE pane, in design px. */
257
+ halfWidth: number;
258
+ halfHeight: number;
259
+ near: number;
260
+ far: number;
261
+ eye: Vec3;
262
+ up: Vec3;
263
+ /** Where the camera looks: the subject, slid so it lands centred in the
264
+ * VISIBLE box rather than in the pane. */
265
+ target: Vec3;
266
+ /** CSS px per design px. The one scale every consumer measures with. */
267
+ pxPerUnit: number;
268
+ }
269
+
270
+ /**
271
+ * Frame a subtree head-on inside a pane the chrome is partly covering.
272
+ *
273
+ * The subject is fitted to `visible`; the frustum is then grown to cover `pane`
274
+ * AT THE SAME SCALE, and the target slid so the subject sits in the middle of
275
+ * the hole. So the picture behind the chrome is simply extra: nothing is
276
+ * scaled to fit furniture, and nothing hides under it.
277
+ *
278
+ * Pass the same rect twice for the un-inset behaviour (the tests, and any
279
+ * caller that genuinely owns its whole pane).
280
+ */
281
+ export function headOnCamera(
282
+ frame: IsolationFrame,
283
+ pane: PaneRect,
284
+ visible: PaneRect,
285
+ ): HeadOnCamera {
286
+ const box = visible.width > 0 && visible.height > 0 ? visible : pane;
287
+ const fitted = isolationExtent(frame, box.height === 0 ? 1 : box.width / box.height);
288
+ // The scale the fit implies, and the only place it is decided.
289
+ const pxPerUnit = fitted.halfHeight > 0 ? box.height / (2 * fitted.halfHeight) : 1;
290
+ const halfWidth = pane.width / (2 * pxPerUnit);
291
+ const halfHeight = pane.height / (2 * pxPerUnit);
292
+
293
+ // ── THE SLIDE ────────────────────────────────────────────────────────────
294
+ //
295
+ // A camera looks at the middle of its pane, so the subject lands there. Push
296
+ // the target the OTHER way by however far the visible box's centre is from
297
+ // the pane's, and the subject lands in the visible box's middle instead.
298
+ //
299
+ // The axes are the frame's OWN basis, so this works for a rolled or a
300
+ // face-down subject exactly as it does for a flat one: column 0 is screen
301
+ // right and column 1 is screen up (`worldOf` maps design +x to column 0).
302
+ // Pane y runs DOWN, world up runs UP, which is why the two signs differ.
303
+ const dx = (visible.x + visible.width / 2 - (pane.x + pane.width / 2)) / pxPerUnit;
304
+ const dy = (visible.y + visible.height / 2 - (pane.y + pane.height / 2)) / pxPerUnit;
305
+ const right: Vec3 = [frame.rotation[0][0], frame.rotation[1][0], frame.rotation[2][0]];
306
+ const { eye, up, far } = isolationEye(frame);
307
+ const target: Vec3 = [
308
+ frame.target[0] - right[0] * dx + up[0] * dy,
309
+ frame.target[1] - right[1] * dx + up[1] * dy,
310
+ frame.target[2] - right[2] * dx + up[2] * dy,
311
+ ];
312
+ // The eye rides the target. The slide is perpendicular to the view normal, so
313
+ // the depth slab `isolationEye` sized is unchanged by it.
314
+ return {
315
+ halfWidth,
316
+ halfHeight,
317
+ near: 0.1,
318
+ far,
319
+ eye: [
320
+ eye[0] + (target[0] - frame.target[0]),
321
+ eye[1] + (target[1] - frame.target[1]),
322
+ eye[2] + (target[2] - frame.target[2]),
323
+ ],
324
+ up,
325
+ target,
326
+ pxPerUnit,
327
+ };
328
+ }
329
+
330
+ /**
331
+ * What a layer actually FILLS of its authored box, as a `[w, h, d]` extent.
332
+ *
333
+ * For everything but a loaded model that is the box itself, flat. A model is a
334
+ * different story: `applyFit` (shot/mesh.tsx) contain-fits the bind-pose box
335
+ * into the authored one, so a tall model in a square box paints a tall sliver
336
+ * and the two side thirds are empty. This mirrors that arithmetic exactly —
337
+ * the same `min(w/bx, h/by)`, the same per-axis `stretch`, the same "z takes
338
+ * the smaller of the two" — so the camera frames the MODEL and not its crate.
339
+ *
340
+ * Depth comes back with it, which is the second thing the old box could not
341
+ * say: a flat subtree used to collapse to `MIN_HALF_EXTENT` and size the ortho
342
+ * slab off a guess.
343
+ *
344
+ * ponytail: a primitive reports no bounds and falls back to its authored box.
345
+ * That is right for authored shapes (a Rect IS its box) and slightly generous
346
+ * for a non-uniformly scaled primitive, whose `primitiveFrameScale` is uniform.
347
+ * Upgrade by consulting the primitive's own params if it ever reads loose.
348
+ */
349
+ function occupiedBox(
350
+ layer: { kind: string; mesh?: { fit?: "fit" | "stretch" } },
351
+ w: number,
352
+ h: number,
353
+ bind: readonly [number, number, number] | undefined,
354
+ ): readonly [number, number, number] {
355
+ if (layer.kind !== "mesh" || bind === undefined) return [w, h, 0];
356
+ const [bx, by, bz] = bind;
357
+ // A degenerate axis is a flat or empty model; `applyFit` leaves it at scale 1
358
+ // rather than dividing by zero, and so must this.
359
+ if (!(bx > 0 && by > 0)) return [w, h, 0];
360
+ const uniform = Math.min(w / bx, h / by);
361
+ const sx = layer.mesh?.fit === "stretch" ? w / bx : uniform;
362
+ const sy = layer.mesh?.fit === "stretch" ? h / by : uniform;
363
+ return [bx * sx, by * sy, bz * Math.min(sx, sy)];
364
+ }
365
+
108
366
  /** A point in the root's DESIGN frame, out in three world space. Design +y is
109
367
  * down and +z is away, so both flip on the way through the one mapping. */
110
368
  function worldOf(pose: { position: Vec3; rotation: Mat3 }, local: Vec3): Vec3 {
@@ -0,0 +1,325 @@
1
+ // Chrome's JS Self-Profiling API, exposed only to the explicit local
2
+ // `?perf=profile` diagnostics page. The browser starts sampling in the
3
+ // Profiler constructor, so keeping construction behind both URL and caller
4
+ // gates is what makes normal editor, hosted, and export pages pay zero cost.
5
+
6
+ export const JS_SELF_PROFILE_QUERY_VALUE = "profile";
7
+ export const JS_SELF_PROFILE_SAMPLE_INTERVAL_MS = 5;
8
+ export const JS_SELF_PROFILE_MAX_BUFFER_SIZE = 4_096;
9
+ export const JS_SELF_PROFILE_CAPTURE_VERSION = 1;
10
+
11
+ export interface JsProfilerFrame {
12
+ name: string;
13
+ resourceId?: number;
14
+ line?: number;
15
+ column?: number;
16
+ }
17
+
18
+ export interface JsProfilerStack {
19
+ frameId: number;
20
+ parentId?: number;
21
+ }
22
+
23
+ export interface JsProfilerSample {
24
+ timestamp: number;
25
+ stackId?: number;
26
+ }
27
+
28
+ export interface JsProfilerTrace {
29
+ resources: readonly string[];
30
+ frames: readonly JsProfilerFrame[];
31
+ stacks: readonly JsProfilerStack[];
32
+ samples: readonly JsProfilerSample[];
33
+ }
34
+
35
+ interface NativeProfiler extends EventTarget {
36
+ readonly sampleInterval: number;
37
+ readonly stopped: boolean;
38
+ stop(): Promise<JsProfilerTrace>;
39
+ }
40
+
41
+ export interface NativeProfilerConstructor {
42
+ new (options: { sampleInterval: number; maxBufferSize: number }): NativeProfiler;
43
+ }
44
+
45
+ export type JsSelfProfileRefusalCode =
46
+ | "unsupported"
47
+ | "not-allowed"
48
+ | "already-running"
49
+ | "not-running"
50
+ | "buffer-full"
51
+ | "invalid-trace"
52
+ | "empty-trace"
53
+ | "stop-failed";
54
+
55
+ export interface JsSelfProfileRefusal {
56
+ ok: false;
57
+ code: JsSelfProfileRefusalCode;
58
+ message: string;
59
+ }
60
+
61
+ export interface JsSelfProfileStarted {
62
+ ok: true;
63
+ requestedSampleIntervalMs: number;
64
+ actualSampleIntervalMs: number;
65
+ maxBufferSize: number;
66
+ startedAt: number;
67
+ }
68
+
69
+ export interface JsSelfProfileCapture {
70
+ version: typeof JS_SELF_PROFILE_CAPTURE_VERSION;
71
+ requestedSampleIntervalMs: number;
72
+ actualSampleIntervalMs: number;
73
+ maxBufferSize: number;
74
+ startedAt: number;
75
+ stoppedAt: number;
76
+ sampleCount: number;
77
+ /** Serialized in the profiled realm before Playwright transports it. */
78
+ traceJson: string;
79
+ }
80
+
81
+ export type JsSelfProfileStartResult = JsSelfProfileStarted | JsSelfProfileRefusal;
82
+ export type JsSelfProfileStopResult =
83
+ | { ok: true; capture: JsSelfProfileCapture }
84
+ | JsSelfProfileRefusal;
85
+
86
+ export interface JsSelfProfilerController {
87
+ status(): {
88
+ enabled: true;
89
+ supported: boolean;
90
+ running: boolean;
91
+ message: string;
92
+ };
93
+ start(): JsSelfProfileStartResult;
94
+ stop(): Promise<JsSelfProfileStopResult>;
95
+ }
96
+
97
+ export interface JsSelfProfilerEnvironment {
98
+ readonly Profiler?: NativeProfilerConstructor;
99
+ now(): number;
100
+ stringify(value: unknown): string;
101
+ }
102
+
103
+ interface ActiveProfile {
104
+ profiler: NativeProfiler;
105
+ started: JsSelfProfileStarted;
106
+ bufferFull: boolean;
107
+ stopPromise: Promise<JsSelfProfileStopResult> | null;
108
+ }
109
+
110
+ export function isLoopbackHostname(hostname: string): boolean {
111
+ const host = hostname.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
112
+ return host === "localhost" || host === "::1" || /^127(?:\.\d{1,3}){3}$/.test(host);
113
+ }
114
+
115
+ /** Both conditions are mandatory. A copied hosted URL carrying the query does
116
+ * not install a profiler controller, even if its server accidentally emitted
117
+ * a permissive policy header. */
118
+ export function isLocalJsSelfProfileOptIn(url: string): boolean {
119
+ try {
120
+ const parsed = new URL(url);
121
+ const values = parsed.searchParams.getAll("perf");
122
+ return (
123
+ isLoopbackHostname(parsed.hostname) &&
124
+ values.length === 1 &&
125
+ values[0] === JS_SELF_PROFILE_QUERY_VALUE
126
+ );
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ export function browserJsSelfProfilerEnvironment(): JsSelfProfilerEnvironment {
133
+ const Profiler = (globalThis as typeof globalThis & { Profiler?: NativeProfilerConstructor })
134
+ .Profiler;
135
+ return {
136
+ ...(Profiler === undefined ? {} : { Profiler }),
137
+ now: () => performance.now(),
138
+ stringify: (value) => JSON.stringify(value),
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Creating the controller is inert. `start()` is the sole constructor call,
144
+ * and the fixed interval/cap cannot be widened by a console caller or harness
145
+ * argument. One controller can run several sequential captures so fresh,
146
+ * repeat, and post-idle comparisons can reuse one warm page.
147
+ */
148
+ export function createJsSelfProfilerController(
149
+ env: JsSelfProfilerEnvironment = browserJsSelfProfilerEnvironment(),
150
+ ): JsSelfProfilerController {
151
+ let active: ActiveProfile | null = null;
152
+
153
+ const refuse = (code: JsSelfProfileRefusalCode, message: string): JsSelfProfileRefusal => ({
154
+ ok: false,
155
+ code,
156
+ message,
157
+ });
158
+
159
+ const settle = (session: ActiveProfile): Promise<JsSelfProfileStopResult> => {
160
+ if (session.stopPromise !== null) return session.stopPromise;
161
+ session.stopPromise = (async () => {
162
+ // This is the capture boundary. Awaiting Chrome's stop result and
163
+ // validating/stringifying its trace are post-capture bookkeeping and
164
+ // must not extend the last frame cycle in the artifact.
165
+ const stoppedAt = env.now();
166
+ let trace: JsProfilerTrace;
167
+ try {
168
+ trace = await session.profiler.stop();
169
+ } catch (error) {
170
+ const detail = error instanceof Error ? error.message : String(error);
171
+ return refuse(
172
+ session.bufferFull ? "buffer-full" : "stop-failed",
173
+ session.bufferFull
174
+ ? `JS self-profile sample buffer filled before capture completed (${detail})`
175
+ : `JS self-profile could not stop: ${detail}`,
176
+ );
177
+ }
178
+ if (!validTraceShape(trace)) {
179
+ return refuse("invalid-trace", "Chrome returned a malformed JS self-profile trace");
180
+ }
181
+ if (trace.samples.length > JS_SELF_PROFILE_MAX_BUFFER_SIZE) {
182
+ return refuse(
183
+ "invalid-trace",
184
+ `Chrome returned ${trace.samples.length} samples, above the ${JS_SELF_PROFILE_MAX_BUFFER_SIZE}-sample cap`,
185
+ );
186
+ }
187
+ if (
188
+ session.bufferFull ||
189
+ trace.samples.length === JS_SELF_PROFILE_MAX_BUFFER_SIZE
190
+ ) {
191
+ return refuse(
192
+ "buffer-full",
193
+ `JS self-profile reached its ${JS_SELF_PROFILE_MAX_BUFFER_SIZE}-sample cap before the requested frame window completed`,
194
+ );
195
+ }
196
+ if (trace.samples.length === 0) {
197
+ return refuse(
198
+ "empty-trace",
199
+ "Chrome returned no JS samples; refusing to report an unsupported capture as complete",
200
+ );
201
+ }
202
+ let traceJson: string;
203
+ try {
204
+ traceJson = env.stringify(trace);
205
+ } catch (error) {
206
+ const detail = error instanceof Error ? error.message : String(error);
207
+ return refuse("invalid-trace", `JS self-profile trace is not serializable: ${detail}`);
208
+ }
209
+ return {
210
+ ok: true,
211
+ capture: {
212
+ version: JS_SELF_PROFILE_CAPTURE_VERSION,
213
+ requestedSampleIntervalMs: session.started.requestedSampleIntervalMs,
214
+ actualSampleIntervalMs: session.started.actualSampleIntervalMs,
215
+ maxBufferSize: session.started.maxBufferSize,
216
+ startedAt: session.started.startedAt,
217
+ stoppedAt,
218
+ sampleCount: trace.samples.length,
219
+ traceJson,
220
+ },
221
+ };
222
+ })();
223
+ return session.stopPromise;
224
+ };
225
+
226
+ return {
227
+ status() {
228
+ const supported = env.Profiler !== undefined;
229
+ return {
230
+ enabled: true,
231
+ supported,
232
+ running: active !== null,
233
+ message: supported
234
+ ? active === null
235
+ ? "JS self-profiling is ready"
236
+ : "JS self-profiling is running"
237
+ : "Chrome JS Self-Profiling is unavailable; use a Chromium build that exposes window.Profiler",
238
+ };
239
+ },
240
+ start() {
241
+ if (active !== null) {
242
+ return refuse("already-running", "a JS self-profile capture is already running");
243
+ }
244
+ const Profiler = env.Profiler;
245
+ if (Profiler === undefined) {
246
+ return refuse(
247
+ "unsupported",
248
+ "Chrome JS Self-Profiling is unavailable; use a Chromium build that exposes window.Profiler",
249
+ );
250
+ }
251
+ const startedAt = env.now();
252
+ let profiler: NativeProfiler;
253
+ try {
254
+ profiler = new Profiler({
255
+ sampleInterval: JS_SELF_PROFILE_SAMPLE_INTERVAL_MS,
256
+ maxBufferSize: JS_SELF_PROFILE_MAX_BUFFER_SIZE,
257
+ });
258
+ } catch (error) {
259
+ const detail = error instanceof Error ? error.message : String(error);
260
+ const denied =
261
+ (typeof DOMException !== "undefined" &&
262
+ error instanceof DOMException &&
263
+ error.name === "NotAllowedError") ||
264
+ /document policy|not allowed/i.test(detail);
265
+ return refuse(
266
+ denied ? "not-allowed" : "unsupported",
267
+ denied
268
+ ? `JS self-profiling was denied; reload the local stage with ?perf=profile and Document-Policy: js-profiling (${detail})`
269
+ : `Chrome JS Self-Profiling could not start: ${detail}`,
270
+ );
271
+ }
272
+ const actualSampleIntervalMs = profiler.sampleInterval;
273
+ if (!Number.isFinite(actualSampleIntervalMs) || actualSampleIntervalMs <= 0) {
274
+ void profiler.stop().catch(() => {});
275
+ return refuse(
276
+ "unsupported",
277
+ `Chrome reported an invalid JS self-profile interval (${String(actualSampleIntervalMs)})`,
278
+ );
279
+ }
280
+ const started: JsSelfProfileStarted = {
281
+ ok: true,
282
+ requestedSampleIntervalMs: JS_SELF_PROFILE_SAMPLE_INTERVAL_MS,
283
+ actualSampleIntervalMs,
284
+ maxBufferSize: JS_SELF_PROFILE_MAX_BUFFER_SIZE,
285
+ startedAt,
286
+ };
287
+ const session: ActiveProfile = {
288
+ profiler,
289
+ started,
290
+ bufferFull: false,
291
+ stopPromise: null,
292
+ };
293
+ profiler.addEventListener(
294
+ "samplebufferfull",
295
+ () => {
296
+ session.bufferFull = true;
297
+ void settle(session);
298
+ },
299
+ { once: true },
300
+ );
301
+ active = session;
302
+ return started;
303
+ },
304
+ async stop() {
305
+ const session = active;
306
+ if (session === null) {
307
+ return refuse("not-running", "no JS self-profile capture is running");
308
+ }
309
+ const result = await settle(session);
310
+ if (active === session) active = null;
311
+ return result;
312
+ },
313
+ };
314
+ }
315
+
316
+ function validTraceShape(trace: unknown): trace is JsProfilerTrace {
317
+ if (trace === null || typeof trace !== "object") return false;
318
+ const row = trace as Partial<JsProfilerTrace>;
319
+ return (
320
+ Array.isArray(row.resources) &&
321
+ Array.isArray(row.frames) &&
322
+ Array.isArray(row.stacks) &&
323
+ Array.isArray(row.samples)
324
+ );
325
+ }