@editframe/elements 0.37.3-beta → 0.38.1

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 (327) hide show
  1. package/dist/EF_FRAMEGEN.js +17 -14
  2. package/dist/EF_FRAMEGEN.js.map +1 -1
  3. package/dist/EF_RENDERING.js.map +1 -1
  4. package/dist/canvas/EFCanvas.d.ts +9 -2
  5. package/dist/canvas/EFCanvas.js +14 -4
  6. package/dist/canvas/EFCanvas.js.map +1 -1
  7. package/dist/canvas/EFCanvasItem.d.ts +2 -2
  8. package/dist/canvas/overlays/SelectionOverlay.d.ts +10 -2
  9. package/dist/canvas/overlays/SelectionOverlay.js +5 -12
  10. package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
  11. package/dist/canvas/overlays/overlayState.js.map +1 -1
  12. package/dist/canvas/selection/SelectionController.js.map +1 -1
  13. package/dist/elements/EFAudio.d.ts +1 -11
  14. package/dist/elements/EFAudio.js +2 -10
  15. package/dist/elements/EFAudio.js.map +1 -1
  16. package/dist/elements/EFCaptions.d.ts +5 -9
  17. package/dist/elements/EFCaptions.js +34 -11
  18. package/dist/elements/EFCaptions.js.map +1 -1
  19. package/dist/elements/EFImage.d.ts +10 -8
  20. package/dist/elements/EFImage.js +117 -32
  21. package/dist/elements/EFImage.js.map +1 -1
  22. package/dist/elements/EFMedia/AssetMediaEngine.js +2 -2
  23. package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
  24. package/dist/elements/EFMedia/BaseMediaEngine.js +15 -92
  25. package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
  26. package/dist/elements/EFMedia/BufferedSeekingInput.js +10 -11
  27. package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
  28. package/dist/elements/EFMedia/{AssetIdMediaEngine.js → FileMediaEngine.js} +44 -24
  29. package/dist/elements/EFMedia/FileMediaEngine.js.map +1 -0
  30. package/dist/elements/EFMedia/JitMediaEngine.js +14 -13
  31. package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
  32. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
  33. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  34. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +12 -7
  35. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  36. package/dist/elements/EFMedia/shared/timeoutUtils.js +44 -0
  37. package/dist/elements/EFMedia/shared/timeoutUtils.js.map +1 -0
  38. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +1 -1
  39. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
  40. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +4 -4
  41. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
  42. package/dist/elements/EFMedia.d.ts +14 -8
  43. package/dist/elements/EFMedia.js +52 -19
  44. package/dist/elements/EFMedia.js.map +1 -1
  45. package/dist/elements/EFPanZoom.d.ts +2 -2
  46. package/dist/elements/EFPanZoom.js +1 -1
  47. package/dist/elements/EFPanZoom.js.map +1 -1
  48. package/dist/elements/EFSourceMixin.js +16 -8
  49. package/dist/elements/EFSourceMixin.js.map +1 -1
  50. package/dist/elements/EFSurface.d.ts +5 -8
  51. package/dist/elements/EFSurface.js +4 -43
  52. package/dist/elements/EFSurface.js.map +1 -1
  53. package/dist/elements/EFTemporal.d.ts +33 -8
  54. package/dist/elements/EFTemporal.js +92 -40
  55. package/dist/elements/EFTemporal.js.map +1 -1
  56. package/dist/elements/EFText.d.ts +3 -0
  57. package/dist/elements/EFText.js +54 -21
  58. package/dist/elements/EFText.js.map +1 -1
  59. package/dist/elements/EFTextSegment.js +8 -4
  60. package/dist/elements/EFTextSegment.js.map +1 -1
  61. package/dist/elements/EFTimegroup.d.ts +26 -43
  62. package/dist/elements/EFTimegroup.js +295 -314
  63. package/dist/elements/EFTimegroup.js.map +1 -1
  64. package/dist/elements/EFVideo.d.ts +44 -42
  65. package/dist/elements/EFVideo.js +259 -172
  66. package/dist/elements/EFVideo.js.map +1 -1
  67. package/dist/elements/EFWaveform.d.ts +3 -8
  68. package/dist/elements/EFWaveform.js +18 -13
  69. package/dist/elements/EFWaveform.js.map +1 -1
  70. package/dist/elements/ElementPositionInfo.js.map +1 -1
  71. package/dist/elements/FetchMixin.js.map +1 -1
  72. package/dist/elements/TargetController.d.ts +0 -3
  73. package/dist/elements/TargetController.js +12 -35
  74. package/dist/elements/TargetController.js.map +1 -1
  75. package/dist/elements/TimegroupController.js.map +1 -1
  76. package/dist/elements/cloneFactoryRegistry.d.ts +14 -0
  77. package/dist/elements/cloneFactoryRegistry.js +15 -0
  78. package/dist/elements/cloneFactoryRegistry.js.map +1 -0
  79. package/dist/elements/renderTemporalAudio.js +8 -6
  80. package/dist/elements/renderTemporalAudio.js.map +1 -1
  81. package/dist/elements/setupTemporalHierarchy.js +62 -0
  82. package/dist/elements/setupTemporalHierarchy.js.map +1 -0
  83. package/dist/elements/updateAnimations.js +62 -87
  84. package/dist/elements/updateAnimations.js.map +1 -1
  85. package/dist/getRenderInfo.d.ts +3 -2
  86. package/dist/getRenderInfo.js +20 -4
  87. package/dist/getRenderInfo.js.map +1 -1
  88. package/dist/gui/ContextMixin.js +68 -12
  89. package/dist/gui/ContextMixin.js.map +1 -1
  90. package/dist/gui/Controllable.js +1 -1
  91. package/dist/gui/Controllable.js.map +1 -1
  92. package/dist/gui/EFActiveRootTemporal.d.ts +2 -2
  93. package/dist/gui/EFActiveRootTemporal.js.map +1 -1
  94. package/dist/gui/EFControls.d.ts +2 -2
  95. package/dist/gui/EFControls.js +2 -2
  96. package/dist/gui/EFControls.js.map +1 -1
  97. package/dist/gui/EFDial.d.ts +2 -2
  98. package/dist/gui/EFDial.js +12 -9
  99. package/dist/gui/EFDial.js.map +1 -1
  100. package/dist/gui/EFFilmstrip.d.ts +2 -0
  101. package/dist/gui/EFFilmstrip.js +18 -10
  102. package/dist/gui/EFFilmstrip.js.map +1 -1
  103. package/dist/gui/EFFitScale.d.ts +28 -4
  104. package/dist/gui/EFFitScale.js +88 -26
  105. package/dist/gui/EFFitScale.js.map +1 -1
  106. package/dist/gui/EFFocusOverlay.d.ts +2 -2
  107. package/dist/gui/EFFocusOverlay.js +3 -3
  108. package/dist/gui/EFFocusOverlay.js.map +1 -1
  109. package/dist/gui/EFOverlayItem.d.ts +2 -2
  110. package/dist/gui/EFOverlayLayer.d.ts +2 -2
  111. package/dist/gui/EFPause.d.ts +2 -2
  112. package/dist/gui/EFPause.js +1 -1
  113. package/dist/gui/EFPlay.d.ts +2 -2
  114. package/dist/gui/EFPlay.js +1 -1
  115. package/dist/gui/EFPreview.js +1 -1
  116. package/dist/gui/EFResizableBox.d.ts +2 -2
  117. package/dist/gui/EFResizableBox.js +5 -5
  118. package/dist/gui/EFResizableBox.js.map +1 -1
  119. package/dist/gui/EFScrubber.d.ts +2 -2
  120. package/dist/gui/EFScrubber.js +8 -13
  121. package/dist/gui/EFScrubber.js.map +1 -1
  122. package/dist/gui/EFTimeDisplay.d.ts +6 -2
  123. package/dist/gui/EFTimeDisplay.js +25 -7
  124. package/dist/gui/EFTimeDisplay.js.map +1 -1
  125. package/dist/gui/EFTimelineRuler.d.ts +2 -2
  126. package/dist/gui/EFTimelineRuler.js +3 -3
  127. package/dist/gui/EFTimelineRuler.js.map +1 -1
  128. package/dist/gui/EFToggleLoop.d.ts +2 -2
  129. package/dist/gui/EFToggleLoop.js +1 -1
  130. package/dist/gui/EFTogglePlay.d.ts +2 -2
  131. package/dist/gui/EFTogglePlay.js +1 -1
  132. package/dist/gui/EFTransformHandles.d.ts +2 -2
  133. package/dist/gui/EFTransformHandles.js +6 -6
  134. package/dist/gui/EFTransformHandles.js.map +1 -1
  135. package/dist/gui/EFWorkbench.d.ts +40 -36
  136. package/dist/gui/EFWorkbench.js +436 -822
  137. package/dist/gui/EFWorkbench.js.map +1 -1
  138. package/dist/gui/FitScaleHelpers.js.map +1 -1
  139. package/dist/gui/PlaybackController.d.ts +3 -8
  140. package/dist/gui/PlaybackController.js +59 -56
  141. package/dist/gui/PlaybackController.js.map +1 -1
  142. package/dist/gui/TWMixin.js +1 -1
  143. package/dist/gui/TWMixin.js.map +1 -1
  144. package/dist/gui/TargetOrContextMixin.js +43 -6
  145. package/dist/gui/TargetOrContextMixin.js.map +1 -1
  146. package/dist/gui/ef-theme.css +136 -0
  147. package/dist/gui/hierarchy/EFHierarchy.d.ts +2 -2
  148. package/dist/gui/hierarchy/EFHierarchy.js +14 -24
  149. package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
  150. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
  151. package/dist/gui/hierarchy/EFHierarchyItem.js +22 -10
  152. package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
  153. package/dist/gui/icons.js.map +1 -1
  154. package/dist/gui/previewSettingsContext.d.ts +18 -0
  155. package/dist/gui/previewSettingsContext.js.map +1 -1
  156. package/dist/gui/theme.js +34 -0
  157. package/dist/gui/theme.js.map +1 -0
  158. package/dist/gui/timeline/EFTimeline.d.ts +2 -2
  159. package/dist/gui/timeline/EFTimeline.js +70 -52
  160. package/dist/gui/timeline/EFTimeline.js.map +1 -1
  161. package/dist/gui/timeline/EFTimelineRow.d.ts +5 -3
  162. package/dist/gui/timeline/EFTimelineRow.js +55 -32
  163. package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
  164. package/dist/gui/timeline/TrimHandles.d.ts +23 -9
  165. package/dist/gui/timeline/TrimHandles.js +224 -51
  166. package/dist/gui/timeline/TrimHandles.js.map +1 -1
  167. package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
  168. package/dist/gui/timeline/timelineEditingContext.d.ts +34 -0
  169. package/dist/gui/timeline/timelineEditingContext.js +24 -0
  170. package/dist/gui/timeline/timelineEditingContext.js.map +1 -0
  171. package/dist/gui/timeline/timelineStateContext.js.map +1 -1
  172. package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
  173. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
  174. package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +2 -3
  175. package/dist/gui/timeline/tracks/CaptionsTrack.js +17 -75
  176. package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
  177. package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +52 -0
  178. package/dist/gui/timeline/tracks/EFThumbnailStrip.js +596 -0
  179. package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -0
  180. package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -1
  181. package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
  182. package/dist/gui/timeline/tracks/TextTrack.d.ts +3 -2
  183. package/dist/gui/timeline/tracks/TextTrack.js +17 -43
  184. package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
  185. package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +3 -4
  186. package/dist/gui/timeline/tracks/TimegroupTrack.js +33 -23
  187. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
  188. package/dist/gui/timeline/tracks/TrackItem.d.ts +7 -9
  189. package/dist/gui/timeline/tracks/TrackItem.js +18 -17
  190. package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
  191. package/dist/gui/timeline/tracks/VideoTrack.d.ts +3 -3
  192. package/dist/gui/timeline/tracks/VideoTrack.js +11 -14
  193. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
  194. package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -1
  195. package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
  196. package/dist/gui/timeline/tracks/waveformUtils.js +1 -1
  197. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
  198. package/dist/gui/tree/EFTree.d.ts +2 -2
  199. package/dist/gui/tree/EFTree.js +8 -14
  200. package/dist/gui/tree/EFTree.js.map +1 -1
  201. package/dist/gui/tree/EFTreeItem.d.ts +2 -2
  202. package/dist/gui/tree/EFTreeItem.js +3 -3
  203. package/dist/gui/tree/EFTreeItem.js.map +1 -1
  204. package/dist/gui/tree/treeContext.js.map +1 -1
  205. package/dist/index.d.ts +10 -8
  206. package/dist/index.js +6 -5
  207. package/dist/index.js.map +1 -1
  208. package/dist/node.d.ts +2 -2
  209. package/dist/node.js +2 -2
  210. package/dist/preview/AdaptiveResolutionTracker.js +3 -3
  211. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
  212. package/dist/preview/FrameController.d.ts +2 -17
  213. package/dist/preview/FrameController.js +40 -63
  214. package/dist/preview/FrameController.js.map +1 -1
  215. package/dist/preview/QualityUpgradeScheduler.d.ts +76 -0
  216. package/dist/preview/QualityUpgradeScheduler.js +158 -0
  217. package/dist/preview/QualityUpgradeScheduler.js.map +1 -0
  218. package/dist/preview/RenderContext.d.ts +119 -1
  219. package/dist/preview/RenderContext.js +21 -3
  220. package/dist/preview/RenderContext.js.map +1 -1
  221. package/dist/preview/RenderProfiler.js.map +1 -1
  222. package/dist/preview/RenderStats.js +85 -0
  223. package/dist/preview/RenderStats.js.map +1 -0
  224. package/dist/preview/encoding/canvasEncoder.js +2 -52
  225. package/dist/preview/encoding/canvasEncoder.js.map +1 -1
  226. package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
  227. package/dist/preview/encoding/workerEncoder.js.map +1 -1
  228. package/dist/preview/logger.js.map +1 -1
  229. package/dist/preview/previewSettings.d.ts +34 -0
  230. package/dist/preview/previewSettings.js +29 -17
  231. package/dist/preview/previewSettings.js.map +1 -1
  232. package/dist/preview/previewTypes.js +4 -4
  233. package/dist/preview/previewTypes.js.map +1 -1
  234. package/dist/preview/renderElementToCanvas.d.ts +44 -0
  235. package/dist/preview/renderElementToCanvas.js +72 -0
  236. package/dist/preview/renderElementToCanvas.js.map +1 -0
  237. package/dist/preview/renderTimegroupToCanvas.d.ts +134 -32
  238. package/dist/preview/renderTimegroupToCanvas.js +321 -146
  239. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  240. package/dist/preview/renderTimegroupToCanvas.types.d.ts +51 -0
  241. package/dist/preview/renderTimegroupToVideo.d.ts +20 -35
  242. package/dist/preview/renderTimegroupToVideo.js +94 -106
  243. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  244. package/dist/preview/renderTimegroupToVideo.types.d.ts +42 -0
  245. package/dist/preview/renderVideoToVideo.js +286 -0
  246. package/dist/preview/renderVideoToVideo.js.map +1 -0
  247. package/dist/preview/renderers.d.ts +56 -0
  248. package/dist/preview/renderers.js +13 -1
  249. package/dist/preview/renderers.js.map +1 -1
  250. package/dist/preview/rendering/ScaleConfig.js +74 -0
  251. package/dist/preview/rendering/ScaleConfig.js.map +1 -0
  252. package/dist/preview/rendering/inlineImages.d.ts +13 -0
  253. package/dist/preview/rendering/inlineImages.js +7 -44
  254. package/dist/preview/rendering/inlineImages.js.map +1 -1
  255. package/dist/preview/rendering/loadImage.d.ts +8 -0
  256. package/dist/preview/rendering/loadImage.js +22 -0
  257. package/dist/preview/rendering/loadImage.js.map +1 -0
  258. package/dist/preview/rendering/renderToImageNative.js +3 -3
  259. package/dist/preview/rendering/renderToImageNative.js.map +1 -1
  260. package/dist/preview/rendering/serializeTimelineDirect.js +224 -68
  261. package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
  262. package/dist/preview/statsTrackingStrategy.js +1 -101
  263. package/dist/preview/statsTrackingStrategy.js.map +1 -1
  264. package/dist/preview/workers/WorkerPool.js +0 -1
  265. package/dist/preview/workers/WorkerPool.js.map +1 -1
  266. package/dist/preview/workers/encoderWorkerInline.js +21 -54
  267. package/dist/preview/workers/encoderWorkerInline.js.map +1 -1
  268. package/dist/render/EFRenderAPI.d.ts +2 -1
  269. package/dist/render/EFRenderAPI.js +12 -36
  270. package/dist/render/EFRenderAPI.js.map +1 -1
  271. package/dist/render/getRenderData.js +4 -4
  272. package/dist/render/getRenderData.js.map +1 -1
  273. package/dist/style.css +114 -163
  274. package/dist/transcoding/cache/RequestDeduplicator.js +1 -0
  275. package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
  276. package/dist/transcoding/types/index.d.ts +1 -1
  277. package/dist/transcoding/utils/UrlGenerator.js +10 -3
  278. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  279. package/dist/utils/LRUCache.js +1 -0
  280. package/dist/utils/LRUCache.js.map +1 -1
  281. package/dist/utils/frameTime.js +23 -1
  282. package/dist/utils/frameTime.js.map +1 -1
  283. package/package.json +45 -8
  284. package/scripts/build-css.js +8 -1
  285. package/test/setup.ts +0 -1
  286. package/test/useAssetMSW.ts +50 -0
  287. package/test/visualRegressionUtils.ts +23 -9
  288. package/tsdown.config.ts +6 -1
  289. package/dist/_virtual/rolldown_runtime.js +0 -27
  290. package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +0 -1
  291. package/dist/elements/EFThumbnailStrip.d.ts +0 -167
  292. package/dist/elements/EFThumbnailStrip.js +0 -731
  293. package/dist/elements/EFThumbnailStrip.js.map +0 -1
  294. package/dist/elements/SessionThumbnailCache.js +0 -154
  295. package/dist/elements/SessionThumbnailCache.js.map +0 -1
  296. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +0 -688
  297. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +0 -1
  298. package/dist/node_modules/react/cjs/react.development.js +0 -1521
  299. package/dist/node_modules/react/cjs/react.development.js.map +0 -1
  300. package/dist/node_modules/react/index.js +0 -13
  301. package/dist/node_modules/react/index.js.map +0 -1
  302. package/dist/node_modules/react/jsx-runtime.js +0 -13
  303. package/dist/node_modules/react/jsx-runtime.js.map +0 -1
  304. package/dist/preview/encoding/types.d.ts +0 -1
  305. package/dist/preview/renderTimegroupPreview.js +0 -686
  306. package/dist/preview/renderTimegroupPreview.js.map +0 -1
  307. package/dist/preview/rendering/renderToImage.d.ts +0 -2
  308. package/dist/preview/rendering/renderToImage.js +0 -95
  309. package/dist/preview/rendering/renderToImage.js.map +0 -1
  310. package/dist/preview/rendering/renderToImageForeignObject.js +0 -163
  311. package/dist/preview/rendering/renderToImageForeignObject.js.map +0 -1
  312. package/dist/preview/rendering/renderToImageNative.d.ts +0 -1
  313. package/dist/preview/rendering/svgSerializer.js +0 -43
  314. package/dist/preview/rendering/svgSerializer.js.map +0 -1
  315. package/dist/preview/rendering/types.d.ts +0 -2
  316. package/dist/preview/thumbnailCacheSettings.js +0 -52
  317. package/dist/preview/thumbnailCacheSettings.js.map +0 -1
  318. package/dist/sandbox/PlaybackControls.d.ts +0 -1
  319. package/dist/sandbox/PlaybackControls.js +0 -10
  320. package/dist/sandbox/PlaybackControls.js.map +0 -1
  321. package/dist/sandbox/ScenarioRunner.d.ts +0 -1
  322. package/dist/sandbox/ScenarioRunner.js +0 -1
  323. package/dist/sandbox/defineSandbox.d.ts +0 -1
  324. package/dist/sandbox/index.d.ts +0 -3
  325. package/dist/sandbox/index.js +0 -2
  326. package/test/EFVideo.framegen.browsertest.ts +0 -80
  327. package/test/thumbnail-performance-test.html +0 -116
