@editframe/elements 0.30.2-beta.0 → 0.31.0-beta.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 (324) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +5 -0
  2. package/dist/EF_FRAMEGEN.js +20 -4
  3. package/dist/EF_FRAMEGEN.js.map +1 -1
  4. package/dist/EF_INTERACTIVE.js.map +1 -1
  5. package/dist/_virtual/rolldown_runtime.js +27 -0
  6. package/dist/canvas/EFCanvas.d.ts +311 -0
  7. package/dist/canvas/EFCanvas.js +1089 -0
  8. package/dist/canvas/EFCanvas.js.map +1 -0
  9. package/dist/canvas/EFCanvasItem.d.ts +55 -0
  10. package/dist/canvas/EFCanvasItem.js +72 -0
  11. package/dist/canvas/EFCanvasItem.js.map +1 -0
  12. package/dist/canvas/api/CanvasAPI.d.ts +115 -0
  13. package/dist/canvas/api/CanvasAPI.js +182 -0
  14. package/dist/canvas/api/CanvasAPI.js.map +1 -0
  15. package/dist/canvas/api/types.d.ts +42 -0
  16. package/dist/canvas/coordinateTransform.js +90 -0
  17. package/dist/canvas/coordinateTransform.js.map +1 -0
  18. package/dist/canvas/getElementBounds.js +40 -0
  19. package/dist/canvas/getElementBounds.js.map +1 -0
  20. package/dist/canvas/overlays/SelectionOverlay.js +265 -0
  21. package/dist/canvas/overlays/SelectionOverlay.js.map +1 -0
  22. package/dist/canvas/overlays/overlayState.js +153 -0
  23. package/dist/canvas/overlays/overlayState.js.map +1 -0
  24. package/dist/canvas/selection/SelectionController.js +105 -0
  25. package/dist/canvas/selection/SelectionController.js.map +1 -0
  26. package/dist/canvas/selection/SelectionModel.d.ts +98 -0
  27. package/dist/canvas/selection/SelectionModel.js +229 -0
  28. package/dist/canvas/selection/SelectionModel.js.map +1 -0
  29. package/dist/canvas/selection/selectionContext.d.ts +31 -0
  30. package/dist/canvas/selection/selectionContext.js +12 -0
  31. package/dist/canvas/selection/selectionContext.js.map +1 -0
  32. package/dist/elements/ContainerInfo.d.ts +29 -0
  33. package/dist/elements/ContainerInfo.js +30 -0
  34. package/dist/elements/ContainerInfo.js.map +1 -0
  35. package/dist/elements/EFAudio.d.ts +13 -3
  36. package/dist/elements/EFAudio.js +64 -10
  37. package/dist/elements/EFAudio.js.map +1 -1
  38. package/dist/elements/EFCaptions.d.ts +18 -16
  39. package/dist/elements/EFCaptions.js +110 -19
  40. package/dist/elements/EFCaptions.js.map +1 -1
  41. package/dist/elements/EFImage.d.ts +12 -2
  42. package/dist/elements/EFImage.js +79 -9
  43. package/dist/elements/EFImage.js.map +1 -1
  44. package/dist/elements/EFMedia/AssetIdMediaEngine.js +51 -4
  45. package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
  46. package/dist/elements/EFMedia/AssetMediaEngine.js +125 -52
  47. package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
  48. package/dist/elements/EFMedia/BaseMediaEngine.js +24 -6
  49. package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
  50. package/dist/elements/EFMedia/JitMediaEngine.js +12 -8
  51. package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
  52. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +46 -7
  53. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
  54. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +98 -73
  55. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +1 -1
  56. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +28 -5
  57. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +1 -1
  58. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +18 -6
  59. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +1 -1
  60. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +8 -2
  61. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +1 -1
  62. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +31 -6
  63. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +1 -1
  64. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +28 -5
  65. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +1 -1
  66. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +97 -72
  67. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +1 -1
  68. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -1
  69. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  70. package/dist/elements/EFMedia/shared/BufferUtils.js +1 -1
  71. package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
  72. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +25 -14
  73. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  74. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +47 -16
  75. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +1 -1
  76. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +37 -19
  77. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
  78. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +65 -21
  79. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
  80. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +8 -3
  81. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +1 -1
  82. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +32 -9
  83. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +1 -1
  84. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +33 -10
  85. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +1 -1
  86. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +23 -8
  87. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +1 -1
  88. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +34 -10
  89. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +1 -1
  90. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +31 -8
  91. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +1 -1
  92. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +31 -114
  93. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +1 -1
  94. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +44 -8
  95. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
  96. package/dist/elements/EFMedia.d.ts +18 -7
  97. package/dist/elements/EFMedia.js +23 -3
  98. package/dist/elements/EFMedia.js.map +1 -1
  99. package/dist/elements/EFPanZoom.d.ts +96 -0
  100. package/dist/elements/EFPanZoom.js +290 -0
  101. package/dist/elements/EFPanZoom.js.map +1 -0
  102. package/dist/elements/EFSourceMixin.js +7 -6
  103. package/dist/elements/EFSourceMixin.js.map +1 -1
  104. package/dist/elements/EFSurface.d.ts +6 -6
  105. package/dist/elements/EFSurface.js +7 -2
  106. package/dist/elements/EFSurface.js.map +1 -1
  107. package/dist/elements/EFTemporal.d.ts +2 -1
  108. package/dist/elements/EFTemporal.js +192 -71
  109. package/dist/elements/EFTemporal.js.map +1 -1
  110. package/dist/elements/EFText.d.ts +5 -4
  111. package/dist/elements/EFText.js +102 -13
  112. package/dist/elements/EFText.js.map +1 -1
  113. package/dist/elements/EFTextSegment.d.ts +32 -6
  114. package/dist/elements/EFTextSegment.js +53 -15
  115. package/dist/elements/EFTextSegment.js.map +1 -1
  116. package/dist/elements/EFThumbnailStrip.d.ts +118 -56
  117. package/dist/elements/EFThumbnailStrip.js +522 -358
  118. package/dist/elements/EFThumbnailStrip.js.map +1 -1
  119. package/dist/elements/EFTimegroup.d.ts +223 -27
  120. package/dist/elements/EFTimegroup.js +850 -147
  121. package/dist/elements/EFTimegroup.js.map +1 -1
  122. package/dist/elements/EFVideo.d.ts +42 -5
  123. package/dist/elements/EFVideo.js +165 -11
  124. package/dist/elements/EFVideo.js.map +1 -1
  125. package/dist/elements/EFWaveform.d.ts +6 -6
  126. package/dist/elements/EFWaveform.js +2 -1
  127. package/dist/elements/EFWaveform.js.map +1 -1
  128. package/dist/elements/ElementPositionInfo.d.ts +35 -0
  129. package/dist/elements/ElementPositionInfo.js +49 -0
  130. package/dist/elements/ElementPositionInfo.js.map +1 -0
  131. package/dist/elements/FetchMixin.js +16 -1
  132. package/dist/elements/FetchMixin.js.map +1 -1
  133. package/dist/elements/SessionThumbnailCache.js +152 -0
  134. package/dist/elements/SessionThumbnailCache.js.map +1 -0
  135. package/dist/elements/TargetController.js +3 -1
  136. package/dist/elements/TargetController.js.map +1 -1
  137. package/dist/elements/TimegroupController.js +9 -3
  138. package/dist/elements/TimegroupController.js.map +1 -1
  139. package/dist/elements/findRootTemporal.js +30 -0
  140. package/dist/elements/findRootTemporal.js.map +1 -0
  141. package/dist/elements/renderTemporalAudio.js +18 -5
  142. package/dist/elements/renderTemporalAudio.js.map +1 -1
  143. package/dist/elements/updateAnimations.js +171 -28
  144. package/dist/elements/updateAnimations.js.map +1 -1
  145. package/dist/getRenderInfo.d.ts +2 -2
  146. package/dist/gui/ContextMixin.js +4 -2
  147. package/dist/gui/ContextMixin.js.map +1 -1
  148. package/dist/gui/Controllable.js +74 -1
  149. package/dist/gui/Controllable.js.map +1 -1
  150. package/dist/gui/EFActiveRootTemporal.d.ts +50 -0
  151. package/dist/gui/EFActiveRootTemporal.js +94 -0
  152. package/dist/gui/EFActiveRootTemporal.js.map +1 -0
  153. package/dist/gui/EFConfiguration.d.ts +11 -5
  154. package/dist/gui/EFConfiguration.js.map +1 -1
  155. package/dist/gui/EFControls.d.ts +2 -2
  156. package/dist/gui/EFControls.js +109 -13
  157. package/dist/gui/EFControls.js.map +1 -1
  158. package/dist/gui/EFDial.d.ts +4 -4
  159. package/dist/gui/EFFilmstrip.d.ts +11 -214
  160. package/dist/gui/EFFilmstrip.js +53 -1152
  161. package/dist/gui/EFFilmstrip.js.map +1 -1
  162. package/dist/gui/EFFitScale.d.ts +3 -3
  163. package/dist/gui/EFFitScale.js +39 -12
  164. package/dist/gui/EFFitScale.js.map +1 -1
  165. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  166. package/dist/gui/EFOverlayItem.d.ts +48 -0
  167. package/dist/gui/EFOverlayItem.js +97 -0
  168. package/dist/gui/EFOverlayItem.js.map +1 -0
  169. package/dist/gui/EFOverlayLayer.d.ts +70 -0
  170. package/dist/gui/EFOverlayLayer.js +104 -0
  171. package/dist/gui/EFOverlayLayer.js.map +1 -0
  172. package/dist/gui/EFPause.d.ts +4 -4
  173. package/dist/gui/EFPlay.d.ts +4 -4
  174. package/dist/gui/EFResizableBox.d.ts +12 -16
  175. package/dist/gui/EFResizableBox.js +109 -451
  176. package/dist/gui/EFResizableBox.js.map +1 -1
  177. package/dist/gui/EFScrubber.d.ts +30 -5
  178. package/dist/gui/EFScrubber.js +224 -31
  179. package/dist/gui/EFScrubber.js.map +1 -1
  180. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  181. package/dist/gui/EFTimeDisplay.js +4 -1
  182. package/dist/gui/EFTimeDisplay.js.map +1 -1
  183. package/dist/gui/EFTimelineRuler.d.ts +71 -0
  184. package/dist/gui/EFTimelineRuler.js +320 -0
  185. package/dist/gui/EFTimelineRuler.js.map +1 -0
  186. package/dist/gui/EFToggleLoop.d.ts +4 -4
  187. package/dist/gui/EFTogglePlay.d.ts +4 -4
  188. package/dist/gui/EFTransformHandles.d.ts +91 -0
  189. package/dist/gui/EFTransformHandles.js +393 -0
  190. package/dist/gui/EFTransformHandles.js.map +1 -0
  191. package/dist/gui/EFWorkbench.d.ts +182 -4
  192. package/dist/gui/EFWorkbench.js +2067 -22
  193. package/dist/gui/EFWorkbench.js.map +1 -1
  194. package/dist/gui/FitScaleHelpers.d.ts +31 -0
  195. package/dist/gui/FitScaleHelpers.js +41 -0
  196. package/dist/gui/FitScaleHelpers.js.map +1 -0
  197. package/dist/gui/PlaybackController.d.ts +2 -1
  198. package/dist/gui/PlaybackController.js +46 -15
  199. package/dist/gui/PlaybackController.js.map +1 -1
  200. package/dist/gui/TWMixin.js +1 -1
  201. package/dist/gui/TWMixin.js.map +1 -1
  202. package/dist/gui/hierarchy/EFHierarchy.d.ts +65 -0
  203. package/dist/gui/hierarchy/EFHierarchy.js +338 -0
  204. package/dist/gui/hierarchy/EFHierarchy.js.map +1 -0
  205. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +118 -0
  206. package/dist/gui/hierarchy/EFHierarchyItem.js +551 -0
  207. package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -0
  208. package/dist/gui/hierarchy/hierarchyContext.d.ts +38 -0
  209. package/dist/gui/hierarchy/hierarchyContext.js +8 -0
  210. package/dist/gui/hierarchy/hierarchyContext.js.map +1 -0
  211. package/dist/gui/icons.js +34 -0
  212. package/dist/gui/icons.js.map +1 -0
  213. package/dist/gui/panZoomTransformContext.js +12 -0
  214. package/dist/gui/panZoomTransformContext.js.map +1 -0
  215. package/dist/gui/previewSettingsContext.js +12 -0
  216. package/dist/gui/previewSettingsContext.js.map +1 -0
  217. package/dist/gui/timeline/EFTimeline.d.ts +270 -0
  218. package/dist/gui/timeline/EFTimeline.js +1369 -0
  219. package/dist/gui/timeline/EFTimeline.js.map +1 -0
  220. package/dist/gui/timeline/EFTimelineRow.js +374 -0
  221. package/dist/gui/timeline/EFTimelineRow.js.map +1 -0
  222. package/dist/gui/timeline/TrimHandles.d.ts +36 -0
  223. package/dist/gui/timeline/TrimHandles.js +204 -0
  224. package/dist/gui/timeline/TrimHandles.js.map +1 -0
  225. package/dist/gui/timeline/flattenHierarchy.js +31 -0
  226. package/dist/gui/timeline/flattenHierarchy.js.map +1 -0
  227. package/dist/gui/timeline/timelineStateContext.d.ts +26 -0
  228. package/dist/gui/timeline/timelineStateContext.js +42 -0
  229. package/dist/gui/timeline/timelineStateContext.js.map +1 -0
  230. package/dist/gui/timeline/tracks/AudioTrack.js +264 -0
  231. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -0
  232. package/dist/gui/timeline/tracks/CaptionsTrack.js +595 -0
  233. package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -0
  234. package/dist/gui/timeline/tracks/HTMLTrack.js +19 -0
  235. package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -0
  236. package/dist/gui/timeline/tracks/ImageTrack.js +53 -0
  237. package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -0
  238. package/dist/gui/timeline/tracks/TextTrack.js +250 -0
  239. package/dist/gui/timeline/tracks/TextTrack.js.map +1 -0
  240. package/dist/gui/timeline/tracks/TimegroupTrack.js +143 -0
  241. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -0
  242. package/dist/gui/timeline/tracks/TrackItem.js +269 -0
  243. package/dist/gui/timeline/tracks/TrackItem.js.map +1 -0
  244. package/dist/gui/timeline/tracks/VideoTrack.js +265 -0
  245. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -0
  246. package/dist/gui/timeline/tracks/WaveformTrack.js +19 -0
  247. package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -0
  248. package/dist/gui/timeline/tracks/ensureTrackItemInit.js +1 -0
  249. package/dist/gui/timeline/tracks/preloadTracks.js +9 -0
  250. package/dist/gui/timeline/tracks/renderTrackChildren.js +119 -0
  251. package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -0
  252. package/dist/gui/timeline/tracks/waveformUtils.js +80 -0
  253. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -0
  254. package/dist/gui/transformCalculations.js +217 -0
  255. package/dist/gui/transformCalculations.js.map +1 -0
  256. package/dist/gui/transformUtils.d.ts +37 -0
  257. package/dist/gui/transformUtils.js +77 -0
  258. package/dist/gui/transformUtils.js.map +1 -0
  259. package/dist/gui/tree/EFTree.d.ts +59 -0
  260. package/dist/gui/tree/EFTree.js +174 -0
  261. package/dist/gui/tree/EFTree.js.map +1 -0
  262. package/dist/gui/tree/EFTreeItem.d.ts +38 -0
  263. package/dist/gui/tree/EFTreeItem.js +146 -0
  264. package/dist/gui/tree/EFTreeItem.js.map +1 -0
  265. package/dist/gui/tree/treeContext.d.ts +60 -0
  266. package/dist/gui/tree/treeContext.js +23 -0
  267. package/dist/gui/tree/treeContext.js.map +1 -0
  268. package/dist/index.d.ts +32 -8
  269. package/dist/index.js +30 -6
  270. package/dist/index.js.map +1 -1
  271. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +688 -0
  272. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
  273. package/dist/node_modules/react/cjs/react.development.js +1521 -0
  274. package/dist/node_modules/react/cjs/react.development.js.map +1 -0
  275. package/dist/node_modules/react/index.js +13 -0
  276. package/dist/node_modules/react/index.js.map +1 -0
  277. package/dist/node_modules/react/jsx-runtime.js +13 -0
  278. package/dist/node_modules/react/jsx-runtime.js.map +1 -0
  279. package/dist/preview/AdaptiveResolutionTracker.js +228 -0
  280. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -0
  281. package/dist/preview/RenderProfiler.js +135 -0
  282. package/dist/preview/RenderProfiler.js.map +1 -0
  283. package/dist/preview/previewSettings.js +131 -0
  284. package/dist/preview/previewSettings.js.map +1 -0
  285. package/dist/preview/previewTypes.js +64 -0
  286. package/dist/preview/previewTypes.js.map +1 -0
  287. package/dist/preview/renderTimegroupPreview.js +656 -0
  288. package/dist/preview/renderTimegroupPreview.js.map +1 -0
  289. package/dist/preview/renderTimegroupToCanvas.d.ts +37 -0
  290. package/dist/preview/renderTimegroupToCanvas.js +840 -0
  291. package/dist/preview/renderTimegroupToCanvas.js.map +1 -0
  292. package/dist/preview/renderTimegroupToVideo.d.ts +39 -0
  293. package/dist/preview/renderTimegroupToVideo.js +274 -0
  294. package/dist/preview/renderTimegroupToVideo.js.map +1 -0
  295. package/dist/preview/renderers.js +16 -0
  296. package/dist/preview/renderers.js.map +1 -0
  297. package/dist/preview/statsTrackingStrategy.js +201 -0
  298. package/dist/preview/statsTrackingStrategy.js.map +1 -0
  299. package/dist/preview/thumbnailCacheSettings.js +52 -0
  300. package/dist/preview/thumbnailCacheSettings.js.map +1 -0
  301. package/dist/preview/workers/WorkerPool.js +178 -0
  302. package/dist/preview/workers/WorkerPool.js.map +1 -0
  303. package/dist/sandbox/PlaybackControls.js +10 -0
  304. package/dist/sandbox/PlaybackControls.js.map +1 -0
  305. package/dist/sandbox/ScenarioRunner.js +1 -0
  306. package/dist/sandbox/index.js +2 -0
  307. package/dist/style.css +68 -67
  308. package/dist/transcoding/types/index.d.ts +2 -1
  309. package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
  310. package/dist/transcoding/utils/UrlGenerator.js +12 -3
  311. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  312. package/dist/utils/LRUCache.js +1 -375
  313. package/dist/utils/LRUCache.js.map +1 -1
  314. package/dist/utils/frameTime.js +14 -0
  315. package/dist/utils/frameTime.js.map +1 -0
  316. package/package.json +3 -3
  317. package/test/profilingPlugin.ts +223 -0
  318. package/test/recordReplayProxyPlugin.js +22 -27
  319. package/test/thumbnail-performance-test.html +116 -0
  320. package/test/visualRegressionUtils.ts +286 -0
  321. package/types.json +1 -1
  322. package/dist/elements/TimegroupController.d.ts +0 -18
  323. package/dist/msToTimeCode.js +0 -17
  324. package/dist/msToTimeCode.js.map +0 -1
@@ -1,16 +1,88 @@
1
1
  import { __decorate } from "../_virtual/_@oxc-project_runtime@0.94.0/helpers/decorate.js";
2
2
  import { ContextMixin } from "./ContextMixin.js";
3
3
  import { TWMixin } from "./TWMixin2.js";
4
+ import { renderTimegroupPreview } from "../preview/renderTimegroupPreview.js";
5
+ import { getPreviewPresentationMode, getPreviewResolutionScale, getRenderMode, getShowStats, isNativeCanvasApiAvailable, setPreviewPresentationMode, setPreviewResolutionScale, setRenderMode, setShowStats } from "../preview/previewSettings.js";
6
+ import { renderTimegroupToCanvas } from "../preview/renderTimegroupToCanvas.js";
7
+ import { RenderCancelledError, renderTimegroupToVideo } from "../preview/renderTimegroupToVideo.js";
8
+ import { findRootTemporal } from "../elements/findRootTemporal.js";
9
+ import { ICONS, phosphorIcon } from "./icons.js";
10
+ import { sessionThumbnailCache } from "../elements/SessionThumbnailCache.js";
11
+ import "../elements/EFThumbnailStrip.js";
12
+ import { createStatsTrackingStrategy } from "../preview/statsTrackingStrategy.js";
13
+ import { getThumbnailCacheMaxSize, onThumbnailCacheSettingsChanged, setThumbnailCacheMaxSize } from "../preview/thumbnailCacheSettings.js";
14
+ import { AdaptiveResolutionTracker } from "../preview/AdaptiveResolutionTracker.js";
15
+ import { previewSettingsContext } from "./previewSettingsContext.js";
16
+ import "./EFFitScale.js";
17
+ import { EFTimegroup } from "../elements/EFTimegroup.js";
18
+ import { provide } from "@lit/context";
4
19
  import { LitElement, css, html } from "lit";
5
- import { customElement, eventOptions, property } from "lit/decorators.js";
20
+ import { customElement, eventOptions, property, state } from "lit/decorators.js";
6
21
  import { createRef, ref } from "lit/directives/ref.js";
7
22
 
8
23
  //#region src/gui/EFWorkbench.ts