@@ -1 +1 @@
1
- {"version":3,"file":"SelectionOverlay.js","names":["SelectionOverlay","panZoom","current: Node | null","canvasWithMetadata: CanvasWithMetadata"],"sources":["../../../src/canvas/overlays/SelectionOverlay.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport {\n selectionContext,\n type SelectionContext,\n} from \"../selection/selectionContext.js\";\nimport { panZoomTransformContext } from \"../../gui/panZoomTransformContext.js\";\nimport type { PanZoomTransform } from \"../../elements/EFPanZoom.js\";\nimport {\n type OverlayState,\n type CanvasWithMetadata,\n getOverlayTargets,\n calculateOverlayState,\n} from \"./overlayState.js\";\n\n/**\n * Selection overlay that renders unscaled selection indicators.\n * Uses fixed positioning to ensure 1:1 pixel ratio regardless of zoom level.\n */\n@customElement(\"ef-canvas-selection-overlay\")\nexport class SelectionOverlay extends LitElement {\n static styles = [\n css`\n :host {\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n pointer-events: none;\n z-index: 1000;\n }\n .box-select {\n position: absolute;\n border: 2px dashed rgb(59, 130, 246);\n background: rgba(59, 130, 246, 0.05);\n pointer-events: none;\n }\n .highlight-box {\n position: absolute;\n border: 2px solid rgb(148, 163, 184);\n background: rgba(148, 163, 184, 0.1);\n pointer-events: none;\n box-shadow: 0 0 0 2px rgba(148, 163, 184, 0.3);\n }\n `,\n ];\n\n createRenderRoot() {\n // Return this to render directly to the element (no shadow DOM)\n // This allows the overlay to use fixed positioning relative to viewport\n // Lit will inject styles as a <style> element when createRenderRoot returns this\n return this;\n }\n\n firstUpdated(\n changedProperties: Map<string | number | symbol, unknown>,\n ): void {\n super.firstUpdated?.(changedProperties);\n // When createRenderRoot returns this, Lit injects styles as a <style> element\n // Verify styles are present and log for debugging\n // Only check/warn if we're not in a test environment where styles might not be injected\n const styleElement = this.querySelector(\"style\");\n if (!styleElement) {\n // Only warn if we're in a context where styles are expected (not in isolated test scenarios)\n // Check if we're in a sandbox/test container by looking for common test container attributes\n const isInTestContainer = this.closest(\"[data-test-container]\") !== null ||\n this.closest(\"#sandbox-container\") !== null ||\n window.location.pathname.includes(\"scenario-runner\");\n \n if (!isInTestContainer) {\n console.warn(\n \"[SelectionOverlay] No style element found - styles may not be applied\",\n );\n }\n } else {\n console.log(\n \"[SelectionOverlay] Style element found, content length:\",\n styleElement.textContent?.length || 0,\n );\n }\n }\n\n @consume({ context: selectionContext, subscribe: true })\n selectionFromContext?: SelectionContext;\n\n @consume({ context: panZoomTransformContext, subscribe: true })\n panZoomTransformFromContext?: PanZoomTransform;\n\n /**\n * Selection context as fallback for when overlay is outside context providers (e.g., sibling of pan-zoom).\n */\n @property({ type: Object })\n selection?: SelectionContext;\n\n /**\n * Pan/zoom transform as fallback for when overlay is outside context providers (e.g., sibling of pan-zoom).\n */\n @property({ type: Object })\n panZoomTransform?: PanZoomTransform;\n\n @state()\n private canvasElement: HTMLElement | null = null;\n\n /**\n * Canvas element property - can be set directly when overlay is outside context providers.\n */\n @property({ type: Object })\n canvas?: HTMLElement;\n\n /**\n * Complete overlay state - calculated from targets using the abstraction layer.\n * This is the SINGLE source of truth for overlay bounds.\n */\n @state()\n private overlayState: OverlayState = {\n selection: null,\n boxSelect: null,\n highlight: null,\n };\n\n @state()\n private lastSelectionMode: string | null = null;\n\n private animationFrame?: number;\n private rafLoopActive = false;\n\n connectedCallback(): void {\n super.connectedCallback();\n // Apply styles directly since :host doesn't work in light DOM\n // These styles are critical for proper positioning relative to viewport\n this.style.position = \"fixed\";\n this.style.top = \"0\";\n this.style.left = \"0\";\n this.style.width = \"100vw\";\n this.style.height = \"100vh\";\n this.style.pointerEvents = \"none\";\n this.style.zIndex = \"1000\";\n // Add a data attribute for easier debugging\n this.setAttribute(\"data-selection-overlay\", \"true\");\n // Use requestAnimationFrame to ensure DOM is ready\n requestAnimationFrame(() => {\n // Use canvas property if provided, otherwise try to find it\n if (this.canvas) {\n this.canvasElement = this.canvas;\n } else {\n this.findCanvasElement();\n }\n // Always start RAF loop if we have a canvas element (needed for highlight updates)\n if (this.canvasElement) {\n this.startRafLoop();\n }\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.stopRafLoop();\n }\n\n /**\n * React to selection context changes to ensure box selection visual updates.\n * This is called whenever Lit detects a property change, including context updates.\n * Note: We don't call requestUpdate() here to avoid the Lit warning about scheduling\n * updates after an update completes. The RAF loop handles all updates.\n */\n updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated?.(changedProperties);\n // Check if selection mode changed (context updates might not show in changedProperties)\n const selection = this.effectiveSelection;\n const currentMode = selection?.selectionMode ?? null;\n if (currentMode !== this.lastSelectionMode) {\n this.lastSelectionMode = currentMode;\n // Don't call updateOverlayData() here - let the RAF loop handle it\n // This avoids scheduling updates during the update cycle\n }\n // Ensure RAF loop is running when box selecting (in case it stopped)\n if (currentMode === \"box-selecting\" && !this.rafLoopActive) {\n this.startRafLoop();\n }\n // Ensure RAF loop is running when canvas property is set (for highlight updates)\n if (changedProperties.has(\"canvas\") && this.canvas) {\n this.canvasElement = this.canvas;\n if (!this.rafLoopActive) {\n this.startRafLoop();\n }\n }\n // Ensure RAF loop is always running when we have a canvas (needed for highlight updates)\n if (this.canvasElement && !this.rafLoopActive) {\n this.startRafLoop();\n }\n }\n\n /**\n * Find the EFCanvas element.\n * Handles both cases:\n * 1. Overlay is inside EFCanvas's shadow DOM (old case)\n * 2. Overlay is a sibling of ef-pan-zoom (new case - outside transform)\n */\n private findCanvasElement(): void {\n // First, try to find ef-canvas as a sibling or descendant of ef-pan-zoom\n // (when overlay is outside the transform)\n // Since overlay is a sibling of ef-pan-zoom, we need to search in the parent\n const parent = this.parentElement;\n if (parent) {\n // Look for ef-pan-zoom sibling\n const panZoom = parent.querySelector(\"ef-pan-zoom\") as HTMLElement | null;\n if (panZoom) {\n // Look for ef-canvas inside ef-pan-zoom\n const canvas = panZoom.querySelector(\"ef-canvas\") as HTMLElement | null;\n if (canvas) {\n this.canvasElement = canvas;\n return;\n }\n }\n }\n\n // Also try closest in case overlay is inside pan-zoom somehow\n const panZoom = this.closest(\"ef-pan-zoom\") as HTMLElement | null;\n if (panZoom) {\n const canvas = panZoom.querySelector(\"ef-canvas\") as HTMLElement | null;\n if (canvas) {\n this.canvasElement = canvas;\n return;\n }\n }\n\n // Fallback: traverse up the DOM tree (for when overlay is inside canvas shadow DOM)\n let current: Node | null = this;\n while (current) {\n if (current instanceof ShadowRoot) {\n current = (current as ShadowRoot).host;\n } else if (current instanceof HTMLElement) {\n // Check if this is the EFCanvas element (case-insensitive check)\n if (\n current.tagName === \"EF-CANVAS\" ||\n current.tagName.toLowerCase() === \"ef-canvas\"\n ) {\n this.canvasElement = current;\n return;\n }\n // Check parent element or shadow root host\n const rootNode = current.getRootNode();\n if (rootNode instanceof ShadowRoot) {\n current = rootNode.host;\n } else {\n current = current.parentElement;\n }\n } else {\n const rootNode = (current as Node).getRootNode();\n if (rootNode instanceof ShadowRoot) {\n current = rootNode.host;\n } else {\n current = (current as Node).parentElement;\n }\n }\n }\n }\n\n /**\n * Start continuous RAF loop for smooth overlay updates.\n */\n private startRafLoop(): void {\n if (this.rafLoopActive) {\n return;\n }\n this.rafLoopActive = true;\n this.rafLoop();\n }\n\n /**\n * Stop RAF loop.\n */\n private stopRafLoop(): void {\n this.rafLoopActive = false;\n if (this.animationFrame) {\n cancelAnimationFrame(this.animationFrame);\n this.animationFrame = undefined;\n }\n }\n\n /**\n * Continuous RAF loop to update overlays every frame using Lit render cycle.\n */\n private rafLoop = (): void => {\n if (!this.rafLoopActive) {\n return;\n }\n\n // Update overlay data and trigger Lit render\n this.updateOverlayData();\n\n // Schedule next frame\n this.animationFrame = requestAnimationFrame(this.rafLoop);\n };\n\n /**\n * Get the effective selection context (from context or property).\n */\n private get effectiveSelection(): SelectionContext | undefined {\n return this.selectionFromContext ?? this.selection;\n }\n\n /**\n * Get the effective pan-zoom transform (from context or property).\n */\n private get effectivePanZoomTransform(): PanZoomTransform | undefined {\n return this.panZoomTransformFromContext ?? this.panZoomTransform;\n }\n\n /**\n * Update overlay data state using the abstraction layer.\n * \n * This method now uses the clean separation of:\n * - SEMANTICS: getOverlayTargets() determines WHAT should be shown\n * - MECHANISM: calculateOverlayState() determines HOW to show it\n */\n private updateOverlayData(): void {\n // Ensure canvas element reference is up-to-date\n if (this.canvas && this.canvas !== this.canvasElement) {\n this.canvasElement = this.canvas;\n }\n\n // Get canvas element - required for all overlay calculations\n const effectiveCanvas = this.canvasElement || this.canvas;\n if (!effectiveCanvas) {\n this.overlayState = { selection: null, boxSelect: null, highlight: null };\n return;\n }\n\n // Get canvas rect (try .canvas-content first for accurate positioning)\n let canvasRect = effectiveCanvas.getBoundingClientRect();\n if (effectiveCanvas.shadowRoot) {\n const canvasContent = effectiveCanvas.shadowRoot.querySelector(\n \".canvas-content\",\n ) as HTMLElement;\n if (canvasContent) {\n canvasRect = canvasContent.getBoundingClientRect();\n }\n }\n\n // Get pan-zoom element for box-select coordinate conversion\n const panZoomElement = effectiveCanvas.closest(\"ef-pan-zoom\") as HTMLElement | null;\n\n // Get highlighted element from canvas\n const canvas = effectiveCanvas as any;\n const highlightedElement = canvas?.highlightedElement as HTMLElement | null;\n\n // SEMANTICS: What should be shown?\n const targets = getOverlayTargets(this.effectiveSelection, highlightedElement);\n\n // Adapt canvas to CanvasWithMetadata interface\n const canvasWithMetadata: CanvasWithMetadata = {\n getElementData: (id: string) => canvas?.getElementData?.(id),\n getElement: (id: string) => canvas?.elementRegistry?.get(id),\n querySelector: (selector: string) => effectiveCanvas.querySelector(selector),\n shadowRoot: effectiveCanvas.shadowRoot,\n };\n\n // Read current transform directly from panzoom element (not stale property/context)\n // This ensures we always have the current scale/pan values\n const currentTransform = this.readCurrentTransform(panZoomElement);\n\n // MECHANISM: Calculate screen bounds\n this.overlayState = calculateOverlayState(\n targets,\n canvasWithMetadata,\n canvasRect,\n panZoomElement,\n currentTransform,\n );\n }\n\n /**\n * Read current transform directly from panzoom element.\n * This ensures we always have fresh values instead of stale property/context.\n */\n private readCurrentTransform(panZoomElement: HTMLElement | null): PanZoomTransform | undefined {\n // Try reading from panzoom element directly (most accurate)\n if (panZoomElement) {\n const pz = panZoomElement as any;\n if (typeof pz.x === \"number\" && typeof pz.y === \"number\" && typeof pz.scale === \"number\") {\n return { x: pz.x, y: pz.y, scale: pz.scale };\n }\n }\n\n // Fall back to context/property\n return this.effectivePanZoomTransform;\n }\n\n render() {\n // We only need canvasElement to render overlays\n const effectiveCanvas = this.canvasElement || this.canvas;\n if (!effectiveCanvas) {\n return html``;\n }\n\n // NOTE: Selection visualization is handled by EFTransformHandles (with rotation support).\n // This overlay only renders:\n // - box-select: marquee during drag-to-select\n // - highlight-box: hover indication for non-selected elements\n const { boxSelect, highlight } = this.overlayState;\n const selectionMode = this.effectiveSelection?.selectionMode;\n\n return html`\n ${\n boxSelect\n ? html`\n <div\n class=\"box-select\"\n style=\"left: ${boxSelect.x}px; top: ${boxSelect.y}px; width: ${boxSelect.width}px; height: ${boxSelect.height}px; position: absolute; border: 2px dashed rgb(59, 130, 246); background: rgba(59, 130, 246, 0.05); pointer-events: none;\"\n ></div>\n `\n : html``\n }\n ${\n highlight\n ? html`\n <div\n class=\"highlight-box\"\n style=\"left: ${highlight.x}px; top: ${highlight.y}px; width: ${highlight.width}px; height: ${highlight.height}px; position: absolute; border: 2px solid rgb(148, 163, 184); background: rgba(148, 163, 184, 0.1); pointer-events: none; box-shadow: 0 0 0 2px rgba(148, 163, 184, 0.3);\"\n ></div>\n `\n : html``\n }\n ${\n selectionMode === \"box-selecting\" && !boxSelect\n ? html`\n <div style=\"position: fixed; top: 50px; right: 10px; background: orange; color: white; padding: 4px; z-index: 10000; font-size: 12px;\">\n Box selecting but no bounds! mode=${selectionMode} bounds=${this.effectiveSelection?.boxSelectBounds ? \"exists\" : \"null\"}\n </div>\n `\n : html``\n }\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-canvas-selection-overlay\": SelectionOverlay;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SelectionOverlay.js","names":["SelectionOverlay","panZoom","current: Node | null","canvasWithMetadata: CanvasWithMetadata"],"sources":["../../../src/canvas/overlays/SelectionOverlay.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport {\n selectionContext,\n type SelectionContext,\n} from \"../selection/selectionContext.js\";\nimport { panZoomTransformContext } from \"../../gui/panZoomTransformContext.js\";\nimport type { PanZoomTransform } from \"../../elements/EFPanZoom.js\";\nimport {\n type OverlayState,\n type CanvasWithMetadata,\n getOverlayTargets,\n calculateOverlayState,\n} from \"./overlayState.js\";\n\n/**\n * Selection overlay that renders unscaled selection indicators.\n * Uses fixed positioning to ensure 1:1 pixel ratio regardless of zoom level.\n */\n@customElement(\"ef-canvas-selection-overlay\")\nexport class SelectionOverlay extends LitElement {\n static styles = [\n css`\n :host {\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n pointer-events: none;\n z-index: 1000;\n }\n .box-select {\n position: absolute;\n border: 2px dashed rgb(59, 130, 246);\n background: rgba(59, 130, 246, 0.05);\n pointer-events: none;\n }\n .highlight-box {\n position: absolute;\n border: 2px solid rgb(148, 163, 184);\n background: rgba(148, 163, 184, 0.1);\n pointer-events: none;\n box-shadow: 0 0 0 2px rgba(148, 163, 184, 0.3);\n }\n `,\n ];\n\n createRenderRoot() {\n // Return this to render directly to the element (no shadow DOM)\n // This allows the overlay to use fixed positioning relative to viewport\n // Lit will inject styles as a <style> element when createRenderRoot returns this\n return this;\n }\n\n firstUpdated(\n changedProperties: Map<string | number | symbol, unknown>,\n ): void {\n super.firstUpdated?.(changedProperties);\n }\n\n @consume({ context: selectionContext, subscribe: true })\n selectionFromContext?: SelectionContext;\n\n @consume({ context: panZoomTransformContext, subscribe: true })\n panZoomTransformFromContext?: PanZoomTransform;\n\n /**\n * Selection context as fallback for when overlay is outside context providers (e.g., sibling of pan-zoom).\n */\n @property({ type: Object })\n selection?: SelectionContext;\n\n /**\n * Pan/zoom transform as fallback for when overlay is outside context providers (e.g., sibling of pan-zoom).\n */\n @property({ type: Object })\n panZoomTransform?: PanZoomTransform;\n\n @state()\n private canvasElement: HTMLElement | null = null;\n\n /**\n * Canvas element property - can be set directly when overlay is outside context providers.\n */\n @property({ type: Object })\n canvas?: HTMLElement;\n\n /**\n * Complete overlay state - calculated from targets using the abstraction layer.\n * This is the SINGLE source of truth for overlay bounds.\n */\n @state()\n private overlayState: OverlayState = {\n selection: null,\n boxSelect: null,\n highlight: null,\n };\n\n @state()\n private lastSelectionMode: string | null = null;\n\n /**\n * When true, the RAF loop skips all work. Used during playback to avoid\n * layout-thrashing getBoundingClientRect/getComputedStyle calls that\n * compete with the canvas render pipeline.\n */\n @property({ type: Boolean }) paused = false;\n\n private animationFrame?: number;\n private rafLoopActive = false;\n\n connectedCallback(): void {\n super.connectedCallback();\n // Apply styles directly since :host doesn't work in light DOM\n // These styles are critical for proper positioning relative to viewport\n this.style.position = \"fixed\";\n this.style.top = \"0\";\n this.style.left = \"0\";\n this.style.width = \"100vw\";\n this.style.height = \"100vh\";\n this.style.pointerEvents = \"none\";\n this.style.zIndex = \"1000\";\n // Add a data attribute for easier debugging\n this.setAttribute(\"data-selection-overlay\", \"true\");\n // Use requestAnimationFrame to ensure DOM is ready\n requestAnimationFrame(() => {\n // Use canvas property if provided, otherwise try to find it\n if (this.canvas) {\n this.canvasElement = this.canvas;\n } else {\n this.findCanvasElement();\n }\n // Always start RAF loop if we have a canvas element (needed for highlight updates)\n if (this.canvasElement) {\n this.startRafLoop();\n }\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.stopRafLoop();\n }\n\n /**\n * React to selection context changes to ensure box selection visual updates.\n * This is called whenever Lit detects a property change, including context updates.\n * Note: We don't call requestUpdate() here to avoid the Lit warning about scheduling\n * updates after an update completes. The RAF loop handles all updates.\n */\n updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated?.(changedProperties);\n // Check if selection mode changed (context updates might not show in changedProperties)\n const selection = this.effectiveSelection;\n const currentMode = selection?.selectionMode ?? null;\n if (currentMode !== this.lastSelectionMode) {\n this.lastSelectionMode = currentMode;\n }\n // Ensure RAF loop is running when box selecting (in case it stopped)\n if (currentMode === \"box-selecting\" && !this.rafLoopActive) {\n this.startRafLoop();\n }\n // Ensure RAF loop is running when canvas property is set (for highlight updates)\n if (changedProperties.has(\"canvas\") && this.canvas) {\n this.canvasElement = this.canvas;\n if (!this.rafLoopActive) {\n this.startRafLoop();\n }\n }\n // Start RAF loop if we have a canvas but loop isn't running\n if (this.canvasElement && !this.rafLoopActive) {\n this.startRafLoop();\n }\n // On unpause, force an immediate overlay update to sync stale state\n if (changedProperties.has(\"paused\") && !this.paused) {\n this.updateOverlayData();\n }\n }\n\n /**\n * Find the EFCanvas element.\n * Handles both cases:\n * 1. Overlay is inside EFCanvas's shadow DOM (old case)\n * 2. Overlay is a sibling of ef-pan-zoom (new case - outside transform)\n */\n private findCanvasElement(): void {\n // First, try to find ef-canvas as a sibling or descendant of ef-pan-zoom\n // (when overlay is outside the transform)\n // Since overlay is a sibling of ef-pan-zoom, we need to search in the parent\n const parent = this.parentElement;\n if (parent) {\n // Look for ef-pan-zoom sibling\n const panZoom = parent.querySelector(\"ef-pan-zoom\") as HTMLElement | null;\n if (panZoom) {\n // Look for ef-canvas inside ef-pan-zoom\n const canvas = panZoom.querySelector(\"ef-canvas\") as HTMLElement | null;\n if (canvas) {\n this.canvasElement = canvas;\n return;\n }\n }\n }\n\n // Also try closest in case overlay is inside pan-zoom somehow\n const panZoom = this.closest(\"ef-pan-zoom\") as HTMLElement | null;\n if (panZoom) {\n const canvas = panZoom.querySelector(\"ef-canvas\") as HTMLElement | null;\n if (canvas) {\n this.canvasElement = canvas;\n return;\n }\n }\n\n // Fallback: traverse up the DOM tree (for when overlay is inside canvas shadow DOM)\n let current: Node | null = this;\n while (current) {\n if (current instanceof ShadowRoot) {\n current = (current as ShadowRoot).host;\n } else if (current instanceof HTMLElement) {\n // Check if this is the EFCanvas element (case-insensitive check)\n if (\n current.tagName === \"EF-CANVAS\" ||\n current.tagName.toLowerCase() === \"ef-canvas\"\n ) {\n this.canvasElement = current;\n return;\n }\n // Check parent element or shadow root host\n const rootNode = current.getRootNode();\n if (rootNode instanceof ShadowRoot) {\n current = rootNode.host;\n } else {\n current = current.parentElement;\n }\n } else {\n const rootNode = (current as Node).getRootNode();\n if (rootNode instanceof ShadowRoot) {\n current = rootNode.host;\n } else {\n current = (current as Node).parentElement;\n }\n }\n }\n }\n\n /**\n * Start continuous RAF loop for smooth overlay updates.\n */\n private startRafLoop(): void {\n if (this.rafLoopActive) {\n return;\n }\n this.rafLoopActive = true;\n this.rafLoop();\n }\n\n /**\n * Stop RAF loop.\n */\n private stopRafLoop(): void {\n this.rafLoopActive = false;\n if (this.animationFrame) {\n cancelAnimationFrame(this.animationFrame);\n this.animationFrame = undefined;\n }\n }\n\n /**\n * Continuous RAF loop to update overlays every frame using Lit render cycle.\n * When paused, the loop keeps running (for quick resume) but skips all\n * expensive layout queries.\n */\n private rafLoop = (): void => {\n if (!this.rafLoopActive) {\n return;\n }\n\n // Skip all work when paused to avoid layout-thrashing during playback\n if (!this.paused) {\n this.updateOverlayData();\n }\n\n // Schedule next frame\n this.animationFrame = requestAnimationFrame(this.rafLoop);\n };\n\n /**\n * Get the effective selection context (from context or property).\n */\n private get effectiveSelection(): SelectionContext | undefined {\n return this.selectionFromContext ?? this.selection;\n }\n\n /**\n * Get the effective pan-zoom transform (from context or property).\n */\n private get effectivePanZoomTransform(): PanZoomTransform | undefined {\n return this.panZoomTransformFromContext ?? this.panZoomTransform;\n }\n\n /**\n * Update overlay data state using the abstraction layer.\n *\n * This method now uses the clean separation of:\n * - SEMANTICS: getOverlayTargets() determines WHAT should be shown\n * - MECHANISM: calculateOverlayState() determines HOW to show it\n */\n private updateOverlayData(): void {\n // Ensure canvas element reference is up-to-date\n if (this.canvas && this.canvas !== this.canvasElement) {\n this.canvasElement = this.canvas;\n }\n\n // Get canvas element - required for all overlay calculations\n const effectiveCanvas = this.canvasElement || this.canvas;\n if (!effectiveCanvas) {\n this.overlayState = { selection: null, boxSelect: null, highlight: null };\n return;\n }\n\n // Get canvas rect (try .canvas-content first for accurate positioning)\n let canvasRect = effectiveCanvas.getBoundingClientRect();\n if (effectiveCanvas.shadowRoot) {\n const canvasContent = effectiveCanvas.shadowRoot.querySelector(\n \".canvas-content\",\n ) as HTMLElement;\n if (canvasContent) {\n canvasRect = canvasContent.getBoundingClientRect();\n }\n }\n\n // Get pan-zoom element for box-select coordinate conversion\n const panZoomElement = effectiveCanvas.closest(\n \"ef-pan-zoom\",\n ) as HTMLElement | null;\n\n // Get highlighted element from canvas\n const canvas = effectiveCanvas as any;\n const highlightedElement = canvas?.highlightedElement as HTMLElement | null;\n\n // SEMANTICS: What should be shown?\n const targets = getOverlayTargets(\n this.effectiveSelection,\n highlightedElement,\n );\n\n // Adapt canvas to CanvasWithMetadata interface\n const canvasWithMetadata: CanvasWithMetadata = {\n getElementData: (id: string) => canvas?.getElementData?.(id),\n getElement: (id: string) => canvas?.elementRegistry?.get(id),\n querySelector: (selector: string) =>\n effectiveCanvas.querySelector(selector),\n shadowRoot: effectiveCanvas.shadowRoot,\n };\n\n // Read current transform directly from panzoom element (not stale property/context)\n // This ensures we always have the current scale/pan values\n const currentTransform = this.readCurrentTransform(panZoomElement);\n\n // MECHANISM: Calculate screen bounds\n this.overlayState = calculateOverlayState(\n targets,\n canvasWithMetadata,\n canvasRect,\n panZoomElement,\n currentTransform,\n );\n }\n\n /**\n * Read current transform directly from panzoom element.\n * This ensures we always have fresh values instead of stale property/context.\n */\n private readCurrentTransform(\n panZoomElement: HTMLElement | null,\n ): PanZoomTransform | undefined {\n // Try reading from panzoom element directly (most accurate)\n if (panZoomElement) {\n const pz = panZoomElement as any;\n if (\n typeof pz.x === \"number\" &&\n typeof pz.y === \"number\" &&\n typeof pz.scale === \"number\"\n ) {\n return { x: pz.x, y: pz.y, scale: pz.scale };\n }\n }\n\n // Fall back to context/property\n return this.effectivePanZoomTransform;\n }\n\n render() {\n // We only need canvasElement to render overlays\n const effectiveCanvas = this.canvasElement || this.canvas;\n if (!effectiveCanvas) {\n return html``;\n }\n\n // NOTE: Selection visualization is handled by EFTransformHandles (with rotation support).\n // This overlay only renders:\n // - box-select: marquee during drag-to-select\n // - highlight-box: hover indication for non-selected elements\n const { boxSelect, highlight } = this.overlayState;\n\n return html`\n ${\n boxSelect\n ? html`\n <div\n class=\"box-select\"\n style=\"left: ${boxSelect.x}px; top: ${boxSelect.y}px; width: ${boxSelect.width}px; height: ${boxSelect.height}px; position: absolute; border: 2px dashed rgb(59, 130, 246); background: rgba(59, 130, 246, 0.05); pointer-events: none;\"\n ></div>\n `\n : html``\n }\n ${\n highlight\n ? html`\n <div\n class=\"highlight-box\"\n style=\"left: ${highlight.x}px; top: ${highlight.y}px; width: ${highlight.width}px; height: ${highlight.height}px; position: absolute; border: 2px solid rgb(148, 163, 184); background: rgba(148, 163, 184, 0.1); pointer-events: none; box-shadow: 0 0 0 2px rgba(148, 163, 184, 0.3);\"\n ></div>\n `\n : html``\n }\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-canvas-selection-overlay\": SelectionOverlay;\n }\n}\n"],"mappings":";;;;;;;;;AAqBO,6BAAMA,2BAAyB,WAAW;;;uBA4DH;sBAaP;GACnC,WAAW;GACX,WAAW;GACX,WAAW;GACZ;2BAG0C;gBAOL;uBAGd;uBAmKM;AAC5B,OAAI,CAAC,KAAK,cACR;AAIF,OAAI,CAAC,KAAK,OACR,MAAK,mBAAmB;AAI1B,QAAK,iBAAiB,sBAAsB,KAAK,QAAQ;;;;gBAvQ3C,CACd,GAAG;;;;;;;;;;;;;;;;;;;;;;;MAwBJ;;CAED,mBAAmB;AAIjB,SAAO;;CAGT,aACE,mBACM;AACN,QAAM,eAAe,kBAAkB;;CAsDzC,oBAA0B;AACxB,QAAM,mBAAmB;AAGzB,OAAK,MAAM,WAAW;AACtB,OAAK,MAAM,MAAM;AACjB,OAAK,MAAM,OAAO;AAClB,OAAK,MAAM,QAAQ;AACnB,OAAK,MAAM,SAAS;AACpB,OAAK,MAAM,gBAAgB;AAC3B,OAAK,MAAM,SAAS;AAEpB,OAAK,aAAa,0BAA0B,OAAO;AAEnD,8BAA4B;AAE1B,OAAI,KAAK,OACP,MAAK,gBAAgB,KAAK;OAE1B,MAAK,mBAAmB;AAG1B,OAAI,KAAK,cACP,MAAK,cAAc;IAErB;;CAGJ,uBAA6B;AAC3B,QAAM,sBAAsB;AAC5B,OAAK,aAAa;;;;;;;;CASpB,QAAQ,mBAAiE;AACvE,QAAM,UAAU,kBAAkB;EAGlC,MAAM,cADY,KAAK,oBACQ,iBAAiB;AAChD,MAAI,gBAAgB,KAAK,kBACvB,MAAK,oBAAoB;AAG3B,MAAI,gBAAgB,mBAAmB,CAAC,KAAK,cAC3C,MAAK,cAAc;AAGrB,MAAI,kBAAkB,IAAI,SAAS,IAAI,KAAK,QAAQ;AAClD,QAAK,gBAAgB,KAAK;AAC1B,OAAI,CAAC,KAAK,cACR,MAAK,cAAc;;AAIvB,MAAI,KAAK,iBAAiB,CAAC,KAAK,cAC9B,MAAK,cAAc;AAGrB,MAAI,kBAAkB,IAAI,SAAS,IAAI,CAAC,KAAK,OAC3C,MAAK,mBAAmB;;;;;;;;CAU5B,AAAQ,oBAA0B;EAIhC,MAAM,SAAS,KAAK;AACpB,MAAI,QAAQ;GAEV,MAAMC,YAAU,OAAO,cAAc,cAAc;AACnD,OAAIA,WAAS;IAEX,MAAM,SAASA,UAAQ,cAAc,YAAY;AACjD,QAAI,QAAQ;AACV,UAAK,gBAAgB;AACrB;;;;EAMN,MAAM,UAAU,KAAK,QAAQ,cAAc;AAC3C,MAAI,SAAS;GACX,MAAM,SAAS,QAAQ,cAAc,YAAY;AACjD,OAAI,QAAQ;AACV,SAAK,gBAAgB;AACrB;;;EAKJ,IAAIC,UAAuB;AAC3B,SAAO,QACL,KAAI,mBAAmB,WACrB,WAAW,QAAuB;WACzB,mBAAmB,aAAa;AAEzC,OACE,QAAQ,YAAY,eACpB,QAAQ,QAAQ,aAAa,KAAK,aAClC;AACA,SAAK,gBAAgB;AACrB;;GAGF,MAAM,WAAW,QAAQ,aAAa;AACtC,OAAI,oBAAoB,WACtB,WAAU,SAAS;OAEnB,WAAU,QAAQ;SAEf;GACL,MAAM,WAAY,QAAiB,aAAa;AAChD,OAAI,oBAAoB,WACtB,WAAU,SAAS;OAEnB,WAAW,QAAiB;;;;;;CASpC,AAAQ,eAAqB;AAC3B,MAAI,KAAK,cACP;AAEF,OAAK,gBAAgB;AACrB,OAAK,SAAS;;;;;CAMhB,AAAQ,cAAoB;AAC1B,OAAK,gBAAgB;AACrB,MAAI,KAAK,gBAAgB;AACvB,wBAAqB,KAAK,eAAe;AACzC,QAAK,iBAAiB;;;;;;CA0B1B,IAAY,qBAAmD;AAC7D,SAAO,KAAK,wBAAwB,KAAK;;;;;CAM3C,IAAY,4BAA0D;AACpE,SAAO,KAAK,+BAA+B,KAAK;;;;;;;;;CAUlD,AAAQ,oBAA0B;AAEhC,MAAI,KAAK,UAAU,KAAK,WAAW,KAAK,cACtC,MAAK,gBAAgB,KAAK;EAI5B,MAAM,kBAAkB,KAAK,iBAAiB,KAAK;AACnD,MAAI,CAAC,iBAAiB;AACpB,QAAK,eAAe;IAAE,WAAW;IAAM,WAAW;IAAM,WAAW;IAAM;AACzE;;EAIF,IAAI,aAAa,gBAAgB,uBAAuB;AACxD,MAAI,gBAAgB,YAAY;GAC9B,MAAM,gBAAgB,gBAAgB,WAAW,cAC/C,kBACD;AACD,OAAI,cACF,cAAa,cAAc,uBAAuB;;EAKtD,MAAM,iBAAiB,gBAAgB,QACrC,cACD;EAGD,MAAM,SAAS;EACf,MAAM,qBAAqB,QAAQ;EAGnC,MAAM,UAAU,kBACd,KAAK,oBACL,mBACD;EAGD,MAAMC,qBAAyC;GAC7C,iBAAiB,OAAe,QAAQ,iBAAiB,GAAG;GAC5D,aAAa,OAAe,QAAQ,iBAAiB,IAAI,GAAG;GAC5D,gBAAgB,aACd,gBAAgB,cAAc,SAAS;GACzC,YAAY,gBAAgB;GAC7B;EAID,MAAM,mBAAmB,KAAK,qBAAqB,eAAe;AAGlE,OAAK,eAAe,sBAClB,SACA,oBACA,YACA,gBACA,iBACD;;;;;;CAOH,AAAQ,qBACN,gBAC8B;AAE9B,MAAI,gBAAgB;GAClB,MAAM,KAAK;AACX,OACE,OAAO,GAAG,MAAM,YAChB,OAAO,GAAG,MAAM,YAChB,OAAO,GAAG,UAAU,SAEpB,QAAO;IAAE,GAAG,GAAG;IAAG,GAAG,GAAG;IAAG,OAAO,GAAG;IAAO;;AAKhD,SAAO,KAAK;;CAGd,SAAS;AAGP,MAAI,EADoB,KAAK,iBAAiB,KAAK,QAEjD,QAAO,IAAI;EAOb,MAAM,EAAE,WAAW,cAAc,KAAK;AAEtC,SAAO,IAAI;QAEP,YACI,IAAI;;;6BAGa,UAAU,EAAE,WAAW,UAAU,EAAE,aAAa,UAAU,MAAM,cAAc,UAAU,OAAO;;cAGhH,IAAI,GACT;QAEC,YACI,IAAI;;;6BAGa,UAAU,EAAE,WAAW,UAAU,EAAE,aAAa,UAAU,MAAM,cAAc,UAAU,OAAO;;cAGhH,IAAI,GACT;;;;YA7WJ,QAAQ;CAAE,SAAS;CAAkB,WAAW;CAAM,CAAC;YAGvD,QAAQ;CAAE,SAAS;CAAyB,WAAW;CAAM,CAAC;YAM9D,SAAS,EAAE,MAAM,QAAQ,CAAC;YAM1B,SAAS,EAAE,MAAM,QAAQ,CAAC;YAG1B,OAAO;YAMP,SAAS,EAAE,MAAM,QAAQ,CAAC;YAO1B,OAAO;YAOP,OAAO;YAQP,SAAS,EAAE,MAAM,SAAS,CAAC;+BAxF7B,cAAc,8BAA8B"}
@@ -1 +1 @@
1
- {"version":3,"file":"overlayState.js","names":[],"sources":["../../../src/canvas/overlays/overlayState.ts"],"sourcesContent":["/**\n * Overlay State Management\n *\n * This module provides a clean separation between:\n * - SEMANTICS: What should be shown? (OverlayTargets)\n * - MECHANISM: How to calculate screen bounds (calculateOverlayState)\n *\n * INVARIANTS:\n * 1. Overlay is visible iff bounds are non-null with positive dimensions\n * 2. All bounds are in screen coordinates (relative to viewport origin)\n * 3. Only one element can be highlighted at a time\n *\n * COORDINATE SPACES:\n * - Canvas space: Logical coordinates, unaffected by zoom (used for element positioning)\n * - Screen space: Viewport coordinates (used for overlay positioning)\n */\n\nimport type { PanZoomTransform } from \"../../elements/EFPanZoom.js\";\nimport type { SelectionContext } from \"../selection/selectionContext.js\";\nimport { getElementBounds } from \"../getElementBounds.js\";\n\n// ============================================================================\n// TYPES: Core Concepts\n// ============================================================================\n\n/**\n * Screen-space bounding box for overlay positioning.\n * All values are in viewport coordinates (pixels from viewport origin).\n */\nexport interface ScreenBounds {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Canvas-space bounding box (from selection context).\n */\nexport interface CanvasBounds {\n left: number;\n top: number;\n right: number;\n bottom: number;\n}\n\n/**\n * The complete state of all overlays.\n * Each property is either a ScreenBounds or null.\n * An overlay is visible iff its bounds are non-null.\n */\nexport interface OverlayState {\n selection: ScreenBounds | null;\n boxSelect: ScreenBounds | null;\n highlight: ScreenBounds | null;\n}\n\n/**\n * SEMANTICS: What elements should have overlays?\n * This represents the \"what\" without the \"how\".\n */\nexport interface OverlayTargets {\n selectedIds: Set<string>;\n boxSelectBounds: CanvasBounds | null;\n highlightedElementId: string | null;\n}\n\n/**\n * Interface for canvas element data (metadata).\n */\nexport interface ElementMetadata {\n id: string;\n x: number;\n y: number;\n width: number;\n height: number;\n rotation?: number;\n}\n\n/**\n * Interface for canvas that provides element data.\n */\nexport interface CanvasWithMetadata {\n getElementData(elementId: string): ElementMetadata | undefined;\n getElement?(elementId: string): HTMLElement | undefined;\n querySelector(selector: string): HTMLElement | null;\n shadowRoot: ShadowRoot | null;\n}\n\n// ============================================================================\n// SEMANTICS: Extract what should be shown\n// ============================================================================\n\n/**\n * Extract overlay targets from selection context and highlighted element.\n * This is pure SEMANTICS - it determines WHAT should be shown.\n */\nexport function getOverlayTargets(\n selection: SelectionContext | undefined,\n highlightedElement: HTMLElement | null,\n): OverlayTargets {\n return {\n selectedIds: selection?.selectedIds ? new Set(selection.selectedIds) : new Set(),\n boxSelectBounds: selection?.boxSelectBounds ?? null,\n highlightedElementId:\n highlightedElement?.getAttribute(\"data-element-id\") ??\n highlightedElement?.id ??\n null,\n };\n}\n\n// ============================================================================\n// MECHANISM: Calculate screen bounds\n// ============================================================================\n\n/**\n * Calculate screen bounds for a single element.\n * This is the SINGLE SOURCE OF TRUTH for element → screen bounds conversion.\n *\n * Strategy:\n * 1. Try to use metadata (already in canvas coordinates)\n * 2. Fall back to DOM measurement if metadata unavailable\n *\n * @param elementId - The element's ID (data-element-id or id attribute)\n * @param canvas - Canvas element with getElementData method\n * @param canvasRect - Canvas content element's bounding rect\n * @param scale - Current zoom scale\n * @returns Screen bounds or null if element not found\n */\nexport function calculateElementScreenBounds(\n elementId: string,\n canvas: CanvasWithMetadata,\n canvasRect: DOMRect,\n scale: number,\n): ScreenBounds | null {\n const metadata = canvas.getElementData(elementId);\n\n if (metadata && metadata.width > 0 && metadata.height > 0) {\n // Use metadata (already in canvas coordinates)\n return {\n x: canvasRect.left + metadata.x * scale,\n y: canvasRect.top + metadata.y * scale,\n width: metadata.width * scale,\n height: metadata.height * scale,\n };\n }\n\n // Fallback: find element and use DOM measurement\n const element = findElement(elementId, canvas);\n if (!element) return null;\n\n const bounds = getElementBounds(element);\n return {\n x: bounds.left,\n y: bounds.top,\n width: bounds.width,\n height: bounds.height,\n };\n}\n\n/**\n * Find an element by ID in the canvas (checks both light DOM and shadow DOM).\n */\nfunction findElement(\n elementId: string,\n canvas: CanvasWithMetadata,\n): HTMLElement | null {\n // Try canvas's getElement method first\n if (canvas.getElement) {\n const element = canvas.getElement(elementId);\n if (element) return element;\n }\n\n // Try shadow DOM\n if (canvas.shadowRoot) {\n const element = canvas.shadowRoot.querySelector(\n `[data-element-id=\"${elementId}\"]`,\n ) as HTMLElement | null;\n if (element) return element;\n }\n\n // Try light DOM\n return canvas.querySelector(\n `[data-element-id=\"${elementId}\"]`,\n ) as HTMLElement | null;\n}\n\n/**\n * Calculate the union of multiple bounds (bounding box that contains all).\n */\nexport function unionBounds(bounds: ScreenBounds[]): ScreenBounds | null {\n if (bounds.length === 0) return null;\n\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n\n for (const b of bounds) {\n minX = Math.min(minX, b.x);\n minY = Math.min(minY, b.y);\n maxX = Math.max(maxX, b.x + b.width);\n maxY = Math.max(maxY, b.y + b.height);\n }\n\n return {\n x: minX,\n y: minY,\n width: maxX - minX,\n height: maxY - minY,\n };\n}\n\n/**\n * Calculate selection bounds for all selected elements.\n */\nexport function calculateSelectionBounds(\n selectedIds: Set<string>,\n canvas: CanvasWithMetadata,\n canvasRect: DOMRect,\n scale: number,\n): ScreenBounds | null {\n if (selectedIds.size === 0) return null;\n\n const elementBounds = Array.from(selectedIds)\n .map((id) => calculateElementScreenBounds(id, canvas, canvasRect, scale))\n .filter((b): b is ScreenBounds => b !== null);\n\n return unionBounds(elementBounds);\n}\n\n/**\n * Calculate box-select bounds (convert canvas bounds to screen bounds).\n *\n * @param boxSelectBounds - Box selection bounds in canvas coordinates\n * @param panZoomElement - The pan-zoom element (for canvasToScreen conversion)\n * @param panZoomTransform - Current pan-zoom transform\n * @returns Screen bounds or null\n */\nexport function calculateBoxSelectBounds(\n boxSelectBounds: CanvasBounds | null,\n panZoomElement: HTMLElement | null,\n panZoomTransform: PanZoomTransform | undefined,\n): ScreenBounds | null {\n if (!boxSelectBounds || !panZoomElement || !panZoomTransform) {\n return null;\n }\n\n // Try to use EFPanZoom's canvasToScreen method if available\n const pz = panZoomElement as any;\n if (typeof pz.canvasToScreen === \"function\") {\n const topLeft = pz.canvasToScreen(boxSelectBounds.left, boxSelectBounds.top);\n const bottomRight = pz.canvasToScreen(\n boxSelectBounds.right,\n boxSelectBounds.bottom,\n );\n\n return {\n x: topLeft.x,\n y: topLeft.y,\n width: bottomRight.x - topLeft.x,\n height: bottomRight.y - topLeft.y,\n };\n }\n\n // Fallback: manual calculation\n const panZoomRect = panZoomElement.getBoundingClientRect();\n const { scale, x, y } = panZoomTransform;\n\n const screenLeft = panZoomRect.left + boxSelectBounds.left * scale + x;\n const screenTop = panZoomRect.top + boxSelectBounds.top * scale + y;\n const screenRight = panZoomRect.left + boxSelectBounds.right * scale + x;\n const screenBottom = panZoomRect.top + boxSelectBounds.bottom * scale + y;\n\n return {\n x: screenLeft,\n y: screenTop,\n width: screenRight - screenLeft,\n height: screenBottom - screenTop,\n };\n}\n\n/**\n * Calculate highlight bounds for the highlighted element.\n */\nexport function calculateHighlightBounds(\n highlightedElementId: string | null,\n canvas: CanvasWithMetadata,\n canvasRect: DOMRect,\n scale: number,\n): ScreenBounds | null {\n if (!highlightedElementId) return null;\n\n return calculateElementScreenBounds(\n highlightedElementId,\n canvas,\n canvasRect,\n scale,\n );\n}\n\n// ============================================================================\n// MAIN: Calculate complete overlay state\n// ============================================================================\n\n/**\n * Calculate the complete overlay state from targets.\n * This is the main entry point that combines semantics and mechanism.\n *\n * @param targets - What should be shown (from getOverlayTargets)\n * @param canvas - Canvas element with metadata\n * @param canvasRect - Canvas content bounding rect\n * @param panZoomElement - Pan-zoom element (for box-select conversion)\n * @param panZoomTransform - Current transform\n * @returns Complete overlay state\n */\nexport function calculateOverlayState(\n targets: OverlayTargets,\n canvas: CanvasWithMetadata,\n canvasRect: DOMRect,\n panZoomElement: HTMLElement | null,\n panZoomTransform: PanZoomTransform | undefined,\n): OverlayState {\n const scale = panZoomTransform?.scale ?? 1;\n\n return {\n selection: calculateSelectionBounds(\n targets.selectedIds,\n canvas,\n canvasRect,\n scale,\n ),\n boxSelect: calculateBoxSelectBounds(\n targets.boxSelectBounds,\n panZoomElement,\n panZoomTransform,\n ),\n highlight: calculateHighlightBounds(\n targets.highlightedElementId,\n canvas,\n canvasRect,\n scale,\n ),\n };\n}\n\n// ============================================================================\n// INVARIANTS: Helper functions to check invariants\n// ============================================================================\n\n/**\n * INVARIANT: Overlay is visible iff bounds exist with positive dimensions.\n */\nexport function isOverlayVisible(bounds: ScreenBounds | null): boolean {\n return bounds !== null && bounds.width > 0 && bounds.height > 0;\n}\n\n/**\n * Check if overlay state has any visible overlays.\n */\nexport function hasVisibleOverlays(state: OverlayState): boolean {\n return (\n isOverlayVisible(state.selection) ||\n isOverlayVisible(state.boxSelect) ||\n isOverlayVisible(state.highlight)\n );\n}\n\n"],"mappings":"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"}
1
+ {"version":3,"file":"overlayState.js","names":[],"sources":["../../../src/canvas/overlays/overlayState.ts"],"sourcesContent":["/**\n * Overlay State Management\n *\n * This module provides a clean separation between:\n * - SEMANTICS: What should be shown? (OverlayTargets)\n * - MECHANISM: How to calculate screen bounds (calculateOverlayState)\n *\n * INVARIANTS:\n * 1. Overlay is visible iff bounds are non-null with positive dimensions\n * 2. All bounds are in screen coordinates (relative to viewport origin)\n * 3. Only one element can be highlighted at a time\n *\n * COORDINATE SPACES:\n * - Canvas space: Logical coordinates, unaffected by zoom (used for element positioning)\n * - Screen space: Viewport coordinates (used for overlay positioning)\n */\n\nimport type { PanZoomTransform } from \"../../elements/EFPanZoom.js\";\nimport type { SelectionContext } from \"../selection/selectionContext.js\";\nimport { getElementBounds } from \"../getElementBounds.js\";\n\n// ============================================================================\n// TYPES: Core Concepts\n// ============================================================================\n\n/**\n * Screen-space bounding box for overlay positioning.\n * All values are in viewport coordinates (pixels from viewport origin).\n */\nexport interface ScreenBounds {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Canvas-space bounding box (from selection context).\n */\nexport interface CanvasBounds {\n left: number;\n top: number;\n right: number;\n bottom: number;\n}\n\n/**\n * The complete state of all overlays.\n * Each property is either a ScreenBounds or null.\n * An overlay is visible iff its bounds are non-null.\n */\nexport interface OverlayState {\n selection: ScreenBounds | null;\n boxSelect: ScreenBounds | null;\n highlight: ScreenBounds | null;\n}\n\n/**\n * SEMANTICS: What elements should have overlays?\n * This represents the \"what\" without the \"how\".\n */\nexport interface OverlayTargets {\n selectedIds: Set<string>;\n boxSelectBounds: CanvasBounds | null;\n highlightedElementId: string | null;\n}\n\n/**\n * Interface for canvas element data (metadata).\n */\nexport interface ElementMetadata {\n id: string;\n x: number;\n y: number;\n width: number;\n height: number;\n rotation?: number;\n}\n\n/**\n * Interface for canvas that provides element data.\n */\nexport interface CanvasWithMetadata {\n getElementData(elementId: string): ElementMetadata | undefined;\n getElement?(elementId: string): HTMLElement | undefined;\n querySelector(selector: string): HTMLElement | null;\n shadowRoot: ShadowRoot | null;\n}\n\n// ============================================================================\n// SEMANTICS: Extract what should be shown\n// ============================================================================\n\n/**\n * Extract overlay targets from selection context and highlighted element.\n * This is pure SEMANTICS - it determines WHAT should be shown.\n */\nexport function getOverlayTargets(\n selection: SelectionContext | undefined,\n highlightedElement: HTMLElement | null,\n): OverlayTargets {\n return {\n selectedIds: selection?.selectedIds\n ? new Set(selection.selectedIds)\n : new Set(),\n boxSelectBounds: selection?.boxSelectBounds ?? null,\n highlightedElementId:\n highlightedElement?.getAttribute(\"data-element-id\") ??\n highlightedElement?.id ??\n null,\n };\n}\n\n// ============================================================================\n// MECHANISM: Calculate screen bounds\n// ============================================================================\n\n/**\n * Calculate screen bounds for a single element.\n * This is the SINGLE SOURCE OF TRUTH for element → screen bounds conversion.\n *\n * Strategy:\n * 1. Try to use metadata (already in canvas coordinates)\n * 2. Fall back to DOM measurement if metadata unavailable\n *\n * @param elementId - The element's ID (data-element-id or id attribute)\n * @param canvas - Canvas element with getElementData method\n * @param canvasRect - Canvas content element's bounding rect\n * @param scale - Current zoom scale\n * @returns Screen bounds or null if element not found\n */\nexport function calculateElementScreenBounds(\n elementId: string,\n canvas: CanvasWithMetadata,\n canvasRect: DOMRect,\n scale: number,\n): ScreenBounds | null {\n const metadata = canvas.getElementData(elementId);\n\n if (metadata && metadata.width > 0 && metadata.height > 0) {\n // Use metadata (already in canvas coordinates)\n return {\n x: canvasRect.left + metadata.x * scale,\n y: canvasRect.top + metadata.y * scale,\n width: metadata.width * scale,\n height: metadata.height * scale,\n };\n }\n\n // Fallback: find element and use DOM measurement\n const element = findElement(elementId, canvas);\n if (!element) return null;\n\n const bounds = getElementBounds(element);\n return {\n x: bounds.left,\n y: bounds.top,\n width: bounds.width,\n height: bounds.height,\n };\n}\n\n/**\n * Find an element by ID in the canvas (checks both light DOM and shadow DOM).\n */\nfunction findElement(\n elementId: string,\n canvas: CanvasWithMetadata,\n): HTMLElement | null {\n // Try canvas's getElement method first\n if (canvas.getElement) {\n const element = canvas.getElement(elementId);\n if (element) return element;\n }\n\n // Try shadow DOM\n if (canvas.shadowRoot) {\n const element = canvas.shadowRoot.querySelector(\n `[data-element-id=\"${elementId}\"]`,\n ) as HTMLElement | null;\n if (element) return element;\n }\n\n // Try light DOM\n return canvas.querySelector(\n `[data-element-id=\"${elementId}\"]`,\n ) as HTMLElement | null;\n}\n\n/**\n * Calculate the union of multiple bounds (bounding box that contains all).\n */\nexport function unionBounds(bounds: ScreenBounds[]): ScreenBounds | null {\n if (bounds.length === 0) return null;\n\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n\n for (const b of bounds) {\n minX = Math.min(minX, b.x);\n minY = Math.min(minY, b.y);\n maxX = Math.max(maxX, b.x + b.width);\n maxY = Math.max(maxY, b.y + b.height);\n }\n\n return {\n x: minX,\n y: minY,\n width: maxX - minX,\n height: maxY - minY,\n };\n}\n\n/**\n * Calculate selection bounds for all selected elements.\n */\nexport function calculateSelectionBounds(\n selectedIds: Set<string>,\n canvas: CanvasWithMetadata,\n canvasRect: DOMRect,\n scale: number,\n): ScreenBounds | null {\n if (selectedIds.size === 0) return null;\n\n const elementBounds = Array.from(selectedIds)\n .map((id) => calculateElementScreenBounds(id, canvas, canvasRect, scale))\n .filter((b): b is ScreenBounds => b !== null);\n\n return unionBounds(elementBounds);\n}\n\n/**\n * Calculate box-select bounds (convert canvas bounds to screen bounds).\n *\n * @param boxSelectBounds - Box selection bounds in canvas coordinates\n * @param panZoomElement - The pan-zoom element (for canvasToScreen conversion)\n * @param panZoomTransform - Current pan-zoom transform\n * @returns Screen bounds or null\n */\nexport function calculateBoxSelectBounds(\n boxSelectBounds: CanvasBounds | null,\n panZoomElement: HTMLElement | null,\n panZoomTransform: PanZoomTransform | undefined,\n): ScreenBounds | null {\n if (!boxSelectBounds || !panZoomElement || !panZoomTransform) {\n return null;\n }\n\n // Try to use EFPanZoom's canvasToScreen method if available\n const pz = panZoomElement as any;\n if (typeof pz.canvasToScreen === \"function\") {\n const topLeft = pz.canvasToScreen(\n boxSelectBounds.left,\n boxSelectBounds.top,\n );\n const bottomRight = pz.canvasToScreen(\n boxSelectBounds.right,\n boxSelectBounds.bottom,\n );\n\n return {\n x: topLeft.x,\n y: topLeft.y,\n width: bottomRight.x - topLeft.x,\n height: bottomRight.y - topLeft.y,\n };\n }\n\n // Fallback: manual calculation\n const panZoomRect = panZoomElement.getBoundingClientRect();\n const { scale, x, y } = panZoomTransform;\n\n const screenLeft = panZoomRect.left + boxSelectBounds.left * scale + x;\n const screenTop = panZoomRect.top + boxSelectBounds.top * scale + y;\n const screenRight = panZoomRect.left + boxSelectBounds.right * scale + x;\n const screenBottom = panZoomRect.top + boxSelectBounds.bottom * scale + y;\n\n return {\n x: screenLeft,\n y: screenTop,\n width: screenRight - screenLeft,\n height: screenBottom - screenTop,\n };\n}\n\n/**\n * Calculate highlight bounds for the highlighted element.\n */\nexport function calculateHighlightBounds(\n highlightedElementId: string | null,\n canvas: CanvasWithMetadata,\n canvasRect: DOMRect,\n scale: number,\n): ScreenBounds | null {\n if (!highlightedElementId) return null;\n\n return calculateElementScreenBounds(\n highlightedElementId,\n canvas,\n canvasRect,\n scale,\n );\n}\n\n// ============================================================================\n// MAIN: Calculate complete overlay state\n// ============================================================================\n\n/**\n * Calculate the complete overlay state from targets.\n * This is the main entry point that combines semantics and mechanism.\n *\n * @param targets - What should be shown (from getOverlayTargets)\n * @param canvas - Canvas element with metadata\n * @param canvasRect - Canvas content bounding rect\n * @param panZoomElement - Pan-zoom element (for box-select conversion)\n * @param panZoomTransform - Current transform\n * @returns Complete overlay state\n */\nexport function calculateOverlayState(\n targets: OverlayTargets,\n canvas: CanvasWithMetadata,\n canvasRect: DOMRect,\n panZoomElement: HTMLElement | null,\n panZoomTransform: PanZoomTransform | undefined,\n): OverlayState {\n const scale = panZoomTransform?.scale ?? 1;\n\n return {\n selection: calculateSelectionBounds(\n targets.selectedIds,\n canvas,\n canvasRect,\n scale,\n ),\n boxSelect: calculateBoxSelectBounds(\n targets.boxSelectBounds,\n panZoomElement,\n panZoomTransform,\n ),\n highlight: calculateHighlightBounds(\n targets.highlightedElementId,\n canvas,\n canvasRect,\n scale,\n ),\n };\n}\n\n// ============================================================================\n// INVARIANTS: Helper functions to check invariants\n// ============================================================================\n\n/**\n * INVARIANT: Overlay is visible iff bounds exist with positive dimensions.\n */\nexport function isOverlayVisible(bounds: ScreenBounds | null): boolean {\n return bounds !== null && bounds.width > 0 && bounds.height > 0;\n}\n\n/**\n * Check if overlay state has any visible overlays.\n */\nexport function hasVisibleOverlays(state: OverlayState): boolean {\n return (\n isOverlayVisible(state.selection) ||\n isOverlayVisible(state.boxSelect) ||\n isOverlayVisible(state.highlight)\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SelectionController.js","names":[],"sources":["../../../src/canvas/selection/SelectionController.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { SelectionModel } from \"./SelectionModel.js\";\nimport type { SelectionContext } from \"./selectionContext.js\";\n\n/**\n * Reactive controller that bridges SelectionModel and Lit element lifecycle.\n * Provides selection context to child elements.\n */\nexport class SelectionController implements ReactiveController {\n private host: ReactiveControllerHost;\n private selectionModel: SelectionModel;\n private hitTestFn: ((bounds: DOMRect) => string[]) | null = null;\n selectionContext: SelectionContext;\n\n constructor(host: ReactiveControllerHost) {\n this.host = host;\n this.selectionModel = new SelectionModel();\n this.selectionContext = this.createContextProxy();\n host.addController(this);\n\n // Listen to selection change events from the model\n // Use queueMicrotask to defer the update and avoid Lit warning about\n // scheduling updates after update completed (change-in-update)\n this.selectionModel.addEventListener(\"selectionchange\", () => {\n queueMicrotask(() => this.host.requestUpdate());\n });\n }\n\n hostConnected(): void {\n // Context is provided via @provide decorator\n }\n\n hostDisconnected(): void {\n // Cleanup if needed\n }\n\n /**\n * Set the hit test function for box selection.\n */\n setHitTest(fn: (bounds: DOMRect) => string[]): void {\n this.hitTestFn = fn;\n }\n\n /**\n * Get the underlying selection model.\n */\n getModel(): SelectionModel {\n return this.selectionModel;\n }\n\n /**\n * Create a proxy context that delegates to the selection model.\n */\n private createContextProxy(): SelectionContext {\n const controller = this;\n return new Proxy({\n select: (id: string) => {\n controller.selectionModel.select(id);\n // Event will trigger requestUpdate via event listener\n },\n selectMultiple: (ids: string[]) => {\n controller.selectionModel.selectMultiple(ids);\n // Event will trigger requestUpdate via event listener\n },\n addToSelection: (id: string) => {\n controller.selectionModel.addToSelection(id);\n // Event will trigger requestUpdate via event listener\n },\n deselect: (id: string) => {\n controller.selectionModel.deselect(id);\n // Event will trigger requestUpdate via event listener\n },\n toggle: (id: string) => {\n controller.selectionModel.toggle(id);\n // Event will trigger requestUpdate via event listener\n },\n clear: () => {\n controller.selectionModel.clear();\n // Event will trigger requestUpdate via event listener\n },\n startBoxSelect: (x: number, y: number) => {\n controller.selectionModel.startBoxSelect(x, y);\n queueMicrotask(() => controller.host.requestUpdate());\n },\n updateBoxSelect: (x: number, y: number) => {\n controller.selectionModel.updateBoxSelect(x, y);\n queueMicrotask(() => controller.host.requestUpdate());\n },\n endBoxSelect: (\n hitTest: (bounds: DOMRect) => string[],\n addToSelection?: boolean,\n ) => {\n const fn = hitTest || controller.hitTestFn;\n if (fn) {\n controller.selectionModel.endBoxSelect(fn, addToSelection ?? false);\n }\n // Event will trigger requestUpdate via event listener\n },\n createGroup: (ids: string[]) => {\n const groupId = controller.selectionModel.createGroup(ids);\n return groupId;\n },\n ungroup: (groupId: string) => {\n controller.selectionModel.ungroup(groupId);\n },\n selectGroup: (groupId: string) => {\n controller.selectionModel.selectGroup(groupId);\n // Event will trigger requestUpdate via event listener\n },\n getGroupId: (elementId: string) => {\n return controller.selectionModel.getGroupId(elementId);\n },\n getGroupElements: (groupId: string) => {\n return controller.selectionModel.getGroupElements(groupId);\n },\n addEventListener: (\n type: \"selectionchange\",\n listener: (event: CustomEvent) => void,\n ) => {\n controller.selectionModel.addEventListener(type, listener as EventListener);\n },\n removeEventListener: (\n type: \"selectionchange\",\n listener: (event: CustomEvent) => void,\n ) => {\n controller.selectionModel.removeEventListener(type, listener as EventListener);\n },\n } as Omit<SelectionContext, 'selectedIds' | 'selectionMode' | 'boxSelectBounds'>, {\n get(target, prop) {\n if (prop === 'selectedIds') {\n return controller.selectionModel.selectedIds;\n }\n if (prop === 'selectionMode') {\n return controller.selectionModel.selectionMode;\n }\n if (prop === 'boxSelectBounds') {\n return controller.selectionModel.boxSelectBounds;\n }\n return (target as any)[prop];\n },\n }) as SelectionContext;\n }\n}\n"],"mappings":";;;;;;;AAQA,IAAa,sBAAb,MAA+D;CAM7D,YAAY,MAA8B;mBAHkB;AAI1D,OAAK,OAAO;AACZ,OAAK,iBAAiB,IAAI,gBAAgB;AAC1C,OAAK,mBAAmB,KAAK,oBAAoB;AACjD,OAAK,cAAc,KAAK;AAKxB,OAAK,eAAe,iBAAiB,yBAAyB;AAC5D,wBAAqB,KAAK,KAAK,eAAe,CAAC;IAC/C;;CAGJ,gBAAsB;CAItB,mBAAyB;;;;CAOzB,WAAW,IAAyC;AAClD,OAAK,YAAY;;;;;CAMnB,WAA2B;AACzB,SAAO,KAAK;;;;;CAMd,AAAQ,qBAAuC;EAC7C,MAAM,aAAa;AACnB,SAAO,IAAI,MAAM;GACf,SAAS,OAAe;AACtB,eAAW,eAAe,OAAO,GAAG;;GAGtC,iBAAiB,QAAkB;AACjC,eAAW,eAAe,eAAe,IAAI;;GAG/C,iBAAiB,OAAe;AAC9B,eAAW,eAAe,eAAe,GAAG;;GAG9C,WAAW,OAAe;AACxB,eAAW,eAAe,SAAS,GAAG;;GAGxC,SAAS,OAAe;AACtB,eAAW,eAAe,OAAO,GAAG;;GAGtC,aAAa;AACX,eAAW,eAAe,OAAO;;GAGnC,iBAAiB,GAAW,MAAc;AACxC,eAAW,eAAe,eAAe,GAAG,EAAE;AAC9C,yBAAqB,WAAW,KAAK,eAAe,CAAC;;GAEvD,kBAAkB,GAAW,MAAc;AACzC,eAAW,eAAe,gBAAgB,GAAG,EAAE;AAC/C,yBAAqB,WAAW,KAAK,eAAe,CAAC;;GAEvD,eACE,SACA,mBACG;IACH,MAAM,KAAK,WAAW,WAAW;AACjC,QAAI,GACF,YAAW,eAAe,aAAa,IAAI,kBAAkB,MAAM;;GAIvE,cAAc,QAAkB;AAE9B,WADgB,WAAW,eAAe,YAAY,IAAI;;GAG5D,UAAU,YAAoB;AAC5B,eAAW,eAAe,QAAQ,QAAQ;;GAE5C,cAAc,YAAoB;AAChC,eAAW,eAAe,YAAY,QAAQ;;GAGhD,aAAa,cAAsB;AACjC,WAAO,WAAW,eAAe,WAAW,UAAU;;GAExD,mBAAmB,YAAoB;AACrC,WAAO,WAAW,eAAe,iBAAiB,QAAQ;;GAE5D,mBACE,MACA,aACG;AACH,eAAW,eAAe,iBAAiB,MAAM,SAA0B;;GAE7E,sBACE,MACA,aACG;AACH,eAAW,eAAe,oBAAoB,MAAM,SAA0B;;GAEjF,EAAiF,EAChF,IAAI,QAAQ,MAAM;AAChB,OAAI,SAAS,cACX,QAAO,WAAW,eAAe;AAEnC,OAAI,SAAS,gBACX,QAAO,WAAW,eAAe;AAEnC,OAAI,SAAS,kBACX,QAAO,WAAW,eAAe;AAEnC,UAAQ,OAAe;KAE1B,CAAC"}
1
+ {"version":3,"file":"SelectionController.js","names":[],"sources":["../../../src/canvas/selection/SelectionController.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from \"lit\";\nimport { SelectionModel } from \"./SelectionModel.js\";\nimport type { SelectionContext } from \"./selectionContext.js\";\n\n/**\n * Reactive controller that bridges SelectionModel and Lit element lifecycle.\n * Provides selection context to child elements.\n */\nexport class SelectionController implements ReactiveController {\n private host: ReactiveControllerHost;\n private selectionModel: SelectionModel;\n private hitTestFn: ((bounds: DOMRect) => string[]) | null = null;\n selectionContext: SelectionContext;\n\n constructor(host: ReactiveControllerHost) {\n this.host = host;\n this.selectionModel = new SelectionModel();\n this.selectionContext = this.createContextProxy();\n host.addController(this);\n\n // Listen to selection change events from the model\n // Use queueMicrotask to defer the update and avoid Lit warning about\n // scheduling updates after update completed (change-in-update)\n this.selectionModel.addEventListener(\"selectionchange\", () => {\n queueMicrotask(() => this.host.requestUpdate());\n });\n }\n\n hostConnected(): void {\n // Context is provided via @provide decorator\n }\n\n hostDisconnected(): void {\n // Cleanup if needed\n }\n\n /**\n * Set the hit test function for box selection.\n */\n setHitTest(fn: (bounds: DOMRect) => string[]): void {\n this.hitTestFn = fn;\n }\n\n /**\n * Get the underlying selection model.\n */\n getModel(): SelectionModel {\n return this.selectionModel;\n }\n\n /**\n * Create a proxy context that delegates to the selection model.\n */\n private createContextProxy(): SelectionContext {\n const controller = this;\n return new Proxy(\n {\n select: (id: string) => {\n controller.selectionModel.select(id);\n // Event will trigger requestUpdate via event listener\n },\n selectMultiple: (ids: string[]) => {\n controller.selectionModel.selectMultiple(ids);\n // Event will trigger requestUpdate via event listener\n },\n addToSelection: (id: string) => {\n controller.selectionModel.addToSelection(id);\n // Event will trigger requestUpdate via event listener\n },\n deselect: (id: string) => {\n controller.selectionModel.deselect(id);\n // Event will trigger requestUpdate via event listener\n },\n toggle: (id: string) => {\n controller.selectionModel.toggle(id);\n // Event will trigger requestUpdate via event listener\n },\n clear: () => {\n controller.selectionModel.clear();\n // Event will trigger requestUpdate via event listener\n },\n startBoxSelect: (x: number, y: number) => {\n controller.selectionModel.startBoxSelect(x, y);\n queueMicrotask(() => controller.host.requestUpdate());\n },\n updateBoxSelect: (x: number, y: number) => {\n controller.selectionModel.updateBoxSelect(x, y);\n queueMicrotask(() => controller.host.requestUpdate());\n },\n endBoxSelect: (\n hitTest: (bounds: DOMRect) => string[],\n addToSelection?: boolean,\n ) => {\n const fn = hitTest || controller.hitTestFn;\n if (fn) {\n controller.selectionModel.endBoxSelect(fn, addToSelection ?? false);\n }\n // Event will trigger requestUpdate via event listener\n },\n createGroup: (ids: string[]) => {\n const groupId = controller.selectionModel.createGroup(ids);\n return groupId;\n },\n ungroup: (groupId: string) => {\n controller.selectionModel.ungroup(groupId);\n },\n selectGroup: (groupId: string) => {\n controller.selectionModel.selectGroup(groupId);\n // Event will trigger requestUpdate via event listener\n },\n getGroupId: (elementId: string) => {\n return controller.selectionModel.getGroupId(elementId);\n },\n getGroupElements: (groupId: string) => {\n return controller.selectionModel.getGroupElements(groupId);\n },\n addEventListener: (\n type: \"selectionchange\",\n listener: (event: CustomEvent) => void,\n ) => {\n controller.selectionModel.addEventListener(\n type,\n listener as EventListener,\n );\n },\n removeEventListener: (\n type: \"selectionchange\",\n listener: (event: CustomEvent) => void,\n ) => {\n controller.selectionModel.removeEventListener(\n type,\n listener as EventListener,\n );\n },\n } as Omit<\n SelectionContext,\n \"selectedIds\" | \"selectionMode\" | \"boxSelectBounds\"\n >,\n {\n get(target, prop) {\n if (prop === \"selectedIds\") {\n return controller.selectionModel.selectedIds;\n }\n if (prop === \"selectionMode\") {\n return controller.selectionModel.selectionMode;\n }\n if (prop === \"boxSelectBounds\") {\n return controller.selectionModel.boxSelectBounds;\n }\n return (target as any)[prop];\n },\n },\n ) as SelectionContext;\n }\n}\n"],"mappings":";;;;;;;AAQA,IAAa,sBAAb,MAA+D;CAM7D,YAAY,MAA8B;mBAHkB;AAI1D,OAAK,OAAO;AACZ,OAAK,iBAAiB,IAAI,gBAAgB;AAC1C,OAAK,mBAAmB,KAAK,oBAAoB;AACjD,OAAK,cAAc,KAAK;AAKxB,OAAK,eAAe,iBAAiB,yBAAyB;AAC5D,wBAAqB,KAAK,KAAK,eAAe,CAAC;IAC/C;;CAGJ,gBAAsB;CAItB,mBAAyB;;;;CAOzB,WAAW,IAAyC;AAClD,OAAK,YAAY;;;;;CAMnB,WAA2B;AACzB,SAAO,KAAK;;;;;CAMd,AAAQ,qBAAuC;EAC7C,MAAM,aAAa;AACnB,SAAO,IAAI,MACT;GACE,SAAS,OAAe;AACtB,eAAW,eAAe,OAAO,GAAG;;GAGtC,iBAAiB,QAAkB;AACjC,eAAW,eAAe,eAAe,IAAI;;GAG/C,iBAAiB,OAAe;AAC9B,eAAW,eAAe,eAAe,GAAG;;GAG9C,WAAW,OAAe;AACxB,eAAW,eAAe,SAAS,GAAG;;GAGxC,SAAS,OAAe;AACtB,eAAW,eAAe,OAAO,GAAG;;GAGtC,aAAa;AACX,eAAW,eAAe,OAAO;;GAGnC,iBAAiB,GAAW,MAAc;AACxC,eAAW,eAAe,eAAe,GAAG,EAAE;AAC9C,yBAAqB,WAAW,KAAK,eAAe,CAAC;;GAEvD,kBAAkB,GAAW,MAAc;AACzC,eAAW,eAAe,gBAAgB,GAAG,EAAE;AAC/C,yBAAqB,WAAW,KAAK,eAAe,CAAC;;GAEvD,eACE,SACA,mBACG;IACH,MAAM,KAAK,WAAW,WAAW;AACjC,QAAI,GACF,YAAW,eAAe,aAAa,IAAI,kBAAkB,MAAM;;GAIvE,cAAc,QAAkB;AAE9B,WADgB,WAAW,eAAe,YAAY,IAAI;;GAG5D,UAAU,YAAoB;AAC5B,eAAW,eAAe,QAAQ,QAAQ;;GAE5C,cAAc,YAAoB;AAChC,eAAW,eAAe,YAAY,QAAQ;;GAGhD,aAAa,cAAsB;AACjC,WAAO,WAAW,eAAe,WAAW,UAAU;;GAExD,mBAAmB,YAAoB;AACrC,WAAO,WAAW,eAAe,iBAAiB,QAAQ;;GAE5D,mBACE,MACA,aACG;AACH,eAAW,eAAe,iBACxB,MACA,SACD;;GAEH,sBACE,MACA,aACG;AACH,eAAW,eAAe,oBACxB,MACA,SACD;;GAEJ,EAID,EACE,IAAI,QAAQ,MAAM;AAChB,OAAI,SAAS,cACX,QAAO,WAAW,eAAe;AAEnC,OAAI,SAAS,gBACX,QAAO,WAAW,eAAe;AAEnC,OAAI,SAAS,kBACX,QAAO,WAAW,eAAe;AAEnC,UAAQ,OAAe;KAE1B,CACF"}
@@ -1,4 +1,4 @@
1
- import { FrameRenderable, FrameState, FrameTask } from "../preview/FrameController.js";
1
+ import { FrameRenderable, FrameState } from "../preview/FrameController.js";
2
2
  import { EFMedia } from "./EFMedia.js";
3
3
  import * as lit_html2 from "lit-html";
4
4
  import * as lit_html_directives_ref_js1 from "lit-html/directives/ref.js";
@@ -20,11 +20,6 @@ declare class EFAudio extends EFAudio_base implements FrameRenderable {
20
20
  audioElementRef: lit_html_directives_ref_js1.Ref<HTMLAudioElement>;
21
21
  protected updated(changedProperties: Map<PropertyKey, unknown>): void;
22
22
  render(): lit_html2.TemplateResult<1>;
23
- /**
24
- * @deprecated Use FrameRenderable methods (prepareFrame, renderFrame) via FrameController instead.
25
- * This is a compatibility wrapper that delegates to the new system.
26
- */
27
- frameTask: FrameTask;
28
23
  /**
29
24
  * Query readiness state for a given time.
30
25
  * @implements FrameRenderable
@@ -40,11 +35,6 @@ declare class EFAudio extends EFAudio_base implements FrameRenderable {
40
35
  * @implements FrameRenderable
41
36
  */
42
37
  renderFrame(_timeMs: number): void;
43
- /**
44
- * Legacy getter for fragment index task (maps to frameTask)
45
- * Still used by EFCaptions
46
- */
47
- get fragmentIndexTask(): FrameTask;
48
38
  }
49
39
  declare global {
50
40
  interface HTMLElementTagNameMap {
@@ -1,6 +1,6 @@
1
- import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
2
1
  import { TWMixin } from "../gui/TWMixin2.js";
3
- import { PRIORITY_AUDIO, createFrameTaskWrapper } from "../preview/FrameController.js";
2
+ import { PRIORITY_AUDIO } from "../preview/FrameController.js";
3
+ import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
4
4
  import { EFMedia } from "./EFMedia.js";
5
5
  import { html } from "lit";
6
6
  import { customElement, property } from "lit/decorators.js";
@@ -12,7 +12,6 @@ let EFAudio = class EFAudio$1 extends TWMixin(EFMedia) {
12
12
  super(..._args);
13
13
  this.volume = 1;
14
14
  this.audioElementRef = createRef();
15
- this.frameTask = createFrameTaskWrapper(this, { getTimeMs: () => this.desiredSeekTimeMs });
16
15
  }
17
16
  /**
18
17
  * EFAudio only requires audio tracks - skip video track validation
@@ -56,13 +55,6 @@ let EFAudio = class EFAudio$1 extends TWMixin(EFMedia) {
56
55
  * @implements FrameRenderable
57
56
  */
58
57
  renderFrame(_timeMs) {}
59
- /**
60
- * Legacy getter for fragment index task (maps to frameTask)
61
- * Still used by EFCaptions
62
- */
63
- get fragmentIndexTask() {
64
- return this.frameTask;
65
- }
66
58
  };
67
59
  __decorate([property({
68
60
  type: Number,
@@ -1 +1 @@
1
- {"version":3,"file":"EFAudio.js","names":["EFAudio","#mediaEngineLoaded"],"sources":["../../src/elements/EFAudio.ts"],"sourcesContent":["import { html } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport { TWMixin } from \"../gui/TWMixin.js\";\nimport {\n type FrameRenderable,\n type FrameState,\n createFrameTaskWrapper,\n PRIORITY_AUDIO,\n} from \"../preview/FrameController.js\";\nimport { EFMedia } from \"./EFMedia.js\";\n\n@customElement(\"ef-audio\")\nexport class EFAudio extends TWMixin(EFMedia) implements FrameRenderable {\n /**\n * EFAudio only requires audio tracks - skip video track validation\n * to avoid unnecessary network requests to transcoding service.\n */\n override get requiredTracks(): \"audio\" | \"video\" | \"both\" {\n return \"audio\";\n }\n\n /**\n * Audio volume level (0.0 to 1.0)\n * @domAttribute \"volume\"\n */\n @property({ type: Number, attribute: \"volume\", reflect: true })\n volume = 1.0;\n\n audioElementRef = createRef<HTMLAudioElement>();\n\n #mediaEngineLoaded = false;\n\n protected updated(\n changedProperties: Map<PropertyKey, unknown>,\n ): void {\n super.updated(changedProperties);\n \n // Sync volume property to HTMLAudioElement whenever it changes or element is first rendered\n if (this.audioElementRef.value) {\n if (changedProperties.has(\"volume\") || changedProperties.size === 0) {\n this.audioElementRef.value.volume = this.volume;\n }\n }\n }\n\n render() {\n return html`<audio ${ref(this.audioElementRef)}></audio>`;\n }\n\n /**\n * @deprecated Use FrameRenderable methods (prepareFrame, renderFrame) via FrameController instead.\n * This is a compatibility wrapper that delegates to the new system.\n */\n frameTask = createFrameTaskWrapper(this, {\n getTimeMs: () => this.desiredSeekTimeMs,\n });\n\n // ============================================================================\n // FrameRenderable Implementation\n // Centralized frame control - no Lit Tasks\n // ============================================================================\n\n /**\n * Query readiness state for a given time.\n * @implements FrameRenderable\n */\n getFrameState(_timeMs: number): FrameState {\n // Check if media engine is loaded\n const isReady = this.#mediaEngineLoaded && this.mediaEngineTask.status === 2; // COMPLETE\n\n return {\n needsPreparation: !isReady,\n isReady,\n priority: PRIORITY_AUDIO,\n };\n }\n\n /**\n * Async preparation - waits for media engine to load.\n * @implements FrameRenderable\n */\n async prepareFrame(_timeMs: number, signal: AbortSignal): Promise<void> {\n // Just ensure media engine is loaded\n const mediaEngine = await this.getMediaEngine(signal);\n this.#mediaEngineLoaded = !!mediaEngine;\n signal.throwIfAborted();\n }\n\n /**\n * Synchronous render - audio plays via HTMLAudioElement, no explicit render needed.\n * @implements FrameRenderable\n */\n renderFrame(_timeMs: number): void {\n // Audio playback is handled by the browser's HTMLAudioElement\n // No explicit rendering action needed\n }\n\n // ============================================================================\n // End FrameRenderable Implementation\n // ============================================================================\n\n /**\n * Legacy getter for fragment index task (maps to frameTask)\n * Still used by EFCaptions\n */\n get fragmentIndexTask() {\n return this.frameTask;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-audio\": EFAudio;\n }\n}\n"],"mappings":";;;;;;;;;AAaO,oBAAMA,kBAAgB,QAAQ,QAAQ,CAA4B;;;gBAc9D;yBAES,WAA6B;mBAyBnC,uBAAuB,MAAM,EACvC,iBAAiB,KAAK,mBACvB,CAAC;;;;;;CAtCF,IAAa,iBAA6C;AACxD,SAAO;;CAYT,qBAAqB;CAErB,AAAU,QACR,mBACM;AACN,QAAM,QAAQ,kBAAkB;AAGhC,MAAI,KAAK,gBAAgB,OACvB;OAAI,kBAAkB,IAAI,SAAS,IAAI,kBAAkB,SAAS,EAChE,MAAK,gBAAgB,MAAM,SAAS,KAAK;;;CAK/C,SAAS;AACP,SAAO,IAAI,UAAU,IAAI,KAAK,gBAAgB,CAAC;;;;;;CAoBjD,cAAc,SAA6B;EAEzC,MAAM,UAAU,MAAKC,qBAAsB,KAAK,gBAAgB,WAAW;AAE3E,SAAO;GACL,kBAAkB,CAAC;GACnB;GACA,UAAU;GACX;;;;;;CAOH,MAAM,aAAa,SAAiB,QAAoC;AAGtE,QAAKA,oBAAqB,CAAC,CADP,MAAM,KAAK,eAAe,OAAO;AAErD,SAAO,gBAAgB;;;;;;CAOzB,YAAY,SAAuB;;;;;CAanC,IAAI,oBAAoB;AACtB,SAAO,KAAK;;;YAjFb,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAU,SAAS;CAAM,CAAC;sBAdhE,cAAc,WAAW"}
1
+ {"version":3,"file":"EFAudio.js","names":["EFAudio","#mediaEngineLoaded"],"sources":["../../src/elements/EFAudio.ts"],"sourcesContent":["import { html } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport { TWMixin } from \"../gui/TWMixin.js\";\nimport {\n type FrameRenderable,\n type FrameState,\n PRIORITY_AUDIO,\n} from \"../preview/FrameController.js\";\nimport { EFMedia } from \"./EFMedia.js\";\n\n@customElement(\"ef-audio\")\nexport class EFAudio extends TWMixin(EFMedia) implements FrameRenderable {\n /**\n * EFAudio only requires audio tracks - skip video track validation\n * to avoid unnecessary network requests to transcoding service.\n */\n override get requiredTracks(): \"audio\" | \"video\" | \"both\" {\n return \"audio\";\n }\n\n /**\n * Audio volume level (0.0 to 1.0)\n * @domAttribute \"volume\"\n */\n @property({ type: Number, attribute: \"volume\", reflect: true })\n volume = 1.0;\n\n audioElementRef = createRef<HTMLAudioElement>();\n\n #mediaEngineLoaded = false;\n\n protected updated(changedProperties: Map<PropertyKey, unknown>): void {\n super.updated(changedProperties);\n\n // Sync volume property to HTMLAudioElement whenever it changes or element is first rendered\n if (this.audioElementRef.value) {\n if (changedProperties.has(\"volume\") || changedProperties.size === 0) {\n this.audioElementRef.value.volume = this.volume;\n }\n }\n }\n\n render() {\n return html`<audio ${ref(this.audioElementRef)}></audio>`;\n }\n\n // ============================================================================\n // FrameRenderable Implementation\n // ============================================================================\n\n /**\n * Query readiness state for a given time.\n * @implements FrameRenderable\n */\n getFrameState(_timeMs: number): FrameState {\n // Check if media engine is loaded\n const isReady =\n this.#mediaEngineLoaded && this.mediaEngineTask.status === 2; // COMPLETE\n\n return {\n needsPreparation: !isReady,\n isReady,\n priority: PRIORITY_AUDIO,\n };\n }\n\n /**\n * Async preparation - waits for media engine to load.\n * @implements FrameRenderable\n */\n async prepareFrame(_timeMs: number, signal: AbortSignal): Promise<void> {\n // Just ensure media engine is loaded\n const mediaEngine = await this.getMediaEngine(signal);\n this.#mediaEngineLoaded = !!mediaEngine;\n signal.throwIfAborted();\n }\n\n /**\n * Synchronous render - audio plays via HTMLAudioElement, no explicit render needed.\n * @implements FrameRenderable\n */\n renderFrame(_timeMs: number): void {\n // Audio playback is handled by the browser's HTMLAudioElement\n // No explicit rendering action needed\n }\n\n // ============================================================================\n // End FrameRenderable Implementation\n // ============================================================================\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-audio\": EFAudio;\n }\n}\n"],"mappings":";;;;;;;;;AAYO,oBAAMA,kBAAgB,QAAQ,QAAQ,CAA4B;;;gBAc9D;yBAES,WAA6B;;;;;;CAX/C,IAAa,iBAA6C;AACxD,SAAO;;CAYT,qBAAqB;CAErB,AAAU,QAAQ,mBAAoD;AACpE,QAAM,QAAQ,kBAAkB;AAGhC,MAAI,KAAK,gBAAgB,OACvB;OAAI,kBAAkB,IAAI,SAAS,IAAI,kBAAkB,SAAS,EAChE,MAAK,gBAAgB,MAAM,SAAS,KAAK;;;CAK/C,SAAS;AACP,SAAO,IAAI,UAAU,IAAI,KAAK,gBAAgB,CAAC;;;;;;CAWjD,cAAc,SAA6B;EAEzC,MAAM,UACJ,MAAKC,qBAAsB,KAAK,gBAAgB,WAAW;AAE7D,SAAO;GACL,kBAAkB,CAAC;GACnB;GACA,UAAU;GACX;;;;;;CAOH,MAAM,aAAa,SAAiB,QAAoC;AAGtE,QAAKA,oBAAqB,CAAC,CADP,MAAM,KAAK,eAAe,OAAO;AAErD,SAAO,gBAAgB;;;;;;CAOzB,YAAY,SAAuB;;YAzDlC,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAU,SAAS;CAAM,CAAC;sBAdhE,cAAc,WAAW"}
@@ -1,10 +1,10 @@
1
- import { FrameRenderable, FrameState, FrameTask } from "../preview/FrameController.js";
1
+ import { FrameRenderable, FrameState } from "../preview/FrameController.js";
2
2
  import { EFSourceMixinInterface } from "./EFSourceMixin.js";
3
3
  import { TemporalMixinInterface } from "./EFTemporal.js";
4
4
  import { FetchMixinInterface } from "./FetchMixin.js";
5
5
  import { AsyncValue } from "./EFMedia.js";
6
- import { EFVideo } from "./EFVideo.js";
7
6
  import { EFAudio } from "./EFAudio.js";
7
+ import { EFVideo } from "./EFVideo.js";
8
8
  import * as lit1 from "lit";
9
9
  import { LitElement, PropertyValueMap } from "lit";
10
10
  import * as lit_html0 from "lit-html";
@@ -28,7 +28,7 @@ interface Caption {
28
28
  * Caption active word element - displays the currently spoken word.
29
29
  * Uses light DOM for simplicity - parent sets textContent directly.
30
30
  */
31
- declare class EFCaptionsActiveWord extends HTMLElement {
31
+ declare class EFCaptionsActiveWord extends LitElement {
32
32
  #private;
33
33
  set wordText(text: string);
34
34
  get wordText(): string;
@@ -39,7 +39,7 @@ declare class EFCaptionsActiveWord extends HTMLElement {
39
39
  * Caption segment element - displays a full caption segment.
40
40
  * Uses light DOM for simplicity - parent sets textContent directly.
41
41
  */
42
- declare class EFCaptionsSegment extends HTMLElement {
42
+ declare class EFCaptionsSegment extends LitElement {
43
43
  #private;
44
44
  set segmentText(text: string);
45
45
  get segmentText(): string;
@@ -91,15 +91,11 @@ declare class EFCaptions extends EFCaptions_base implements FrameRenderable {
91
91
  * AsyncValue wrapper for backwards compatibility
92
92
  */
93
93
  unifiedCaptionsDataTask: AsyncValue<Caption | null>;
94
+ shouldAutoReady(): boolean;
94
95
  /**
95
96
  * Load captions data from all possible sources
96
97
  */
97
98
  loadCaptionsData(signal?: AbortSignal): Promise<Caption | null>;
98
- /**
99
- * @deprecated Use FrameRenderable methods (prepareFrame, renderFrame) via FrameController instead.
100
- * This is a compatibility wrapper that delegates to the new system.
101
- */
102
- frameTask: FrameTask;
103
99
  /**
104
100
  * Query readiness state for a given time.
105
101
  * @implements FrameRenderable
@@ -1,6 +1,6 @@
1
+ import { PRIORITY_CAPTIONS } from "../preview/FrameController.js";
1
2
  import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
2
3
  import { EFTemporal, flushStartTimeMsCache } from "./EFTemporal.js";
3
- import { PRIORITY_CAPTIONS, createFrameTaskWrapper } from "../preview/FrameController.js";
4
4
  import { EFSourceMixin } from "./EFSourceMixin.js";
5
5
  import { FetchMixin } from "./FetchMixin.js";
6
6
  import { AsyncValue } from "./EFMedia.js";
@@ -19,7 +19,7 @@ const stopWords = new Set([
19
19
  "?",
20
20
  ","
21
21
  ]);
22
- let EFCaptionsActiveWord = class EFCaptionsActiveWord$1 extends HTMLElement {
22
+ let EFCaptionsActiveWord = class EFCaptionsActiveWord$1 extends LitElement {
23
23
  #wordText = "";
24
24
  #wordIndex = 0;
25
25
  set wordText(text) {
@@ -45,7 +45,7 @@ let EFCaptionsActiveWord = class EFCaptionsActiveWord$1 extends HTMLElement {
45
45
  }
46
46
  };
47
47
  EFCaptionsActiveWord = __decorate([customElement("ef-captions-active-word")], EFCaptionsActiveWord);
48
- let EFCaptionsSegment = class EFCaptionsSegment$1 extends HTMLElement {
48
+ let EFCaptionsSegment = class EFCaptionsSegment$1 extends LitElement {
49
49
  #segmentText = "";
50
50
  set segmentText(text) {
51
51
  this.#segmentText = text;
@@ -102,7 +102,6 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
102
102
  this.beforeActiveWordContainers = this.getElementsByTagName("ef-captions-before-active-word");
103
103
  this.afterActiveWordContainers = this.getElementsByTagName("ef-captions-after-active-word");
104
104
  this.unifiedCaptionsDataTask = new AsyncValue();
105
- this.frameTask = createFrameTaskWrapper(this);
106
105
  }
107
106
  static {
108
107
  this.styles = [css`
@@ -128,12 +127,14 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
128
127
  }
129
128
  transcriptionsPath() {
130
129
  if (!this.targetElement) return null;
131
- if (this.targetElement.assetId) return `${this.apiHost}/api/v1/isobmff_files/${this.targetElement.assetId}/transcription`;
130
+ const fileId = this.targetElement.fileId ?? this.targetElement.assetId;
131
+ if (fileId) return `${this.apiHost}/api/v1/files/${fileId}/transcription`;
132
132
  return null;
133
133
  }
134
134
  captionsPath() {
135
135
  if (!this.targetElement) return null;
136
- if (this.targetElement.assetId) return `${this.apiHost}/api/v1/caption_files/${this.targetElement.assetId}`;
136
+ const fileId = this.targetElement.fileId ?? this.targetElement.assetId;
137
+ if (fileId) return `${this.apiHost}/api/v1/files/${fileId}`;
137
138
  const targetSrc = this.targetElement.src;
138
139
  let normalizedSrc = targetSrc.startsWith("/") ? targetSrc.slice(1) : targetSrc;
139
140
  normalizedSrc = normalizedSrc.replace(/^\/+/, "");
@@ -143,22 +144,31 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
143
144
  #captionsDataPromise = null;
144
145
  #captionsDataValue = null;
145
146
  #transcriptionData = null;
147
+ shouldAutoReady() {
148
+ return false;
149
+ }
146
150
  /**
147
151
  * Load captions data from all possible sources
148
152
  */
149
153
  async loadCaptionsData(signal) {
150
- if (this.#captionsDataLoaded && this.#captionsDataValue) return this.#captionsDataValue;
154
+ if (this.#captionsDataLoaded && this.#captionsDataValue) {
155
+ this.setContentReadyState("ready");
156
+ return this.#captionsDataValue;
157
+ }
151
158
  if (this.#captionsDataPromise) return this.#captionsDataPromise;
152
159
  this.unifiedCaptionsDataTask.startPending();
160
+ this.setContentReadyState("loading");
153
161
  this.#captionsDataPromise = this.#doLoadCaptionsData(signal);
154
162
  try {
155
163
  this.#captionsDataValue = await this.#captionsDataPromise;
156
164
  this.#captionsDataLoaded = true;
157
165
  if (this.#captionsDataValue) this.unifiedCaptionsDataTask.setValue(this.#captionsDataValue);
166
+ this.setContentReadyState("ready");
158
167
  return this.#captionsDataValue;
159
168
  } catch (error) {
160
169
  if (error instanceof DOMException && error.name === "AbortError") throw error;
161
170
  console.error("Failed to load captions data:", error);
171
+ this.setContentReadyState("error");
162
172
  return null;
163
173
  } finally {
164
174
  this.#captionsDataPromise = null;
@@ -167,7 +177,7 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
167
177
  async #doLoadCaptionsData(signal) {
168
178
  if (this.captionsData) return this.captionsData;
169
179
  if (this.captionsScript) {
170
- const scriptElement = document.getElementById(this.captionsScript);
180
+ const scriptElement = this.#findElementById(this.captionsScript);
171
181
  if (scriptElement?.textContent) try {
172
182
  return JSON.parse(scriptElement.textContent);
173
183
  } catch (error) {
@@ -236,7 +246,7 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
236
246
  connectedCallback() {
237
247
  super.connectedCallback();
238
248
  this.loadCaptionsData().catch(() => {});
239
- const target = this.targetSelector ? document.getElementById(this.targetSelector) : null;
249
+ const target = this.targetSelector ? this.#findElementById(this.targetSelector) : null;
240
250
  if (target && (target instanceof EFAudio || target instanceof EFVideo)) new CrossUpdateController(target, this);
241
251
  else if (this.hasCustomCaptionsData && this.rootTimegroup) new CrossUpdateController(this.rootTimegroup, this);
242
252
  if (this.rootTimegroup) {
@@ -296,6 +306,7 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
296
306
  }
297
307
  this.updateTextContainers();
298
308
  if (changedProperties.has("captionsData") || changedProperties.has("captionsSrc") || changedProperties.has("captionsScript")) {
309
+ this.emitContentChange("source");
299
310
  this.#cachedIntrinsicDurationMs = null;
300
311
  this.#captionsDataLoaded = false;
301
312
  this.#captionsDataValue = null;
@@ -359,7 +370,7 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
359
370
  }
360
371
  }
361
372
  get targetElement() {
362
- const target = document.getElementById(this.targetSelector ?? "");
373
+ const target = this.targetSelector ? this.#findElementById(this.targetSelector) : null;
363
374
  if (target instanceof EFAudio || target instanceof EFVideo) return target;
364
375
  if (this.hasCustomCaptionsData) return null;
365
376
  return null;
@@ -367,12 +378,24 @@ let EFCaptions = class EFCaptions$1 extends EFSourceMixin(EFTemporal(FetchMixin(
367
378
  get hasCustomCaptionsData() {
368
379
  return !!(this.captionsData || this.captionsSrc || this.captionsScript);
369
380
  }
381
+ /**
382
+ * Find element by ID, searching within clone scope first to avoid cross-boundary references.
383
+ * @private
384
+ */
385
+ #findElementById(id) {
386
+ const container = this.closest("ef-timegroup, ef-configuration");
387
+ if (container) {
388
+ const result = container.querySelector(`#${CSS.escape(id)}`);
389
+ if (result) return result;
390
+ }
391
+ return document.getElementById(id);
392
+ }
370
393
  get intrinsicDurationMs() {
371
394
  if (this.#cachedIntrinsicDurationMs !== null) return this.#cachedIntrinsicDurationMs;
372
395
  let captionsData = null;
373
396
  if (this.captionsData) captionsData = this.captionsData;
374
397
  else if (this.captionsScript) {
375
- const scriptElement = document.getElementById(this.captionsScript);
398
+ const scriptElement = this.#findElementById(this.captionsScript);
376
399
  if (scriptElement?.textContent) try {
377
400
  captionsData = JSON.parse(scriptElement.textContent);
378
401
  } catch {}