24
+ /** Debounce delay before considering the preview "at rest" after motion stops */
25
+ const REST_DEBOUNCE_MS = 200;
9
26
  let EFWorkbench = class EFWorkbench$1 extends ContextMixin(TWMixin(LitElement)) {
10
27
  constructor(..._args) {
11
28
  super(..._args);
12
29
  this.rendering = false;
30
+ this.panZoomTransform = {
31
+ x: 0,
32
+ y: 0,
33
+ scale: 1
34
+ };
35
+ this.isExporting = false;
36
+ this.exportProgress = null;
37
+ this.exportStatus = "idle";
38
+ this.previewSettings = {
39
+ presentationMode: getPreviewPresentationMode(),
40
+ renderMode: getRenderMode(),
41
+ resolutionScale: getPreviewResolutionScale(),
42
+ showStats: getShowStats(),
43
+ thumbnailCacheMaxSize: getThumbnailCacheMaxSize()
44
+ };
45
+ this.renderMode = this.previewSettings.renderMode;
46
+ this.presentationMode = this.previewSettings.presentationMode;
47
+ this.previewResolutionScale = this.previewSettings.resolutionScale;
48
+ this.debugThumbnailTimestamps = false;
49
+ this.thumbnailCacheMaxSize = this.previewSettings.thumbnailCacheMaxSize;
50
+ this.thumbnailCacheStats = null;
51
+ this.cacheStatsUpdateInterval = null;
52
+ this.exportOptions = {
53
+ includeAudio: true,
54
+ scale: 1,
55
+ useInOut: false,
56
+ inMs: 0,
57
+ outMs: 0
58
+ };
59
+ this.exportAbortController = null;
60
+ this.isPlaying = false;
61
+ this.isScrubbing = false;
62
+ this.isAtRest = true;
63
+ this.currentAdaptiveScale = 1;
64
+ this.showStats = this.previewSettings.showStats;
65
+ this.statsStrategy = null;
66
+ this.isScrubbingRef = { current: false };
67
+ this.restDebounceTimer = null;
68
+ this.playingCheckInterval = null;
69
+ this.adaptiveTracker = null;
70
+ this.savePanZoomDebounceTimer = null;
71
+ this.cloneOverlayRef = createRef();
72
+ this.cloneRefresh = null;
73
+ this.cloneAnimationFrame = null;
74
+ this.cloneRootElement = null;
75
+ this.cloneTimegroup = null;
76
+ this.structureObserver = null;
77
+ this.rebuildPending = false;
78
+ this.canvasRefresh = null;
79
+ this.canvasPreviewRef = createRef();
80
+ this.canvasPreviewResult = null;
81
+ this.canvasAnimationFrame = null;
82
+ this.boundHandleTransformChanged = this.handleTransformChanged.bind(this);
13
83
  this.focusOverlay = createRef();
84
+ this.lastCanvasZoom = 1;
85
+ this.zoomReinitTimeout = null;
14
86
  this.drawOverlays = () => {
15
87
  const focusOverlay = this.focusOverlay.value;
16
88
  if (focusOverlay) if (this.focusedElement) {
@@ -30,14 +102,20 @@ let EFWorkbench = class EFWorkbench$1 extends ContextMixin(TWMixin(LitElement))
30
102
  static {
31
103
  this.styles = [css`
32
104
  :host {
33
- display: block;
105
+ display: flex;
106
+ flex-direction: column;
34
107
  width: 100%;
35
108
  height: 100%;
109
+ min-width: 0;
110
+ min-height: 0;
111
+ overflow: hidden;
36
112
 
37
113
  /* Light mode colors */
38
114
  --workbench-bg: rgb(30 41 59); /* slate-800 */
39
115
  --workbench-overlay-border: rgb(59 130 246); /* blue-500 */
40
116
  --workbench-overlay-bg: rgb(191 219 254); /* blue-200 */
117
+ --toolbar-bg: rgb(15 23 42); /* slate-900 */
118
+ --toolbar-border: rgba(148, 163, 184, 0.2);
41
119
  }
42
120
 
43
121
  :host(.dark), :host-context(.dark) {
@@ -45,6 +123,309 @@ let EFWorkbench = class EFWorkbench$1 extends ContextMixin(TWMixin(LitElement))
45
123
  --workbench-bg: rgb(2 6 23); /* slate-950 */
46
124
  --workbench-overlay-border: rgb(96 165 250); /* blue-400 */
47
125
  --workbench-overlay-bg: rgb(30 58 138); /* blue-900 */
126
+ --toolbar-bg: rgb(2 6 23);
127
+ --toolbar-border: rgba(148, 163, 184, 0.15);
128
+ }
129
+
130
+ .toolbar {
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: space-between;
134
+ gap: 8px;
135
+ padding: 8px 12px;
136
+ background: var(--toolbar-bg);
137
+ border-bottom: 1px solid var(--toolbar-border);
138
+ flex-shrink: 0;
139
+ font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
140
+ position: relative;
141
+ z-index: 20;
142
+ }
143
+
144
+ .toolbar-left {
145
+ display: flex;
146
+ align-items: center;
147
+ gap: 8px;
148
+ }
149
+
150
+ .toolbar-right {
151
+ display: flex;
152
+ align-items: center;
153
+ gap: 8px;
154
+ }
155
+
156
+ .toolbar-btn {
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ gap: 6px;
161
+ padding: 6px 12px;
162
+ background: rgba(51, 65, 85, 0.6);
163
+ border: 1px solid rgba(148, 163, 184, 0.2);
164
+ border-radius: 6px;
165
+ color: #e2e8f0;
166
+ font-size: 12px;
167
+ font-weight: 500;
168
+ cursor: pointer;
169
+ transition: all 0.15s ease;
170
+ }
171
+
172
+ .toolbar-btn:hover {
173
+ background: rgba(51, 65, 85, 0.9);
174
+ border-color: rgba(148, 163, 184, 0.3);
175
+ }
176
+
177
+ .toolbar-btn.active {
178
+ background: rgba(59, 130, 246, 0.2);
179
+ border-color: rgba(59, 130, 246, 0.4);
180
+ color: #60a5fa;
181
+ }
182
+
183
+ .toolbar-btn.primary {
184
+ background: linear-gradient(135deg, #3b82f6, #2563eb);
185
+ border-color: transparent;
186
+ color: white;
187
+ font-weight: 600;
188
+ }
189
+
190
+ .toolbar-btn.primary:hover {
191
+ background: linear-gradient(135deg, #60a5fa, #3b82f6);
192
+ }
193
+
194
+ .toolbar-icon-btn {
195
+ display: flex;
196
+ align-items: center;
197
+ justify-content: center;
198
+ width: 32px;
199
+ height: 32px;
200
+ padding: 0;
201
+ background: rgba(51, 65, 85, 0.6);
202
+ border: 1px solid rgba(148, 163, 184, 0.2);
203
+ border-radius: 6px;
204
+ color: #e2e8f0;
205
+ cursor: pointer;
206
+ transition: all 0.15s ease;
207
+ }
208
+
209
+ .toolbar-icon-btn:hover {
210
+ background: rgba(51, 65, 85, 0.9);
211
+ border-color: rgba(148, 163, 184, 0.3);
212
+ }
213
+
214
+ .toolbar-icon-btn.active {
215
+ background: rgba(59, 130, 246, 0.2);
216
+ border-color: rgba(59, 130, 246, 0.4);
217
+ color: #60a5fa;
218
+ }
219
+
220
+ .mode-indicator {
221
+ display: inline-flex;
222
+ align-items: center;
223
+ gap: 4px;
224
+ padding: 2px 8px;
225
+ border-radius: 10px;
226
+ font-size: 10px;
227
+ font-weight: 600;
228
+ text-transform: uppercase;
229
+ letter-spacing: 0.5px;
230
+ white-space: nowrap;
231
+ }
232
+
233
+ .mode-indicator.dom {
234
+ background: rgba(34, 197, 94, 0.2);
235
+ color: #4ade80;
236
+ border: 1px solid rgba(34, 197, 94, 0.3);
237
+ }
238
+
239
+ .mode-indicator.canvas {
240
+ background: rgba(168, 85, 247, 0.2);
241
+ color: #c084fc;
242
+ border: 1px solid rgba(168, 85, 247, 0.3);
243
+ }
244
+
245
+ .canvas-container {
246
+ position: relative;
247
+ overflow: hidden;
248
+ flex: 1;
249
+ display: grid;
250
+ grid-template-columns: 100%;
251
+ grid-template-rows: 100%;
252
+ min-height: 0;
253
+ }
254
+
255
+ .canvas-container ::slotted(*) {
256
+ width: 100%;
257
+ height: 100%;
258
+ grid-column: 1;
259
+ grid-row: 1;
260
+ }
261
+
262
+ .clone-overlay {
263
+ position: absolute;
264
+ inset: 0;
265
+ pointer-events: none;
266
+ z-index: 1;
267
+ }
268
+
269
+ .clone-content {
270
+ position: absolute;
271
+ transform-origin: 0 0;
272
+ }
273
+
274
+ .playback-stats {
275
+ position: absolute;
276
+ top: 8px;
277
+ left: 8px;
278
+ width: 200px;
279
+ background: rgba(0, 0, 0, 0.75);
280
+ backdrop-filter: blur(4px);
281
+ border-radius: 6px;
282
+ padding: 8px 12px;
283
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
284
+ font-size: 11px;
285
+ color: #e2e8f0;
286
+ z-index: 10;
287
+ pointer-events: none;
288
+ line-height: 1.5;
289
+ }
290
+
291
+ .playback-stats .stat-row {
292
+ display: flex;
293
+ justify-content: space-between;
294
+ gap: 8px;
295
+ }
296
+
297
+ .playback-stats .stat-label {
298
+ color: #94a3b8;
299
+ flex-shrink: 0;
300
+ width: 85px;
301
+ }
302
+
303
+ .playback-stats .stat-value {
304
+ font-weight: 600;
305
+ text-align: right;
306
+ flex: 1;
307
+ font-variant-numeric: tabular-nums;
308
+ }
309
+
310
+ .playback-stats .stat-value.good {
311
+ color: #4ade80;
312
+ }
313
+
314
+ .playback-stats .stat-value.warning {
315
+ color: #fbbf24;
316
+ }
317
+
318
+ .playback-stats .stat-value.bad {
319
+ color: #f87171;
320
+ }
321
+
322
+ .pressure-histogram {
323
+ display: flex;
324
+ align-items: flex-end;
325
+ gap: 1px;
326
+ height: 24px;
327
+ margin-top: 8px;
328
+ padding-top: 8px;
329
+ border-top: 1px solid rgba(148, 163, 184, 0.2);
330
+ }
331
+
332
+ .pressure-histogram .bar {
333
+ flex: 1;
334
+ min-width: 2px;
335
+ max-width: 4px;
336
+ border-radius: 1px 1px 0 0;
337
+ transition: height 0.1s ease-out;
338
+ }
339
+
340
+ .pressure-histogram .bar.nominal {
341
+ background: #4ade80;
342
+ height: 25%;
343
+ }
344
+
345
+ .pressure-histogram .bar.fair {
346
+ background: #a3e635;
347
+ height: 50%;
348
+ }
349
+
350
+ .pressure-histogram .bar.serious {
351
+ background: #fbbf24;
352
+ height: 75%;
353
+ }
354
+
355
+ .pressure-histogram .bar.critical {
356
+ background: #f87171;
357
+ height: 100%;
358
+ }
359
+
360
+ .pressure-histogram-label {
361
+ display: flex;
362
+ justify-content: space-between;
363
+ margin-top: 4px;
364
+ font-size: 9px;
365
+ color: #64748b;
366
+ }
367
+
368
+ .dropdown-panel {
369
+ position: fixed;
370
+ margin: 0;
371
+ padding: 14px 16px;
372
+ min-width: 260px;
373
+ max-width: calc(100vw - 32px);
374
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.98));
375
+ border: 1px solid rgba(148, 163, 184, 0.3);
376
+ border-radius: 10px;
377
+ backdrop-filter: blur(12px);
378
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
379
+ }
380
+
381
+ .dropdown-panel::backdrop {
382
+ background: transparent;
383
+ }
384
+
385
+ .dropdown-panel:popover-open {
386
+ /* Animation for opening */
387
+ animation: popover-fade-in 0.15s ease-out;
388
+ }
389
+
390
+ @keyframes popover-fade-in {
391
+ from {
392
+ opacity: 0;
393
+ transform: translateY(-4px);
394
+ }
395
+ to {
396
+ opacity: 1;
397
+ transform: translateY(0);
398
+ }
399
+ }
400
+
401
+ .dropdown-header {
402
+ display: flex;
403
+ align-items: center;
404
+ justify-content: space-between;
405
+ margin-bottom: 12px;
406
+ padding-bottom: 10px;
407
+ border-bottom: 1px solid rgba(148, 163, 184, 0.15);
408
+ }
409
+
410
+ .dropdown-title {
411
+ color: #e2e8f0;
412
+ font-size: 13px;
413
+ font-weight: 600;
414
+ }
415
+
416
+ .dropdown-close {
417
+ background: transparent;
418
+ border: none;
419
+ color: #64748b;
420
+ cursor: pointer;
421
+ padding: 2px;
422
+ line-height: 1;
423
+ font-size: 14px;
424
+ transition: color 0.15s;
425
+ }
426
+
427
+ .dropdown-close:hover {
428
+ color: #94a3b8;
48
429
  }
49
430
  `];
50
431
  }
@@ -52,52 +433,1716 @@ let EFWorkbench = class EFWorkbench$1 extends ContextMixin(TWMixin(LitElement))
52
433
  event.preventDefault();
53
434
  }
54
435
  connectedCallback() {
55
- document.body.style.width = "100%";
56
- document.body.style.height = "100%";
57
- document.documentElement.style.width = "100%";
58
- document.documentElement.style.height = "100%";
59
436
  super.connectedCallback();
437
+ this.addEventListener("transform-changed", this.boundHandleTransformChanged);
438
+ this.startMotionStateTracking();
439
+ this.adaptiveTracker = new AdaptiveResolutionTracker({ onScaleChange: (scale) => {
440
+ const oldScale = this.currentAdaptiveScale;
441
+ this.currentAdaptiveScale = scale;
442
+ if (this.previewResolutionScale === "auto" && this.presentationMode === "canvas" && !this.isAtRest) {
443
+ if (this.canvasPreviewResult) {
444
+ this.canvasPreviewResult.setResolutionScale(scale);
445
+ console.log(`[EFWorkbench] Resolution changed ${(oldScale * 100).toFixed(0)}% → ${(scale * 100).toFixed(0)}% (instant)`);
446
+ }
447
+ } else console.log(`[EFWorkbench] Adaptive scale updated to ${(scale * 100).toFixed(0)}% (no change: atRest=${this.isAtRest}, mode=${this.presentationMode})`);
448
+ } });
449
+ this.updateThumbnailCacheStats();
450
+ this.startCacheStatsUpdates();
451
+ onThumbnailCacheSettingsChanged(() => {
452
+ const newSize = getThumbnailCacheMaxSize();
453
+ this.thumbnailCacheMaxSize = newSize;
454
+ this.previewSettings = {
455
+ ...this.previewSettings,
456
+ thumbnailCacheMaxSize: newSize
457
+ };
458
+ sessionThumbnailCache.setMaxSize(newSize);
459
+ this.updateThumbnailCacheStats();
460
+ });
60
461
  }
61
462
  disconnectedCallback() {
62
463
  super.disconnectedCallback();
63
- document.body.style.width = "";
64
- document.body.style.height = "";
65
- document.documentElement.style.width = "";
66
- document.documentElement.style.height = "";
464
+ if (this.statsStrategy) {
465
+ this.statsStrategy.stop();
466
+ this.statsStrategy = null;
467
+ }
468
+ if (this.presentationMode === "clone") this.stopCloneOverlay();
469
+ else if (this.presentationMode === "dom") this.stopDomMode();
470
+ else if (this.presentationMode === "canvas") this.stopCanvasMode();
471
+ const timegroup = this.getTimegroup();
472
+ if (timegroup) {
473
+ timegroup.style.clipPath = "";
474
+ timegroup.style.pointerEvents = "";
475
+ }
476
+ this.removeEventListener("transform-changed", this.boundHandleTransformChanged);
477
+ this.stopMotionStateTracking();
478
+ this.stopCacheStatsUpdates();
479
+ if (this.adaptiveTracker) {
480
+ this.adaptiveTracker.dispose();
481
+ this.adaptiveTracker = null;
482
+ }
483
+ if (this.savePanZoomDebounceTimer !== null) {
484
+ clearTimeout(this.savePanZoomDebounceTimer);
485
+ this.savePanZoomDebounceTimer = null;
486
+ }
487
+ this.savePreviewPanZoom();
488
+ }
489
+ firstUpdated() {
490
+ requestAnimationFrame(() => {
491
+ this.restorePreviewPanZoom();
492
+ });
493
+ if (this.presentationMode === "clone") this.initCloneOverlay();
494
+ else if (this.presentationMode === "dom") this.initDomMode();
495
+ else if (this.presentationMode === "canvas") this.initCanvasMode();
496
+ }
497
+ handleTransformChanged(e) {
498
+ this.panZoomTransform = e.detail;
499
+ this.debouncedSavePreviewPanZoom();
500
+ if (this.presentationMode === "clone") this.updateCloneTransform();
501
+ else if (this.presentationMode === "canvas") {
502
+ this.updateCanvasTransform();
503
+ const zoomRatio = e.detail.scale / this.lastCanvasZoom;
504
+ if (zoomRatio < .75 || zoomRatio > 1.33) {
505
+ if (this.zoomReinitTimeout !== null) clearTimeout(this.zoomReinitTimeout);
506
+ this.zoomReinitTimeout = window.setTimeout(() => {
507
+ this.zoomReinitTimeout = null;
508
+ if (this.presentationMode === "canvas") {
509
+ this.lastCanvasZoom = this.panZoomTransform.scale;
510
+ this.stopCanvasMode();
511
+ this.initCanvasMode();
512
+ }
513
+ }, 500);
514
+ }
515
+ }
516
+ }
517
+ getTimegroup() {
518
+ const canvas = this.querySelector("[slot='canvas']");
519
+ if (!canvas) return null;
520
+ return canvas.querySelector("ef-timegroup");
521
+ }
522
+ /**
523
+ * Get the root timegroup ID for localStorage key generation.
524
+ * Returns null if no root timegroup is found or it has no ID.
525
+ */
526
+ getRootTimegroupId() {
527
+ const timegroup = this.getTimegroup();
528
+ if (!timegroup) return null;
529
+ const rootTemporal = findRootTemporal(timegroup);
530
+ if (rootTemporal instanceof EFTimegroup && rootTemporal.id) return rootTemporal.id;
531
+ return null;
532
+ }
533
+ /**
534
+ * Get localStorage key for preview pan/zoom state.
535
+ */
536
+ getPreviewPanZoomStorageKey() {
537
+ const rootId = this.getRootTimegroupId();
538
+ return rootId ? `ef-workbench-panzoom-${rootId}` : null;
539
+ }
540
+ /**
541
+ * Save preview pan/zoom to localStorage.
542
+ */
543
+ savePreviewPanZoom() {
544
+ const storageKey = this.getPreviewPanZoomStorageKey();
545
+ if (!storageKey) return;
546
+ try {
547
+ const state$1 = {
548
+ x: this.panZoomTransform.x,
549
+ y: this.panZoomTransform.y,
550
+ scale: this.panZoomTransform.scale
551
+ };
552
+ localStorage.setItem(storageKey, JSON.stringify(state$1));
553
+ } catch (error) {
554
+ console.warn("Failed to save preview pan/zoom to localStorage", error);
555
+ }
556
+ }
557
+ /**
558
+ * Restore preview pan/zoom from localStorage.
559
+ */
560
+ restorePreviewPanZoom() {
561
+ const storageKey = this.getPreviewPanZoomStorageKey();
562
+ if (!storageKey) return;
563
+ try {
564
+ const stored = localStorage.getItem(storageKey);
565
+ if (!stored) return;
566
+ const state$1 = JSON.parse(stored);
567
+ if (typeof state$1.x === "number" && typeof state$1.y === "number" && typeof state$1.scale === "number" && state$1.scale > 0) {
568
+ const clampedScale = Math.max(.1, Math.min(5, state$1.scale));
569
+ this.panZoomTransform = {
570
+ x: state$1.x,
571
+ y: state$1.y,
572
+ scale: clampedScale
573
+ };
574
+ requestAnimationFrame(() => {
575
+ const panZoomElement = this.querySelector("ef-pan-zoom");
576
+ if (panZoomElement) {
577
+ panZoomElement.x = this.panZoomTransform.x;
578
+ panZoomElement.y = this.panZoomTransform.y;
579
+ panZoomElement.scale = this.panZoomTransform.scale;
580
+ }
581
+ if (this.presentationMode === "clone") this.updateCloneTransform();
582
+ else if (this.presentationMode === "canvas") this.updateCanvasTransform();
583
+ });
584
+ }
585
+ } catch (error) {
586
+ console.warn("Failed to restore preview pan/zoom from localStorage", error);
587
+ }
588
+ }
589
+ /**
590
+ * Debounced save of preview pan/zoom to avoid excessive localStorage writes.
591
+ */
592
+ debouncedSavePreviewPanZoom() {
593
+ if (this.savePanZoomDebounceTimer !== null) clearTimeout(this.savePanZoomDebounceTimer);
594
+ this.savePanZoomDebounceTimer = window.setTimeout(() => {
595
+ this.savePanZoomDebounceTimer = null;
596
+ this.savePreviewPanZoom();
597
+ }, 200);
598
+ }
599
+ /**
600
+ * Start polling for motion state (playing/scrubbing).
601
+ * We use polling because:
602
+ * - Playing state comes from timegroup's playbackController
603
+ * - Scrubbing state comes from isScrubbingRef (set by EFScrubber)
604
+ */
605
+ startMotionStateTracking() {
606
+ if (this.playingCheckInterval !== null) return;
607
+ this.playingCheckInterval = window.setInterval(() => {
608
+ this.updateMotionState();
609
+ }, 50);
610
+ }
611
+ stopMotionStateTracking() {
612
+ if (this.playingCheckInterval !== null) {
613
+ clearInterval(this.playingCheckInterval);
614
+ this.playingCheckInterval = null;
615
+ }
616
+ if (this.restDebounceTimer !== null) {
617
+ clearTimeout(this.restDebounceTimer);
618
+ this.restDebounceTimer = null;
619
+ }
620
+ }
621
+ /**
622
+ * Update motion state by checking timegroup and scrubbing ref.
623
+ */
624
+ updateMotionState() {
625
+ const timegroup = this.getTimegroup();
626
+ const wasPlaying = this.isPlaying;
627
+ const wasScrubbing = this.isScrubbing;
628
+ this.isPlaying = timegroup?.playing ?? false;
629
+ this.isScrubbing = this.isScrubbingRef.current;
630
+ const wasInMotion = wasPlaying || wasScrubbing;
631
+ const isInMotion = this.isPlaying || this.isScrubbing;
632
+ if (isInMotion && !wasInMotion) this.handleMotionStart();
633
+ else if (!isInMotion && wasInMotion) this.handleMotionStop();
634
+ }
635
+ /**
636
+ * Called when motion starts (playing or scrubbing began).
637
+ */
638
+ handleMotionStart() {
639
+ if (this.restDebounceTimer !== null) {
640
+ clearTimeout(this.restDebounceTimer);
641
+ this.restDebounceTimer = null;
642
+ }
643
+ this.isAtRest = false;
644
+ if (this.previewResolutionScale === "auto" && this.adaptiveTracker) {
645
+ const timegroup = this.getTimegroup();
646
+ if (timegroup) {
647
+ const compositionWidth = timegroup.offsetWidth || 1920;
648
+ const compositionHeight = timegroup.offsetHeight || 1080;
649
+ const rect = timegroup.getBoundingClientRect();
650
+ const displayScale = Math.min(rect.width / compositionWidth, rect.height / compositionHeight);
651
+ this.adaptiveTracker.initializeAtScale(displayScale);
652
+ this.currentAdaptiveScale = this.adaptiveTracker.getRecommendedScale();
653
+ if (this.canvasPreviewResult) this.canvasPreviewResult.setResolutionScale(this.currentAdaptiveScale);
654
+ console.log(`[EFWorkbench] Motion started, set resolution to ${(this.currentAdaptiveScale * 100).toFixed(0)}% (displayScale=${(displayScale * 100).toFixed(0)}%)`);
655
+ }
656
+ }
657
+ console.log(`[EFWorkbench] Motion started (playing=${this.isPlaying}, scrubbing=${this.isScrubbing})`);
658
+ }
659
+ /**
660
+ * Called when motion stops (not playing and not scrubbing).
661
+ * Starts a debounce timer before transitioning to rest state.
662
+ */
663
+ handleMotionStop() {
664
+ if (this.restDebounceTimer !== null) clearTimeout(this.restDebounceTimer);
665
+ this.restDebounceTimer = window.setTimeout(() => {
666
+ this.restDebounceTimer = null;
667
+ this.transitionToRest();
668
+ }, REST_DEBOUNCE_MS);
669
+ }
670
+ /**
671
+ * Called after debounce period when we're confirmed to be at rest.
672
+ */
673
+ transitionToRest() {
674
+ this.isAtRest = true;
675
+ console.log("[EFWorkbench] Transitioned to rest state");
676
+ if (this.previewResolutionScale === "auto" && this.presentationMode === "canvas") {
677
+ this.adaptiveTracker?.reset();
678
+ this.currentAdaptiveScale = 1;
679
+ if (this.canvasPreviewResult) {
680
+ this.canvasPreviewResult.setResolutionScale(1);
681
+ console.log("[EFWorkbench] Set full resolution for rest state (instant)");
682
+ }
683
+ }
684
+ }
685
+ /**
686
+ * Get the effective resolution scale based on current mode and motion state.
687
+ * For "auto" mode, returns full resolution at rest, adaptive scale in motion.
688
+ */
689
+ getEffectiveResolutionScale(timegroup, canvasContainer) {
690
+ if (this.previewResolutionScale !== "auto") return this.getResolutionScale(timegroup, canvasContainer);
691
+ const compositionWidth = timegroup.offsetWidth || 1920;
692
+ const compositionHeight = timegroup.offsetHeight || 1080;
693
+ const rect = timegroup.getBoundingClientRect();
694
+ const displayedWidth = rect.width;
695
+ const displayedHeight = rect.height;
696
+ const displayScale = Math.min(displayedWidth / compositionWidth, displayedHeight / compositionHeight);
697
+ if (this.isAtRest) {
698
+ const scale = Math.max(.1, Math.min(1, displayScale));
699
+ console.log(`[EFWorkbench] Auto mode (at rest): using display scale ${(scale * 100).toFixed(1)}%`);
700
+ return scale;
701
+ } else {
702
+ const adaptiveScale = this.currentAdaptiveScale;
703
+ const targetScale = Math.min(displayScale, adaptiveScale);
704
+ const scale = Math.max(.1, Math.min(1, targetScale));
705
+ console.log(`[EFWorkbench] Auto mode (in motion): adaptive=${adaptiveScale}, display=${displayScale.toFixed(2)}, final=${(scale * 100).toFixed(1)}%`);
706
+ return scale;
707
+ }
708
+ }
709
+ /**
710
+ * Apply settings when dependencies are ready.
711
+ * Called from updated() hook when settings change or dependencies become available.
712
+ */
713
+ applySettings() {
714
+ this.presentationMode = this.previewSettings.presentationMode;
715
+ this.renderMode = this.previewSettings.renderMode;
716
+ this.previewResolutionScale = this.previewSettings.resolutionScale;
717
+ this.showStats = this.previewSettings.showStats;
718
+ this.thumbnailCacheMaxSize = this.previewSettings.thumbnailCacheMaxSize;
719
+ this.updateStatsStrategy();
720
+ }
721
+ /**
722
+ * Update or create stats tracking strategy based on current mode and settings.
723
+ */
724
+ updateStatsStrategy() {
725
+ if (this.statsStrategy) {
726
+ this.statsStrategy.stop();
727
+ this.statsStrategy = null;
728
+ }
729
+ if (!this.showStats) return;
730
+ const timegroup = this.getTimegroup();
731
+ if (!timegroup || !this.adaptiveTracker) return;
732
+ const compositionWidth = timegroup.offsetWidth || 1920;
733
+ const compositionHeight = timegroup.offsetHeight || 1080;
734
+ const strategy = createStatsTrackingStrategy(this.presentationMode, {
735
+ timegroup,
736
+ adaptiveTracker: this.adaptiveTracker,
737
+ canvasPreviewResult: this.canvasPreviewResult ?? void 0,
738
+ compositionWidth,
739
+ compositionHeight,
740
+ getResolutionScale: this.canvasPreviewResult?.getResolutionScale,
741
+ isAtRest: () => this.isAtRest,
742
+ isExporting: () => this.isExporting
743
+ });
744
+ if (strategy) {
745
+ this.statsStrategy = strategy;
746
+ strategy.start();
747
+ }
748
+ }
749
+ initCloneOverlay() {
750
+ if (this.presentationMode !== "clone") return;
751
+ const timegroup = this.getTimegroup();
752
+ const cloneContainer = this.cloneOverlayRef.value;
753
+ if (!timegroup || !cloneContainer) {
754
+ setTimeout(() => this.initCloneOverlay(), 100);
755
+ return;
756
+ }
757
+ this.cloneTimegroup = timegroup;
758
+ timegroup.proxyMode = false;
759
+ timegroup.updateComplete.then(() => {
760
+ if (this.presentationMode !== "clone") return;
761
+ this.finishCloneSetup(timegroup, cloneContainer);
762
+ });
763
+ }
764
+ finishCloneSetup(timegroup, cloneContainer) {
765
+ timegroup.style.clipPath = "inset(100%)";
766
+ timegroup.style.pointerEvents = "none";
767
+ cloneContainer.style.display = "block";
768
+ this.rebuildClone(timegroup);
769
+ this.setupStructureObserver(timegroup);
770
+ }
771
+ rebuildClone(timegroup) {
772
+ if (this.presentationMode !== "clone") return;
773
+ const container = this.cloneOverlayRef.value;
774
+ if (!container) return;
775
+ try {
776
+ const { container: previewContainer, refresh } = renderTimegroupPreview(timegroup);
777
+ container.innerHTML = "";
778
+ previewContainer.classList.add("clone-content");
779
+ container.appendChild(previewContainer);
780
+ this.cloneRefresh = refresh;
781
+ this.cloneRootElement = previewContainer.firstElementChild ?? null;
782
+ if (this.cloneRootElement) {
783
+ this.cloneRootElement.style.opacity = "1";
784
+ this.cloneRootElement.style.clipPath = "none";
785
+ this.cloneRootElement.style.position = "relative";
786
+ this.cloneRootElement.style.inset = "auto";
787
+ this.cloneRootElement.style.top = "0";
788
+ this.cloneRootElement.style.right = "auto";
789
+ this.cloneRootElement.style.bottom = "auto";
790
+ this.cloneRootElement.style.left = "0";
791
+ }
792
+ this.updateCloneTransform();
793
+ this.observeShadowRoots(timegroup);
794
+ if (this.cloneAnimationFrame === null) this.startCloneLoop();
795
+ } catch (e) {
796
+ console.error("Failed to build clone:", e);
797
+ }
798
+ }
799
+ setupStructureObserver(timegroup) {
800
+ if (this.structureObserver) this.structureObserver.disconnect();
801
+ this.structureObserver = new MutationObserver((mutations) => {
802
+ if (this.presentationMode !== "clone") return;
803
+ if (mutations.some((m) => m.type === "childList" && (m.addedNodes.length > 0 || m.removedNodes.length > 0)) && !this.rebuildPending) {
804
+ this.rebuildPending = true;
805
+ requestAnimationFrame(() => {
806
+ this.rebuildPending = false;
807
+ if (this.presentationMode === "clone") this.rebuildClone(timegroup);
808
+ });
809
+ }
810
+ });
811
+ this.structureObserver.observe(timegroup, {
812
+ childList: true,
813
+ subtree: true
814
+ });
815
+ this.observeShadowRoots(timegroup);
816
+ }
817
+ observeShadowRoots(root) {
818
+ if (!this.structureObserver) return;
819
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);
820
+ let node = root;
821
+ while (node) {
822
+ if (node.shadowRoot) this.structureObserver.observe(node.shadowRoot, {
823
+ childList: true,
824
+ subtree: true
825
+ });
826
+ node = walker.nextNode();
827
+ }
828
+ }
829
+ updateCloneTransform() {
830
+ if (this.presentationMode !== "clone") return;
831
+ const container = this.cloneOverlayRef.value;
832
+ if (!container) return;
833
+ const cloneContent = container.querySelector(".clone-content");
834
+ if (!cloneContent) return;
835
+ const { x, y, scale } = this.panZoomTransform;
836
+ cloneContent.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
837
+ }
838
+ startCloneLoop() {
839
+ const loop = () => {
840
+ if (this.presentationMode !== "clone" || !this.cloneRefresh) {
841
+ this.cloneAnimationFrame = null;
842
+ return;
843
+ }
844
+ if (!this.isExporting) {
845
+ this.cloneRefresh();
846
+ if (this.cloneRootElement) {
847
+ this.cloneRootElement.style.clipPath = "none";
848
+ this.cloneRootElement.style.opacity = "1";
849
+ this.cloneRootElement.style.position = "relative";
850
+ this.cloneRootElement.style.inset = "auto";
851
+ this.cloneRootElement.style.top = "0";
852
+ this.cloneRootElement.style.right = "auto";
853
+ this.cloneRootElement.style.bottom = "auto";
854
+ this.cloneRootElement.style.left = "0";
855
+ }
856
+ }
857
+ this.cloneAnimationFrame = requestAnimationFrame(loop);
858
+ };
859
+ this.cloneAnimationFrame = requestAnimationFrame(loop);
860
+ }
861
+ stopCloneOverlay() {
862
+ if (this.cloneAnimationFrame !== null) {
863
+ cancelAnimationFrame(this.cloneAnimationFrame);
864
+ this.cloneAnimationFrame = null;
865
+ }
866
+ if (this.structureObserver) {
867
+ this.structureObserver.disconnect();
868
+ this.structureObserver = null;
869
+ }
870
+ this.cloneRefresh = null;
871
+ this.cloneRootElement = null;
872
+ this.cloneTimegroup = null;
873
+ this.rebuildPending = false;
874
+ const container = this.cloneOverlayRef.value;
875
+ if (container) {
876
+ container.innerHTML = "";
877
+ container.style.display = "none";
878
+ }
879
+ }
880
+ async handlePresentationModeChange(mode) {
881
+ if (mode === this.presentationMode) return;
882
+ const previousMode = this.presentationMode;
883
+ if (previousMode === "clone") this.stopCloneOverlay();
884
+ else if (previousMode === "dom") this.stopDomMode();
885
+ else if (previousMode === "canvas") this.stopCanvasMode();
886
+ setPreviewPresentationMode(mode);
887
+ this.previewSettings = {
888
+ ...this.previewSettings,
889
+ presentationMode: mode
890
+ };
891
+ await this.updateComplete;
892
+ if (mode === "clone") this.initCloneOverlay();
893
+ else if (mode === "dom") this.initDomMode();
894
+ else if (mode === "canvas") this.initCanvasMode();
895
+ }
896
+ initDomMode() {
897
+ if (this.presentationMode !== "dom") return;
898
+ const timegroup = this.getTimegroup();
899
+ if (!timegroup) {
900
+ setTimeout(() => this.initDomMode(), 100);
901
+ return;
902
+ }
903
+ const fitScale = this.querySelector("[slot='canvas']");
904
+ if (fitScale?.removeScale && fitScale?.paused !== void 0) {
905
+ fitScale.paused = true;
906
+ fitScale.removeScale();
907
+ }
908
+ timegroup.proxyMode = false;
909
+ timegroup.style.clipPath = "";
910
+ timegroup.style.pointerEvents = "";
911
+ }
912
+ stopDomMode() {
913
+ const timegroup = this.getTimegroup();
914
+ if (timegroup) {
915
+ timegroup.style.clipPath = "inset(100%)";
916
+ timegroup.style.pointerEvents = "none";
917
+ }
918
+ const fitScale = this.querySelector("[slot='canvas']");
919
+ if (fitScale?.paused !== void 0) fitScale.paused = false;
920
+ if (this.statsStrategy) {
921
+ this.statsStrategy.stop();
922
+ this.statsStrategy = null;
923
+ }
924
+ }
925
+ /**
926
+ * Get the resolution scale for canvas rendering (for fixed scale modes).
927
+ *
928
+ * Logic:
929
+ * - Get actual displayed size from getBoundingClientRect()
930
+ * - For "Full": render at displayed size (1:1 pixel mapping)
931
+ * - For other settings: render at that % of displayed size
932
+ * - Never exceed composition size (100%)
933
+ *
934
+ * Note: For "auto" mode, use getEffectiveResolutionScale() instead.
935
+ */
936
+ getResolutionScale(timegroup, _canvasContainer) {
937
+ if (this.previewResolutionScale === "auto") return this.getEffectiveResolutionScale(timegroup, _canvasContainer);
938
+ const compositionWidth = timegroup.offsetWidth || 1920;
939
+ const compositionHeight = timegroup.offsetHeight || 1080;
940
+ const rect = timegroup.getBoundingClientRect();
941
+ const displayedWidth = rect.width;
942
+ const displayedHeight = rect.height;
943
+ const displayScale = Math.min(displayedWidth / compositionWidth, displayedHeight / compositionHeight);
944
+ const targetScale = this.previewResolutionScale === 1 ? displayScale : Math.min(displayScale, this.previewResolutionScale);
945
+ const finalScale = Math.max(.1, Math.min(1, targetScale));
946
+ const renderWidth = Math.floor(compositionWidth * finalScale);
947
+ const renderHeight = Math.floor(compositionHeight * finalScale);
948
+ console.log(`[EFWorkbench] Resolution scale:
949
+ Composition (offsetWidth×offsetHeight): ${compositionWidth}×${compositionHeight}
950
+ Displayed (boundingRect): ${Math.round(displayedWidth)}×${Math.round(displayedHeight)}
951
+ Display scale: ${(displayScale * 100).toFixed(1)}%
952
+ Setting: ${this.previewResolutionScale === 1 ? "Full" : `${Math.round(this.previewResolutionScale * 100)}%`}
953
+ Final: ${(finalScale * 100).toFixed(1)}% → ${renderWidth}×${renderHeight}`);
954
+ return finalScale;
955
+ }
956
+ initCanvasMode() {
957
+ if (this.presentationMode !== "canvas") return;
958
+ const timegroup = this.getTimegroup();
959
+ const canvasContainer = this.canvasPreviewRef.value;
960
+ if (!timegroup || !canvasContainer) {
961
+ setTimeout(() => this.initCanvasMode(), 100);
962
+ return;
963
+ }
964
+ timegroup.proxyMode = false;
965
+ timegroup.style.clipPath = "inset(100%)";
966
+ timegroup.style.pointerEvents = "none";
967
+ canvasContainer.style.display = "block";
968
+ const initialResolutionScale = this.previewResolutionScale === "auto" ? this.getEffectiveResolutionScale(timegroup, canvasContainer) : this.getResolutionScale(timegroup, canvasContainer);
969
+ this.lastCanvasZoom = this.panZoomTransform.scale;
970
+ timegroup.offsetWidth;
971
+ timegroup.offsetHeight;
972
+ try {
973
+ const result = renderTimegroupToCanvas(timegroup, {
974
+ scale: 1,
975
+ resolutionScale: initialResolutionScale
976
+ });
977
+ this.canvasPreviewResult = result;
978
+ const { container, canvas, refresh, getResolutionScale } = result;
979
+ canvas.classList.add("clone-content");
980
+ canvasContainer.innerHTML = "";
981
+ canvasContainer.appendChild(container);
982
+ this.updateCanvasTransform();
983
+ const loop = async () => {
984
+ if (this.presentationMode !== "canvas") return;
985
+ if (!this.isExporting) try {
986
+ await refresh();
987
+ this.updateCanvasTransform();
988
+ } catch (e) {
989
+ console.error("Canvas refresh failed:", e);
990
+ }
991
+ this.canvasAnimationFrame = requestAnimationFrame(loop);
992
+ };
993
+ this.canvasAnimationFrame = requestAnimationFrame(loop);
994
+ } catch (e) {
995
+ console.error("Failed to init canvas mode:", e);
996
+ }
997
+ }
998
+ stopCanvasMode() {
999
+ if (this.canvasAnimationFrame !== null) {
1000
+ cancelAnimationFrame(this.canvasAnimationFrame);
1001
+ this.canvasAnimationFrame = null;
1002
+ }
1003
+ if (this.zoomReinitTimeout !== null) {
1004
+ clearTimeout(this.zoomReinitTimeout);
1005
+ this.zoomReinitTimeout = null;
1006
+ }
1007
+ this.canvasPreviewResult = null;
1008
+ if (this.statsStrategy) {
1009
+ this.statsStrategy.stop();
1010
+ this.statsStrategy = null;
1011
+ }
1012
+ const container = this.canvasPreviewRef.value;
1013
+ if (container) {
1014
+ container.innerHTML = "";
1015
+ container.style.display = "none";
1016
+ }
1017
+ }
1018
+ updateCanvasTransform() {
1019
+ if (this.presentationMode !== "canvas") return;
1020
+ const container = this.canvasPreviewRef.value;
1021
+ if (!container) return;
1022
+ const canvas = container.querySelector("canvas");
1023
+ if (!canvas) return;
1024
+ const { x, y, scale } = this.panZoomTransform;
1025
+ canvas.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
1026
+ }
1027
+ initCanvasRenderer() {
1028
+ const timegroup = this.getTimegroup();
1029
+ if (!timegroup) return null;
1030
+ try {
1031
+ const { canvas, refresh } = renderTimegroupToCanvas(timegroup, 1);
1032
+ this.canvasRefresh = refresh;
1033
+ return {
1034
+ canvas,
1035
+ refresh
1036
+ };
1037
+ } catch (e) {
1038
+ console.error("Failed to init canvas renderer:", e);
1039
+ return null;
1040
+ }
1041
+ }
1042
+ /** Start video export with progress tracking */
1043
+ async startExport(options = {}) {
1044
+ const timegroup = this.getTimegroup();
1045
+ if (!timegroup) {
1046
+ console.error("No timegroup found for export");
1047
+ return;
1048
+ }
1049
+ if (this.isExporting) {
1050
+ console.warn("Export already in progress");
1051
+ return;
1052
+ }
1053
+ this.exportAbortController = new AbortController();
1054
+ this.isExporting = true;
1055
+ this.exportProgress = null;
1056
+ this.exportStatus = "rendering";
1057
+ try {
1058
+ await renderTimegroupToVideo(timegroup, {
1059
+ ...options,
1060
+ signal: this.exportAbortController.signal,
1061
+ onProgress: (progress) => {
1062
+ this.exportProgress = progress;
1063
+ }
1064
+ });
1065
+ this.exportStatus = "complete";
1066
+ setTimeout(() => {
1067
+ this.isExporting = false;
1068
+ this.exportProgress = null;
1069
+ this.exportStatus = "idle";
1070
+ this.exportAbortController = null;
1071
+ }, 2e3);
1072
+ } catch (e) {
1073
+ if (e instanceof RenderCancelledError) {
1074
+ console.log("Export cancelled by user");
1075
+ this.exportStatus = "cancelled";
1076
+ setTimeout(() => {
1077
+ this.isExporting = false;
1078
+ this.exportProgress = null;
1079
+ this.exportStatus = "idle";
1080
+ this.exportAbortController = null;
1081
+ }, 1500);
1082
+ } else {
1083
+ console.error("Export failed:", e);
1084
+ this.exportStatus = "error";
1085
+ setTimeout(() => {
1086
+ this.isExporting = false;
1087
+ this.exportProgress = null;
1088
+ this.exportStatus = "idle";
1089
+ this.exportAbortController = null;
1090
+ }, 3e3);
1091
+ }
1092
+ }
1093
+ }
1094
+ /** Cancel the current export */
1095
+ cancelExport() {
1096
+ if (this.exportAbortController) this.exportAbortController.abort();
1097
+ }
1098
+ positionPopover(popover, anchorId) {
1099
+ const anchor = this.shadowRoot?.getElementById(anchorId);
1100
+ if (!anchor) return;
1101
+ const anchorRect = anchor.getBoundingClientRect();
1102
+ const popoverRect = popover.getBoundingClientRect();
1103
+ const padding = 8;
1104
+ let top = anchorRect.bottom + padding;
1105
+ let left = anchorRect.right - popoverRect.width;
1106
+ if (left < padding) left = padding;
1107
+ if (left + popoverRect.width > window.innerWidth - padding) left = window.innerWidth - popoverRect.width - padding;
1108
+ if (top + popoverRect.height > window.innerHeight - padding) top = anchorRect.top - popoverRect.height - padding;
1109
+ popover.style.top = `${top}px`;
1110
+ popover.style.left = `${left}px`;
1111
+ }
1112
+ handleSettingsPopoverToggle(e) {
1113
+ const popover = e.target;
1114
+ if (e.newState === "open") requestAnimationFrame(() => {
1115
+ this.positionPopover(popover, "settings-btn");
1116
+ });
1117
+ }
1118
+ handleExportPopoverToggle(e) {
1119
+ const popover = e.target;
1120
+ if (e.newState === "open") {
1121
+ if (this.exportOptions.outMs === 0) {
1122
+ const timegroup = this.getTimegroup();
1123
+ if (timegroup) this.exportOptions = {
1124
+ ...this.exportOptions,
1125
+ outMs: timegroup.durationMs
1126
+ };
1127
+ }
1128
+ requestAnimationFrame(() => {
1129
+ this.positionPopover(popover, "export-btn");
1130
+ });
1131
+ }
1132
+ }
1133
+ handleStartExport() {
1134
+ this.startExport({
1135
+ includeAudio: this.exportOptions.includeAudio,
1136
+ scale: this.exportOptions.scale,
1137
+ fromMs: this.exportOptions.useInOut ? this.exportOptions.inMs : void 0,
1138
+ toMs: this.exportOptions.useInOut ? this.exportOptions.outMs : void 0
1139
+ });
1140
+ }
1141
+ updateExportOption(key, value) {
1142
+ this.exportOptions = {
1143
+ ...this.exportOptions,
1144
+ [key]: value
1145
+ };
1146
+ }
1147
+ formatTime(ms) {
1148
+ const totalSeconds = Math.floor(ms / 1e3);
1149
+ const minutes = Math.floor(totalSeconds / 60);
1150
+ const seconds = totalSeconds % 60;
1151
+ if (minutes > 0) return `${minutes}:${seconds.toString().padStart(2, "0")}`;
1152
+ return `${seconds}s`;
1153
+ }
1154
+ handleCancelClick() {
1155
+ this.cancelExport();
1156
+ }
1157
+ handleRenderModeChange(mode) {
1158
+ setRenderMode(mode);
1159
+ this.previewSettings = {
1160
+ ...this.previewSettings,
1161
+ renderMode: mode
1162
+ };
1163
+ }
1164
+ handleResolutionScaleChange(scale) {
1165
+ console.log(`[EFWorkbench] Resolution scale changed to ${scale}, presentationMode=${this.presentationMode}`);
1166
+ setPreviewResolutionScale(scale);
1167
+ this.previewSettings = {
1168
+ ...this.previewSettings,
1169
+ resolutionScale: scale
1170
+ };
1171
+ if (scale === "auto") {
1172
+ this.adaptiveTracker?.reset();
1173
+ this.currentAdaptiveScale = 1;
1174
+ }
1175
+ if (this.presentationMode === "canvas") {
1176
+ console.log("[EFWorkbench] Reinitializing canvas mode with new resolution scale");
1177
+ this.stopCanvasMode();
1178
+ this.initCanvasMode();
1179
+ }
1180
+ }
1181
+ async updateThumbnailCacheStats() {
1182
+ try {
1183
+ this.thumbnailCacheStats = await sessionThumbnailCache.getStats();
1184
+ } catch (error) {
1185
+ console.warn("Failed to update thumbnail cache stats:", error);
1186
+ }
1187
+ }
1188
+ startCacheStatsUpdates() {
1189
+ this.cacheStatsUpdateInterval = window.setInterval(() => {
1190
+ this.updateThumbnailCacheStats();
1191
+ }, 2e3);
1192
+ }
1193
+ stopCacheStatsUpdates() {
1194
+ if (this.cacheStatsUpdateInterval !== null) {
1195
+ clearInterval(this.cacheStatsUpdateInterval);
1196
+ this.cacheStatsUpdateInterval = null;
1197
+ }
1198
+ }
1199
+ handleThumbnailCacheMaxSizeChange(size) {
1200
+ setThumbnailCacheMaxSize(size);
1201
+ this.previewSettings = {
1202
+ ...this.previewSettings,
1203
+ thumbnailCacheMaxSize: size
1204
+ };
1205
+ sessionThumbnailCache.setMaxSize(size);
1206
+ this.updateThumbnailCacheStats();
1207
+ }
1208
+ async handleClearThumbnailCache() {
1209
+ await sessionThumbnailCache.clear();
1210
+ await this.updateThumbnailCacheStats();
1211
+ }
1212
+ formatBytes(bytes) {
1213
+ if (bytes < 1024) return `${bytes} B`;
1214
+ else if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1215
+ else return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1216
+ }
1217
+ handleDebugThumbnailTimestampsToggle(enabled) {
1218
+ this.debugThumbnailTimestamps = enabled;
1219
+ this.dispatchEvent(new CustomEvent("ef-debug-thumbnail-timestamps-changed", {
1220
+ detail: { enabled },
1221
+ bubbles: true,
1222
+ composed: true
1223
+ }));
1224
+ }
1225
+ handleShowStatsToggle(enabled) {
1226
+ setShowStats(enabled);
1227
+ this.previewSettings = {
1228
+ ...this.previewSettings,
1229
+ showStats: enabled
1230
+ };
1231
+ }
1232
+ /**
1233
+ * Reset and fit the preview to show all content centered.
1234
+ * Finds the pan-zoom element and calls fitToContent() on it.
1235
+ */
1236
+ handleFitToContent() {
1237
+ const panZoomElement = this.querySelector("ef-pan-zoom");
1238
+ if (panZoomElement && typeof panZoomElement.fitToContent === "function") panZoomElement.fitToContent();
1239
+ }
1240
+ renderSettingsPopover() {
1241
+ const isAvailable = isNativeCanvasApiAvailable();
1242
+ return html`
1243
+ <div
1244
+ id="settings-popover"
1245
+ popover="auto"
1246
+ class="dropdown-panel"
1247
+ @toggle=${this.handleSettingsPopoverToggle}
1248
+ >
1249
+ <div class="dropdown-header">
1250
+ <span class="dropdown-title">Preview Settings</span>
1251
+ <button class="dropdown-close" popovertarget="settings-popover" popovertargetaction="hide">✕</button>
1252
+ </div>
1253
+
1254
+ <!-- Presentation Mode Setting -->
1255
+ <div style="
1256
+ background: rgba(51, 65, 85, 0.4);
1257
+ border-radius: 8px;
1258
+ padding: 12px;
1259
+ margin-bottom: 10px;
1260
+ ">
1261
+ <div style="color: #e2e8f0; font-size: 12px; font-weight: 500; margin-bottom: 10px;">Presentation Mode</div>
1262
+
1263
+ <div style="display: flex; gap: 4px; background: rgba(30, 41, 59, 0.6); border-radius: 6px; padding: 3px;">
1264
+ <button
1265
+ @click=${() => this.handlePresentationModeChange("clone")}
1266
+ style="
1267
+ flex: 1;
1268
+ padding: 6px 10px;
1269
+ border: none;
1270
+ border-radius: 4px;
1271
+ font-size: 11px;
1272
+ font-weight: 500;
1273
+ cursor: pointer;
1274
+ transition: all 0.15s ease;
1275
+ background: ${this.presentationMode === "clone" ? "rgba(59, 130, 246, 0.3)" : "transparent"};
1276
+ color: ${this.presentationMode === "clone" ? "#60a5fa" : "#94a3b8"};
1277
+ border: 1px solid ${this.presentationMode === "clone" ? "rgba(59, 130, 246, 0.4)" : "transparent"};
1278
+ "
1279
+ >Clone</button>
1280
+ <button
1281
+ @click=${() => this.handlePresentationModeChange("dom")}
1282
+ style="
1283
+ flex: 1;
1284
+ padding: 6px 10px;
1285
+ border: none;
1286
+ border-radius: 4px;
1287
+ font-size: 11px;
1288
+ font-weight: 500;
1289
+ cursor: pointer;
1290
+ transition: all 0.15s ease;
1291
+ background: ${this.presentationMode === "dom" ? "rgba(34, 197, 94, 0.3)" : "transparent"};
1292
+ color: ${this.presentationMode === "dom" ? "#4ade80" : "#94a3b8"};
1293
+ border: 1px solid ${this.presentationMode === "dom" ? "rgba(34, 197, 94, 0.4)" : "transparent"};
1294
+ "
1295
+ >DOM</button>
1296
+ <button
1297
+ @click=${() => this.handlePresentationModeChange("canvas")}
1298
+ style="
1299
+ flex: 1;
1300
+ padding: 6px 10px;
1301
+ border: none;
1302
+ border-radius: 4px;
1303
+ font-size: 11px;
1304
+ font-weight: 500;
1305
+ cursor: pointer;
1306
+ transition: all 0.15s ease;
1307
+ background: ${this.presentationMode === "canvas" ? "rgba(168, 85, 247, 0.3)" : "transparent"};
1308
+ color: ${this.presentationMode === "canvas" ? "#c084fc" : "#94a3b8"};
1309
+ border: 1px solid ${this.presentationMode === "canvas" ? "rgba(168, 85, 247, 0.4)" : "transparent"};
1310
+ "
1311
+ >Canvas</button>
1312
+ </div>
1313
+
1314
+ <div style="margin-top: 8px; color: #64748b; font-size: 10px; line-height: 1.4;">
1315
+ ${this.presentationMode === "clone" ? "Default. Shows a styled clone synced from the hidden original." : this.presentationMode === "dom" ? "Shows the real timegroup DOM directly." : "Renders to canvas each frame (experimental)."}
1316
+ </div>
1317
+ </div>
1318
+
1319
+ <!-- Render Mode Setting -->
1320
+ <div style="
1321
+ background: rgba(51, 65, 85, 0.4);
1322
+ border-radius: 8px;
1323
+ padding: 12px;
1324
+ ">
1325
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
1326
+ <span style="color: #e2e8f0; font-size: 12px; font-weight: 500;">Render Mode</span>
1327
+ ${isAvailable ? html`
1328
+ <div style="display: flex; align-items: center; gap: 5px;">
1329
+ <span style="
1330
+ display: inline-block;
1331
+ width: 7px;
1332
+ height: 7px;
1333
+ border-radius: 50%;
1334
+ background: #4ade80;
1335
+ "></span>
1336
+ <span style="color: #4ade80; font-size: 10px; font-weight: 500;">
1337
+ Native Available
1338
+ </span>
1339
+ </div>
1340
+ ` : ""}
1341
+ </div>
1342
+
1343
+ <div style="display: flex; gap: 4px; background: rgba(30, 41, 59, 0.6); border-radius: 6px; padding: 3px;">
1344
+ <button
1345
+ @click=${() => this.handleRenderModeChange("foreignObject")}
1346
+ style="
1347
+ flex: 1;
1348
+ padding: 6px 8px;
1349
+ border: none;
1350
+ border-radius: 4px;
1351
+ font-size: 10px;
1352
+ font-weight: 500;
1353
+ cursor: pointer;
1354
+ transition: all 0.15s ease;
1355
+ background: ${this.renderMode === "foreignObject" ? "rgba(59, 130, 246, 0.3)" : "transparent"};
1356
+ color: ${this.renderMode === "foreignObject" ? "#60a5fa" : "#94a3b8"};
1357
+ border: 1px solid ${this.renderMode === "foreignObject" ? "rgba(59, 130, 246, 0.4)" : "transparent"};
1358
+ "
1359
+ >foreignObject</button>
1360
+ <button
1361
+ @click=${() => this.handleRenderModeChange("native")}
1362
+ ?disabled=${!isAvailable}
1363
+ style="
1364
+ flex: 1;
1365
+ padding: 6px 8px;
1366
+ border: none;
1367
+ border-radius: 4px;
1368
+ font-size: 10px;
1369
+ font-weight: 500;
1370
+ cursor: ${isAvailable ? "pointer" : "not-allowed"};
1371
+ transition: all 0.15s ease;
1372
+ background: ${this.renderMode === "native" ? "rgba(34, 197, 94, 0.3)" : "transparent"};
1373
+ color: ${this.renderMode === "native" ? "#4ade80" : isAvailable ? "#94a3b8" : "#64748b"};
1374
+ border: 1px solid ${this.renderMode === "native" ? "rgba(34, 197, 94, 0.4)" : "transparent"};
1375
+ opacity: ${isAvailable ? "1" : "0.5"};
1376
+ "
1377
+ >native</button>
1378
+ </div>
1379
+
1380
+ <div style="margin-top: 8px; color: #64748b; font-size: 10px; line-height: 1.4;">
1381
+ ${this.renderMode === "foreignObject" ? "SVG foreignObject serialization. Works everywhere but slower." : "Chrome's drawElementImage API. Fastest, requires chrome://flags/#canvas-draw-element."}
1382
+ </div>
1383
+ </div>
1384
+
1385
+ <!-- Preview Resolution Setting -->
1386
+ <div style="
1387
+ background: rgba(51, 65, 85, 0.4);
1388
+ border-radius: 8px;
1389
+ padding: 12px;
1390
+ margin-top: 10px;
1391
+ ">
1392
+ <div style="color: #e2e8f0; font-size: 12px; font-weight: 500; margin-bottom: 10px;">Preview Resolution</div>
1393
+
1394
+ <div style="display: flex; gap: 4px; background: rgba(30, 41, 59, 0.6); border-radius: 6px; padding: 3px;">
1395
+ <button
1396
+ @click=${() => this.handleResolutionScaleChange("auto")}
1397
+ style="
1398
+ flex: 1;
1399
+ padding: 6px 8px;
1400
+ border: none;
1401
+ border-radius: 4px;
1402
+ font-size: 10px;
1403
+ font-weight: 500;
1404
+ cursor: pointer;
1405
+ transition: all 0.15s ease;
1406
+ background: ${this.previewResolutionScale === "auto" ? "rgba(34, 197, 94, 0.3)" : "transparent"};
1407
+ color: ${this.previewResolutionScale === "auto" ? "#4ade80" : "#94a3b8"};
1408
+ border: 1px solid ${this.previewResolutionScale === "auto" ? "rgba(34, 197, 94, 0.4)" : "transparent"};
1409
+ "
1410
+ >Auto</button>
1411
+ <button
1412
+ @click=${() => this.handleResolutionScaleChange(1)}
1413
+ style="
1414
+ flex: 1;
1415
+ padding: 6px 8px;
1416
+ border: none;
1417
+ border-radius: 4px;
1418
+ font-size: 10px;
1419
+ font-weight: 500;
1420
+ cursor: pointer;
1421
+ transition: all 0.15s ease;
1422
+ background: ${this.previewResolutionScale === 1 ? "rgba(59, 130, 246, 0.3)" : "transparent"};
1423
+ color: ${this.previewResolutionScale === 1 ? "#60a5fa" : "#94a3b8"};
1424
+ border: 1px solid ${this.previewResolutionScale === 1 ? "rgba(59, 130, 246, 0.4)" : "transparent"};
1425
+ "
1426
+ >Full</button>
1427
+ <button
1428
+ @click=${() => this.handleResolutionScaleChange(.75)}
1429
+ style="
1430
+ flex: 1;
1431
+ padding: 6px 8px;
1432
+ border: none;
1433
+ border-radius: 4px;
1434
+ font-size: 10px;
1435
+ font-weight: 500;
1436
+ cursor: pointer;
1437
+ transition: all 0.15s ease;
1438
+ background: ${this.previewResolutionScale === .75 ? "rgba(59, 130, 246, 0.3)" : "transparent"};
1439
+ color: ${this.previewResolutionScale === .75 ? "#60a5fa" : "#94a3b8"};
1440
+ border: 1px solid ${this.previewResolutionScale === .75 ? "rgba(59, 130, 246, 0.4)" : "transparent"};
1441
+ "
1442
+ >3/4</button>
1443
+ <button
1444
+ @click=${() => this.handleResolutionScaleChange(.5)}
1445
+ style="
1446
+ flex: 1;
1447
+ padding: 6px 8px;
1448
+ border: none;
1449
+ border-radius: 4px;
1450
+ font-size: 10px;
1451
+ font-weight: 500;
1452
+ cursor: pointer;
1453
+ transition: all 0.15s ease;
1454
+ background: ${this.previewResolutionScale === .5 ? "rgba(59, 130, 246, 0.3)" : "transparent"};
1455
+ color: ${this.previewResolutionScale === .5 ? "#60a5fa" : "#94a3b8"};
1456
+ border: 1px solid ${this.previewResolutionScale === .5 ? "rgba(59, 130, 246, 0.4)" : "transparent"};
1457
+ "
1458
+ >1/2</button>
1459
+ <button
1460
+ @click=${() => this.handleResolutionScaleChange(.25)}
1461
+ style="
1462
+ flex: 1;
1463
+ padding: 6px 8px;
1464
+ border: none;
1465
+ border-radius: 4px;
1466
+ font-size: 10px;
1467
+ font-weight: 500;
1468
+ cursor: pointer;
1469
+ transition: all 0.15s ease;
1470
+ background: ${this.previewResolutionScale === .25 ? "rgba(59, 130, 246, 0.3)" : "transparent"};
1471
+ color: ${this.previewResolutionScale === .25 ? "#60a5fa" : "#94a3b8"};
1472
+ border: 1px solid ${this.previewResolutionScale === .25 ? "rgba(59, 130, 246, 0.4)" : "transparent"};
1473
+ "
1474
+ >1/4</button>
1475
+ </div>
1476
+
1477
+ <div style="margin-top: 8px; color: #64748b; font-size: 10px; line-height: 1.4;">
1478
+ ${this.previewResolutionScale === "auto" ? `Auto: Full resolution at rest, adaptive during playback/scrub.${!this.isAtRest ? ` Currently: ${Math.round(this.currentAdaptiveScale * 100)}%` : ""}` : this.previewResolutionScale === 1 ? "Full: Matches display resolution (1:1 pixels, adapts to zoom)." : `${Math.round(this.previewResolutionScale * 100)}%: Reduced quality for faster rendering.`}
1479
+ Canvas mode only.
1480
+ </div>
1481
+ </div>
1482
+
1483
+ <!-- Show Performance Stats Setting -->
1484
+ <div style="
1485
+ background: rgba(51, 65, 85, 0.4);
1486
+ border-radius: 8px;
1487
+ padding: 12px;
1488
+ margin-top: 10px;
1489
+ ">
1490
+ <label style="
1491
+ display: flex;
1492
+ align-items: center;
1493
+ gap: 8px;
1494
+ cursor: pointer;
1495
+ ">
1496
+ <input
1497
+ type="checkbox"
1498
+ ?checked=${this.showStats}
1499
+ @change=${(e) => this.handleShowStatsToggle(e.target.checked)}
1500
+ style="
1501
+ width: 14px;
1502
+ height: 14px;
1503
+ accent-color: #3b82f6;
1504
+ cursor: pointer;
1505
+ "
1506
+ />
1507
+ <span style="color: #e2e8f0; font-size: 12px; font-weight: 500;">Show Performance Stats</span>
1508
+ </label>
1509
+
1510
+ <div style="
1511
+ margin-top: 8px;
1512
+ color: #64748b;
1513
+ font-size: 10px;
1514
+ line-height: 1.4;
1515
+ ">
1516
+ Display FPS, CPU pressure, and performance metrics overlay.
1517
+ </div>
1518
+ </div>
1519
+
1520
+ <!-- Thumbnail Cache Setting -->
1521
+ <div
1522
+ data-testid="thumbnail-cache-section"
1523
+ style="
1524
+ background: rgba(51, 65, 85, 0.4);
1525
+ border-radius: 8px;
1526
+ padding: 12px;
1527
+ margin-top: 10px;
1528
+ "
1529
+ >
1530
+ <div style="color: #e2e8f0; font-size: 12px; font-weight: 500; margin-bottom: 10px;">Thumbnail Cache</div>
1531
+
1532
+ <!-- Cache Size Input -->
1533
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
1534
+ <label style="color: #94a3b8; font-size: 11px; min-width: 80px;">Max Size:</label>
1535
+ <input
1536
+ data-testid="thumbnail-cache-size"
1537
+ type="number"
1538
+ min="100"
1539
+ max="5000"
1540
+ step="100"
1541
+ .value=${String(this.thumbnailCacheMaxSize)}
1542
+ @change=${(e) => {
1543
+ const value = parseInt(e.target.value, 10);
1544
+ if (!isNaN(value) && value >= 100 && value <= 5e3) this.handleThumbnailCacheMaxSizeChange(value);
1545
+ }}
1546
+ style="
1547
+ flex: 1;
1548
+ padding: 4px 8px;
1549
+ background: rgba(30, 41, 59, 0.6);
1550
+ border: 1px solid rgba(148, 163, 184, 0.2);
1551
+ border-radius: 4px;
1552
+ color: #e2e8f0;
1553
+ font-size: 11px;
1554
+ "
1555
+ />
1556
+ <span style="color: #64748b; font-size: 10px;">items</span>
1557
+ </div>
1558
+
1559
+ <!-- Cache Statistics -->
1560
+ ${this.thumbnailCacheStats ? html`
1561
+ <div
1562
+ data-testid="thumbnail-cache-stats"
1563
+ style="
1564
+ background: rgba(30, 41, 59, 0.6);
1565
+ border-radius: 6px;
1566
+ padding: 8px;
1567
+ margin-bottom: 10px;
1568
+ font-size: 10px;
1569
+ color: #94a3b8;
1570
+ "
1571
+ >
1572
+ <div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
1573
+ <span>Items:</span>
1574
+ <span style="color: #e2e8f0; font-weight: 500;">${this.thumbnailCacheStats.itemCount} / ${this.thumbnailCacheStats.maxSize}</span>
1575
+ </div>
1576
+ <div style="display: flex; justify-content: space-between;">
1577
+ <span>Size:</span>
1578
+ <span style="color: #e2e8f0; font-weight: 500;">${this.formatBytes(this.thumbnailCacheStats.totalSizeBytes)}</span>
1579
+ </div>
1580
+ </div>
1581
+ ` : ""}
1582
+
1583
+ <!-- Clear Cache Button -->
1584
+ <button
1585
+ data-testid="thumbnail-cache-clear"
1586
+ @click=${() => this.handleClearThumbnailCache()}
1587
+ style="
1588
+ width: 100%;
1589
+ padding: 6px 10px;
1590
+ background: rgba(239, 68, 68, 0.2);
1591
+ border: 1px solid rgba(239, 68, 68, 0.4);
1592
+ border-radius: 4px;
1593
+ color: #f87171;
1594
+ font-size: 11px;
1595
+ font-weight: 500;
1596
+ cursor: pointer;
1597
+ transition: all 0.15s ease;
1598
+ "
1599
+ onmouseover="this.style.background='rgba(239, 68, 68, 0.3)'"
1600
+ onmouseout="this.style.background='rgba(239, 68, 68, 0.2)'"
1601
+ >
1602
+ Clear Cache
1603
+ </button>
1604
+
1605
+ <div style="
1606
+ margin-top: 8px;
1607
+ color: #64748b;
1608
+ font-size: 10px;
1609
+ line-height: 1.4;
1610
+ ">
1611
+ Persistent cache survives page reloads. Stored in IndexedDB.
1612
+ </div>
1613
+ </div>
1614
+
1615
+ <!-- Debug Thumbnails Setting -->
1616
+ <div style="
1617
+ background: rgba(51, 65, 85, 0.4);
1618
+ border-radius: 8px;
1619
+ padding: 12px;
1620
+ margin-top: 10px;
1621
+ ">
1622
+ <label style="
1623
+ display: flex;
1624
+ align-items: center;
1625
+ gap: 8px;
1626
+ cursor: pointer;
1627
+ ">
1628
+ <input
1629
+ type="checkbox"
1630
+ ?checked=${this.debugThumbnailTimestamps}
1631
+ @change=${(e) => this.handleDebugThumbnailTimestampsToggle(e.target.checked)}
1632
+ style="
1633
+ width: 14px;
1634
+ height: 14px;
1635
+ accent-color: #f59e0b;
1636
+ cursor: pointer;
1637
+ "
1638
+ />
1639
+ <span style="color: #e2e8f0; font-size: 12px; font-weight: 500;">Show Thumbnail Timestamps</span>
1640
+ </label>
1641
+
1642
+ <div style="
1643
+ margin-top: 8px;
1644
+ color: #64748b;
1645
+ font-size: 10px;
1646
+ line-height: 1.4;
1647
+ ">
1648
+ Overlays capture timestamps on timeline thumbnails for debugging.
1649
+ </div>
1650
+ </div>
1651
+ </div>
1652
+ `;
1653
+ }
1654
+ renderExportPopover() {
1655
+ const durationMs = this.getTimegroup()?.durationMs ?? 0;
1656
+ return html`
1657
+ <div
1658
+ id="export-popover"
1659
+ popover="auto"
1660
+ class="dropdown-panel"
1661
+ @toggle=${this.handleExportPopoverToggle}
1662
+ >
1663
+ <div class="dropdown-header">
1664
+ <span class="dropdown-title">Export Settings</span>
1665
+ <button class="dropdown-close" popovertarget="export-popover" popovertargetaction="hide">✕</button>
1666
+ </div>
1667
+
1668
+ <!-- Scale -->
1669
+ <div style="margin-bottom: 10px;">
1670
+ <label style="display: block; color: #94a3b8; font-size: 11px; margin-bottom: 4px;">Scale</label>
1671
+ <select
1672
+ style="
1673
+ width: 100%;
1674
+ padding: 6px 10px;
1675
+ background: rgba(51, 65, 85, 0.8);
1676
+ border: 1px solid rgba(148, 163, 184, 0.2);
1677
+ border-radius: 5px;
1678
+ color: #e2e8f0;
1679
+ font-size: 12px;
1680
+ cursor: pointer;
1681
+ "
1682
+ .value=${String(this.exportOptions.scale)}
1683
+ @change=${(e) => this.updateExportOption("scale", Number(e.target.value))}
1684
+ >
1685
+ <option value="1">100% (Full)</option>
1686
+ <option value="0.75">75%</option>
1687
+ <option value="0.5">50%</option>
1688
+ <option value="0.25">25%</option>
1689
+ </select>
1690
+ </div>
1691
+
1692
+ <!-- Audio -->
1693
+ <div style="margin-bottom: 10px;">
1694
+ <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
1695
+ <input
1696
+ type="checkbox"
1697
+ ?checked=${this.exportOptions.includeAudio}
1698
+ @change=${(e) => this.updateExportOption("includeAudio", e.target.checked)}
1699
+ style="width: 14px; height: 14px; accent-color: #3b82f6;"
1700
+ />
1701
+ <span style="color: #e2e8f0; font-size: 12px;">Include Audio</span>
1702
+ </label>
1703
+ </div>
1704
+
1705
+ <!-- In/Out Range -->
1706
+ <div style="margin-bottom: 12px;">
1707
+ <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; margin-bottom: 6px;">
1708
+ <input
1709
+ type="checkbox"
1710
+ ?checked=${this.exportOptions.useInOut}
1711
+ @change=${(e) => this.updateExportOption("useInOut", e.target.checked)}
1712
+ style="width: 14px; height: 14px; accent-color: #3b82f6;"
1713
+ />
1714
+ <span style="color: #e2e8f0; font-size: 12px;">Custom Range</span>
1715
+ </label>
1716
+
1717
+ ${this.exportOptions.useInOut ? html`
1718
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 6px;">
1719
+ <div>
1720
+ <label style="display: block; color: #94a3b8; font-size: 10px; margin-bottom: 2px;">In (ms)</label>
1721
+ <input
1722
+ type="number"
1723
+ min="0"
1724
+ max=${durationMs}
1725
+ .value=${String(this.exportOptions.inMs)}
1726
+ @change=${(e) => this.updateExportOption("inMs", Number(e.target.value))}
1727
+ style="
1728
+ width: 100%;
1729
+ padding: 5px 7px;
1730
+ background: rgba(51, 65, 85, 0.8);
1731
+ border: 1px solid rgba(148, 163, 184, 0.2);
1732
+ border-radius: 4px;
1733
+ color: #e2e8f0;
1734
+ font-size: 11px;
1735
+ font-family: ui-monospace, monospace;
1736
+ "
1737
+ />
1738
+ </div>
1739
+ <div>
1740
+ <label style="display: block; color: #94a3b8; font-size: 10px; margin-bottom: 2px;">Out (ms)</label>
1741
+ <input
1742
+ type="number"
1743
+ min="0"
1744
+ max=${durationMs}
1745
+ .value=${String(this.exportOptions.outMs)}
1746
+ @change=${(e) => this.updateExportOption("outMs", Number(e.target.value))}
1747
+ style="
1748
+ width: 100%;
1749
+ padding: 5px 7px;
1750
+ background: rgba(51, 65, 85, 0.8);
1751
+ border: 1px solid rgba(148, 163, 184, 0.2);
1752
+ border-radius: 4px;
1753
+ color: #e2e8f0;
1754
+ font-size: 11px;
1755
+ font-family: ui-monospace, monospace;
1756
+ "
1757
+ />
1758
+ </div>
1759
+ </div>
1760
+ <div style="color: #64748b; font-size: 10px; margin-top: 4px;">
1761
+ Duration: ${this.formatTime(this.exportOptions.outMs - this.exportOptions.inMs)} / ${this.formatTime(durationMs)}
1762
+ </div>
1763
+ ` : html`
1764
+ <div style="color: #64748b; font-size: 10px;">
1765
+ Full duration: ${this.formatTime(durationMs)}
1766
+ </div>
1767
+ `}
1768
+ </div>
1769
+
1770
+ <!-- Start Export button -->
1771
+ <button
1772
+ class="toolbar-btn primary"
1773
+ style="width: 100%; justify-content: center;"
1774
+ @click=${this.handleStartExport}
1775
+ popovertarget="export-popover"
1776
+ popovertargetaction="hide"
1777
+ >
1778
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1779
+ <polygon points="23 7 16 12 23 17 23 7"></polygon>
1780
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
1781
+ </svg>
1782
+ Start Export
1783
+ </button>
1784
+ </div>
1785
+ `;
1786
+ }
1787
+ renderExportProgressPopover() {
1788
+ const p = this.exportProgress;
1789
+ const progressPercent = p ? Math.round(p.progress * 100) : 0;
1790
+ const isComplete = this.exportStatus === "complete";
1791
+ const isError = this.exportStatus === "error";
1792
+ const isCancelled = this.exportStatus === "cancelled";
1793
+ const isRendering = this.exportStatus === "rendering";
1794
+ let statusColor;
1795
+ let statusText;
1796
+ if (isComplete) {
1797
+ statusColor = "#4ade80";
1798
+ statusText = "Complete!";
1799
+ } else if (isError) {
1800
+ statusColor = "#f87171";
1801
+ statusText = "Failed";
1802
+ } else if (isCancelled) {
1803
+ statusColor = "#fbbf24";
1804
+ statusText = "Cancelled";
1805
+ } else {
1806
+ statusColor = "#60a5fa";
1807
+ statusText = `${progressPercent}%`;
1808
+ }
1809
+ return html`
1810
+ <div
1811
+ id="export-progress-popover"
1812
+ popover="manual"
1813
+ class="dropdown-panel"
1814
+ style="min-width: 240px;"
1815
+ >
1816
+ <div class="dropdown-header">
1817
+ <span class="dropdown-title">Exporting</span>
1818
+ ${isRendering ? html`
1819
+ <button
1820
+ class="dropdown-close"
1821
+ style="color: #f87171;"
1822
+ @click=${this.handleCancelClick}
1823
+ >Cancel</button>
1824
+ ` : null}
1825
+ </div>
1826
+
1827
+ ${isRendering && p !== null ? html`
1828
+ ${p.framePreviewUrl ? html`
1829
+ <div style="margin-bottom: 10px; display: flex; justify-content: center;">
1830
+ <img
1831
+ src=${p.framePreviewUrl}
1832
+ alt="Current frame"
1833
+ style="
1834
+ border-radius: 4px;
1835
+ border: 1px solid rgba(148, 163, 184, 0.2);
1836
+ max-width: 100%;
1837
+ height: auto;
1838
+ "
1839
+ />
1840
+ </div>
1841
+ ` : null}
1842
+
1843
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px 12px; margin-bottom: 10px; font-family: ui-monospace, monospace; font-size: 10px;">
1844
+ <div>
1845
+ <div style="color: #64748b;">Frames</div>
1846
+ <div style="color: #e2e8f0;">${p.currentFrame} / ${p.totalFrames}</div>
1847
+ </div>
1848
+ <div>
1849
+ <div style="color: #64748b;">Time</div>
1850
+ <div style="color: #e2e8f0;">${this.formatTime(p.renderedMs)} / ${this.formatTime(p.totalDurationMs)}</div>
1851
+ </div>
1852
+ <div>
1853
+ <div style="color: #64748b;">Speed</div>
1854
+ <div style="color: ${p.speedMultiplier >= 1 ? "#4ade80" : "#fbbf24"};">${p.speedMultiplier.toFixed(2)}x</div>
1855
+ </div>
1856
+ <div>
1857
+ <div style="color: #64748b;">ETA</div>
1858
+ <div style="color: #e2e8f0;">${this.formatTime(p.estimatedRemainingMs)}</div>
1859
+ </div>
1860
+ </div>
1861
+ ` : null}
1862
+
1863
+ <div style="height: 4px; background: rgba(51, 65, 85, 0.8); border-radius: 2px; overflow: hidden;">
1864
+ <div style="
1865
+ height: 100%;
1866
+ width: ${progressPercent}%;
1867
+ background: ${statusColor};
1868
+ border-radius: 2px;
1869
+ transition: width 0.15s ease-out;
1870
+ "></div>
1871
+ </div>
1872
+
1873
+ <div style="text-align: center; margin-top: 6px; font-size: 11px; font-weight: 600; color: ${statusColor};">
1874
+ ${statusText}
1875
+ </div>
1876
+ </div>
1877
+
1878
+ <style>
1879
+ @keyframes spin {
1880
+ to { transform: rotate(360deg); }
1881
+ }
1882
+ </style>
1883
+ `;
1884
+ }
1885
+ renderToolbar() {
1886
+ return html`
1887
+ <div class="toolbar">
1888
+ <div class="toolbar-left">
1889
+ <!-- Fit to content button -->
1890
+ <button
1891
+ class="toolbar-icon-btn"
1892
+ @click=${this.handleFitToContent}
1893
+ title="Fit to Content (Reset Zoom & Center)"
1894
+ >
1895
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1896
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
1897
+ <path d="M8 12h8M12 8v8"></path>
1898
+ </svg>
1899
+ </button>
1900
+ </div>
1901
+
1902
+ <div class="toolbar-right">
1903
+ <!-- Mode indicator (shown when not in default clone mode) -->
1904
+ ${this.presentationMode !== "clone" ? html`
1905
+ <span class="mode-indicator ${this.presentationMode}">
1906
+ ${this.presentationMode === "dom" ? "DOM" : html`
1907
+ Canvas ${getRenderMode() === "native" ? phosphorIcon(ICONS.lightning, 12) : phosphorIcon(ICONS.code, 12)}
1908
+ `}
1909
+ </span>
1910
+ ` : null}
1911
+
1912
+ <!-- Settings button -->
1913
+ <button
1914
+ id="settings-btn"
1915
+ class="toolbar-icon-btn"
1916
+ popovertarget="settings-popover"
1917
+ title="Preview Settings"
1918
+ >
1919
+ ${phosphorIcon(ICONS.gear, 16)}
1920
+ </button>
1921
+
1922
+ <!-- Export button -->
1923
+ ${this.isExporting ? html`
1924
+ <button
1925
+ id="export-btn"
1926
+ class="toolbar-btn active"
1927
+ style="min-width: 100px;"
1928
+ popovertarget="export-progress-popover"
1929
+ >
1930
+ <div style="width: 12px; height: 12px; border: 2px solid rgba(96, 165, 250, 0.3); border-top-color: #60a5fa; border-radius: 50%; animation: spin 1s linear infinite;"></div>
1931
+ Exporting...
1932
+ </button>
1933
+ ` : html`
1934
+ <button
1935
+ id="export-btn"
1936
+ class="toolbar-btn primary"
1937
+ popovertarget="export-popover"
1938
+ >
1939
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1940
+ <polygon points="23 7 16 12 23 17 23 7"></polygon>
1941
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
1942
+ </svg>
1943
+ Export
1944
+ </button>
1945
+ `}
1946
+ </div>
1947
+ </div>
1948
+
1949
+ <!-- Popovers (rendered into top-layer) -->
1950
+ ${this.renderSettingsPopover()}
1951
+ ${this.renderExportPopover()}
1952
+ ${this.renderExportProgressPopover()}
1953
+ `;
67
1954
  }
68
1955
  update(changedProperties) {
69
1956
  super.update(changedProperties);
70
1957
  if (changedProperties.has("focusedElement")) this.drawOverlays();
71
1958
  }
1959
+ updated(changedProperties) {
1960
+ super.updated(changedProperties);
1961
+ if (this.getTimegroup() && !changedProperties.has("panZoomTransform")) {
1962
+ if (this.panZoomTransform.x === 0 && this.panZoomTransform.y === 0 && this.panZoomTransform.scale === 1) requestAnimationFrame(() => {
1963
+ this.restorePreviewPanZoom();
1964
+ });
1965
+ }
1966
+ if (changedProperties.has("previewSettings") || changedProperties.has("presentationMode") || changedProperties.has("showStats")) this.applySettings();
1967
+ if (changedProperties.has("isExporting")) {
1968
+ const popover = this.shadowRoot?.getElementById("export-progress-popover");
1969
+ if (popover) if (this.isExporting) {
1970
+ popover.showPopover();
1971
+ requestAnimationFrame(() => {
1972
+ this.positionPopover(popover, "export-btn");
1973
+ });
1974
+ } else popover.hidePopover();
1975
+ }
1976
+ }
1977
+ renderPlaybackStats() {
1978
+ if (!this.showStats || !this.statsStrategy) return null;
1979
+ const stats = this.statsStrategy.getStats();
1980
+ if (!stats) return null;
1981
+ const fpsClass = stats.fps >= 55 ? "good" : stats.fps >= 25 ? "warning" : "bad";
1982
+ const renderClass = stats.avgRenderTime !== null ? stats.avgRenderTime <= 20 ? "good" : stats.avgRenderTime <= 30 ? "warning" : "bad" : "";
1983
+ const headroomClass = stats.headroom !== null ? stats.headroom >= 10 ? "good" : stats.headroom >= 0 ? "warning" : "bad" : "";
1984
+ const pressureClass = stats.pressureState === "nominal" ? "good" : stats.pressureState === "fair" ? "good" : stats.pressureState === "serious" ? "warning" : "bad";
1985
+ const scaleClass = stats.resolutionScale !== null ? stats.resolutionScale >= .75 ? "good" : stats.resolutionScale >= .5 ? "warning" : "bad" : "";
1986
+ const motionState = this.isAtRest ? "At Rest" : this.isPlaying ? "Playing" : this.isScrubbing ? "Scrubbing" : "Idle";
1987
+ const renderPressureHistogram = () => {
1988
+ if (stats.pressureHistory.length === 0) return html`<div style="color: #64748b; font-size: 9px;">No pressure data (API not available)</div>`;
1989
+ return html`
1990
+ <div class="pressure-histogram">
1991
+ ${stats.pressureHistory.map((state$1) => html`
1992
+ <div class="bar ${state$1}"></div>
1993
+ `)}
1994
+ </div>
1995
+ <div class="pressure-histogram-label">
1996
+ <span>30s ago</span>
1997
+ <span>now</span>
1998
+ </div>
1999
+ `;
2000
+ };
2001
+ const padNum = (n, decimals, width) => {
2002
+ return n.toFixed(decimals).padStart(width, " ");
2003
+ };
2004
+ return html`
2005
+ <div class="playback-stats">
2006
+ <div class="stat-row">
2007
+ <span class="stat-label">FPS</span>
2008
+ <span class="stat-value ${fpsClass}">${padNum(stats.fps, 1, 5)}</span>
2009
+ </div>
2010
+ ${this.statsStrategy.supportsStat("renderTime") && stats.avgRenderTime !== null ? html`
2011
+ <div class="stat-row">
2012
+ <span class="stat-label">Render</span>
2013
+ <span class="stat-value ${renderClass}">${padNum(stats.avgRenderTime, 1, 5)}ms</span>
2014
+ </div>
2015
+ ` : null}
2016
+ ${this.statsStrategy.supportsStat("headroom") && stats.headroom !== null ? html`
2017
+ <div class="stat-row">
2018
+ <span class="stat-label">Headroom</span>
2019
+ <span class="stat-value ${headroomClass}">${stats.headroom >= 0 ? "+" : ""}${padNum(stats.headroom, 1, 4)}ms</span>
2020
+ </div>
2021
+ ` : null}
2022
+ <div class="stat-row">
2023
+ <span class="stat-label">Resolution</span>
2024
+ <span class="stat-value">${stats.renderWidth}×${stats.renderHeight}</span>
2025
+ </div>
2026
+ ${this.statsStrategy.supportsStat("resolutionScale") && stats.resolutionScale !== null ? html`
2027
+ <div class="stat-row">
2028
+ <span class="stat-label">Scale</span>
2029
+ <span class="stat-value ${scaleClass}">${String(Math.round(stats.resolutionScale * 100)).padStart(3, " ")}%</span>
2030
+ </div>
2031
+ ` : null}
2032
+ <div class="stat-row">
2033
+ <span class="stat-label">CPU</span>
2034
+ <span class="stat-value ${pressureClass}">${stats.pressureState}</span>
2035
+ </div>
2036
+ <div class="stat-row">
2037
+ <span class="stat-label">State</span>
2038
+ <span class="stat-value">${motionState}</span>
2039
+ </div>
2040
+ ${this.statsStrategy.supportsStat("adaptiveResolution") && this.previewResolutionScale === "auto" && stats.samplesAtCurrentScale !== void 0 ? html`
2041
+ <div style="margin-top: 4px; padding-top: 4px; border-top: 1px solid rgba(148, 163, 184, 0.2);">
2042
+ <div class="stat-row">
2043
+ <span class="stat-label">Mode</span>
2044
+ <span class="stat-value good">Auto</span>
2045
+ </div>
2046
+ <div class="stat-row">
2047
+ <span class="stat-label">Samples</span>
2048
+ <span class="stat-value">${String(stats.samplesAtCurrentScale).padStart(3, " ")}/60</span>
2049
+ </div>
2050
+ <div class="stat-row">
2051
+ <span class="stat-label">Scale Up</span>
2052
+ <span class="stat-value ${stats.canScaleUp ? "good" : ""}">${stats.canScaleUp ? "Ready" : "Waiting"}</span>
2053
+ </div>
2054
+ <div class="stat-row">
2055
+ <span class="stat-label">Scale Down</span>
2056
+ <span class="stat-value ${stats.canScaleDown ? "" : "warning"}">${stats.canScaleDown ? "Ready" : "Min"}</span>
2057
+ </div>
2058
+ </div>
2059
+ ` : null}
2060
+
2061
+ <!-- CPU Pressure Histogram -->
2062
+ <div style="margin-top: 8px;">
2063
+ <div style="color: #94a3b8; font-size: 10px; margin-bottom: 4px;">CPU Pressure History</div>
2064
+ ${renderPressureHistogram()}
2065
+ </div>
2066
+ </div>
2067
+ `;
2068
+ }
72
2069
  render() {
73
- if (this.rendering || typeof window !== "undefined" && window.EF_RENDERING?.() === true) return html`
2070
+ if (this.rendering) return html`
74
2071
  <slot class="fixed inset-0 h-full w-full" name="canvas"></slot>
75
2072
  `;
76
2073
  return html`
77
2074
  <div
78
- class="grid h-full w-full"
79
- style="grid-template-rows: 1fr 300px; grid-template-columns: 100%; background-color: var(--workbench-bg);"
2075
+ class="grid overflow-hidden"
2076
+ style="flex: 1; min-height: 0; width: 100%; grid-template-rows: auto 1fr 280px; grid-template-columns: 280px 1fr; background-color: var(--workbench-bg);"
80
2077
  >
2078
+ <!-- Top: Full-width Toolbar -->
2079
+ <div style="grid-row: 1 / 2; grid-column: 1 / -1;">
2080
+ ${this.renderToolbar()}
2081
+ </div>
2082
+
2083
+ <!-- Left: Hierarchy Panel -->
2084
+ <div
2085
+ style="grid-row: 2 / 3; grid-column: 1 / 2; background: rgb(30 41 59); border-right: 1px solid rgba(148, 163, 184, 0.2); min-height: 0; max-height: 100%; display: flex; flex-direction: column; overflow: hidden;"
2086
+ >
2087
+ <slot name="hierarchy"></slot>
2088
+ </div>
2089
+
2090
+ <!-- Center: Canvas area -->
81
2091
  <div
82
- class="relative h-full w-full overflow-hidden"
2092
+ class="canvas-container"
2093
+ style="grid-row: 2 / 3; grid-column: 2 / 3; min-height: 0;"
83
2094
  @wheel=${this.handleStageWheel}
84
2095
  >
85
- <ef-fit-scale class="h-full grid place-content-center">
86
- <slot name="canvas" class="contents"></slot>
87
- </ef-fit-scale>
88
- <div
89
- class="border bg-opacity-20 absolute"
90
- style="border-color: var(--workbench-overlay-border); background-color: var(--workbench-overlay-bg);"
91
- ${ref(this.focusOverlay)}
2096
+ <!-- Original timegroup (hidden in clone/canvas mode, visible in dom mode) -->
2097
+ <slot name="canvas"></slot>
2098
+
2099
+ <!-- Clone overlay (visible in clone mode only) -->
2100
+ <div
2101
+ class="clone-overlay"
2102
+ ${ref(this.cloneOverlayRef)}
2103
+ style="display: ${this.presentationMode === "clone" ? "block" : "none"}"
2104
+ ></div>
2105
+
2106
+ <!-- Canvas preview (visible in canvas mode only) -->
2107
+ <div
2108
+ class="clone-overlay"
2109
+ ${ref(this.canvasPreviewRef)}
2110
+ style="display: ${this.presentationMode === "canvas" ? "block" : "none"}"
92
2111
  ></div>
2112
+
2113
+ <!-- Playback stats overlay (visible in canvas mode only) -->
2114
+ ${this.renderPlaybackStats()}
93
2115
  </div>
94
2116
 
95
- <slot class="overflow inline-block" name="timeline"></slot>
2117
+ <!-- Bottom: Timeline -->
2118
+ <div
2119
+ class="overflow-hidden"
2120
+ style="grid-row: 3 / 4; grid-column: 1 / -1; width: 100%; border-top: 1px solid rgba(148, 163, 184, 0.2);"
2121
+ >
2122
+ <slot name="timeline"></slot>
2123
+ </div>
96
2124
  </div>
97
2125
  `;
98
2126
  }
99
2127
  };
100
2128
  __decorate([property({ type: Boolean })], EFWorkbench.prototype, "rendering", void 0);
2129
+ __decorate([state()], EFWorkbench.prototype, "panZoomTransform", void 0);
2130
+ __decorate([state()], EFWorkbench.prototype, "isExporting", void 0);
2131
+ __decorate([state()], EFWorkbench.prototype, "exportProgress", void 0);
2132
+ __decorate([state()], EFWorkbench.prototype, "exportStatus", void 0);
2133
+ __decorate([provide({ context: previewSettingsContext }), state()], EFWorkbench.prototype, "previewSettings", void 0);
2134
+ __decorate([state()], EFWorkbench.prototype, "renderMode", void 0);
2135
+ __decorate([state()], EFWorkbench.prototype, "presentationMode", void 0);
2136
+ __decorate([state()], EFWorkbench.prototype, "previewResolutionScale", void 0);
2137
+ __decorate([state()], EFWorkbench.prototype, "debugThumbnailTimestamps", void 0);
2138
+ __decorate([state()], EFWorkbench.prototype, "thumbnailCacheMaxSize", void 0);
2139
+ __decorate([state()], EFWorkbench.prototype, "thumbnailCacheStats", void 0);
2140
+ __decorate([state()], EFWorkbench.prototype, "exportOptions", void 0);
2141
+ __decorate([state()], EFWorkbench.prototype, "isPlaying", void 0);
2142
+ __decorate([state()], EFWorkbench.prototype, "isScrubbing", void 0);
2143
+ __decorate([state()], EFWorkbench.prototype, "isAtRest", void 0);
2144
+ __decorate([state()], EFWorkbench.prototype, "currentAdaptiveScale", void 0);
2145
+ __decorate([state()], EFWorkbench.prototype, "showStats", void 0);
101
2146
  __decorate([eventOptions({
102
2147
  passive: false,
103
2148
  capture: true