@editframe/elements 0.45.1 → 0.45.3

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 (148) hide show
  1. package/dist/DelayedLoadingState.js.map +1 -1
  2. package/dist/EF_FRAMEGEN.js.map +1 -1
  3. package/dist/EF_RENDERING.js.map +1 -1
  4. package/dist/canvas/EFCanvas.js +3 -3
  5. package/dist/canvas/EFCanvas.js.map +1 -1
  6. package/dist/canvas/EFCanvasItem.js.map +1 -1
  7. package/dist/canvas/api/CanvasAPI.js.map +1 -1
  8. package/dist/canvas/getElementBounds.js.map +1 -1
  9. package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
  10. package/dist/canvas/overlays/overlayState.js.map +1 -1
  11. package/dist/canvas/selection/SelectionController.js +25 -23
  12. package/dist/canvas/selection/SelectionController.js.map +1 -1
  13. package/dist/canvas/selection/SelectionModel.js.map +1 -1
  14. package/dist/canvas/selection/selectionContext.js.map +1 -1
  15. package/dist/elements/ContainerInfo.js.map +1 -1
  16. package/dist/elements/CrossUpdateController.js.map +1 -1
  17. package/dist/elements/EFAudio.d.ts +2 -2
  18. package/dist/elements/EFAudio.js.map +1 -1
  19. package/dist/elements/EFCaptions.d.ts +2 -2
  20. package/dist/elements/EFCaptions.js.map +1 -1
  21. package/dist/elements/EFImage.d.ts +4 -4
  22. package/dist/elements/EFImage.js +1 -1
  23. package/dist/elements/EFImage.js.map +1 -1
  24. package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
  25. package/dist/elements/EFMedia/CachedFetcher.js.map +1 -1
  26. package/dist/elements/EFMedia/MediaEngine.js.map +1 -1
  27. package/dist/elements/EFMedia/SegmentIndex.js.map +1 -1
  28. package/dist/elements/EFMedia/SegmentTransport.js.map +1 -1
  29. package/dist/elements/EFMedia/TimingModel.js.map +1 -1
  30. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  31. package/dist/elements/EFMedia/shared/GlobalInputCache.js.map +1 -1
  32. package/dist/elements/EFMedia/shared/PrecisionUtils.js.map +1 -1
  33. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  34. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
  35. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
  36. package/dist/elements/EFMedia.js.map +1 -1
  37. package/dist/elements/EFPanZoom.js +9 -8
  38. package/dist/elements/EFPanZoom.js.map +1 -1
  39. package/dist/elements/EFSourceMixin.js.map +1 -1
  40. package/dist/elements/EFSurface.js.map +1 -1
  41. package/dist/elements/EFTemporal.js.map +1 -1
  42. package/dist/elements/EFText.d.ts +4 -4
  43. package/dist/elements/EFText.js.map +1 -1
  44. package/dist/elements/EFTextSegment.d.ts +4 -4
  45. package/dist/elements/EFTimegroup.d.ts +4 -4
  46. package/dist/elements/EFTimegroup.js +7 -8
  47. package/dist/elements/EFTimegroup.js.map +1 -1
  48. package/dist/elements/EFVideo.d.ts +4 -4
  49. package/dist/elements/EFVideo.js.map +1 -1
  50. package/dist/elements/EFWaveform.d.ts +4 -4
  51. package/dist/elements/EFWaveform.js.map +1 -1
  52. package/dist/elements/ElementPositionInfo.js.map +1 -1
  53. package/dist/elements/FetchMixin.js.map +1 -1
  54. package/dist/elements/SampleBuffer.js.map +1 -1
  55. package/dist/elements/TargetController.js.map +1 -1
  56. package/dist/elements/TimegroupController.js.map +1 -1
  57. package/dist/elements/cloneFactoryRegistry.js.map +1 -1
  58. package/dist/elements/durationConverter.js.map +1 -1
  59. package/dist/elements/easingUtils.js.map +1 -1
  60. package/dist/elements/renderTemporalAudio.js.map +1 -1
  61. package/dist/elements/setupTemporalHierarchy.js.map +1 -1
  62. package/dist/elements/updateAnimations.js +1 -1
  63. package/dist/elements/updateAnimations.js.map +1 -1
  64. package/dist/getRenderInfo.js.map +1 -1
  65. package/dist/gui/ContextMixin.js.map +1 -1
  66. package/dist/gui/Controllable.js.map +1 -1
  67. package/dist/gui/EFActiveRootTemporal.js.map +1 -1
  68. package/dist/gui/EFConfiguration.d.ts +4 -4
  69. package/dist/gui/EFControls.js.map +1 -1
  70. package/dist/gui/EFFilmstrip.js.map +1 -1
  71. package/dist/gui/EFFitScale.js.map +1 -1
  72. package/dist/gui/EFOverlayItem.js.map +1 -1
  73. package/dist/gui/EFOverlayLayer.js.map +1 -1
  74. package/dist/gui/EFPreview.js.map +1 -1
  75. package/dist/gui/EFResizableBox.js.map +1 -1
  76. package/dist/gui/EFScrubber.js.map +1 -1
  77. package/dist/gui/EFTimeDisplay.js.map +1 -1
  78. package/dist/gui/EFTimelineRuler.js.map +1 -1
  79. package/dist/gui/EFTogglePlay.js.map +1 -1
  80. package/dist/gui/EFTransformHandles.js.map +1 -1
  81. package/dist/gui/EFWorkbench.js.map +1 -1
  82. package/dist/gui/FitScaleHelpers.js.map +1 -1
  83. package/dist/gui/PlaybackController.js.map +1 -1
  84. package/dist/gui/TWMixin2.js.map +1 -1
  85. package/dist/gui/TargetOrContextMixin.js.map +1 -1
  86. package/dist/gui/currentTimeContext.js.map +1 -1
  87. package/dist/gui/efContext.js.map +1 -1
  88. package/dist/gui/fetchContext.js.map +1 -1
  89. package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
  90. package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
  91. package/dist/gui/hierarchy/hierarchyContext.js.map +1 -1
  92. package/dist/gui/panZoomTransformContext.js.map +1 -1
  93. package/dist/gui/previewSettingsContext.js.map +1 -1
  94. package/dist/gui/theme.js.map +1 -1
  95. package/dist/gui/timeline/EFTimeline.js +0 -1
  96. package/dist/gui/timeline/EFTimeline.js.map +1 -1
  97. package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
  98. package/dist/gui/timeline/TrimHandles.js.map +1 -1
  99. package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
  100. package/dist/gui/timeline/timelineStateContext.js.map +1 -1
  101. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
  102. package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
  103. package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
  104. package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
  105. package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
  106. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
  107. package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
  108. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
  109. package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
  110. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
  111. package/dist/gui/transformCalculations.js.map +1 -1
  112. package/dist/gui/transformUtils.js.map +1 -1
  113. package/dist/gui/tree/EFTree.js.map +1 -1
  114. package/dist/gui/tree/EFTreeItem.js.map +1 -1
  115. package/dist/index.js.map +1 -1
  116. package/dist/otel/BridgeSpanExporter.js.map +1 -1
  117. package/dist/otel/setupBrowserTracing.js.map +1 -1
  118. package/dist/otel/tracingHelpers.js.map +1 -1
  119. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
  120. package/dist/preview/FrameController.js.map +1 -1
  121. package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
  122. package/dist/preview/RenderContext.js.map +1 -1
  123. package/dist/preview/RenderProfiler.js.map +1 -1
  124. package/dist/preview/RenderStats.js.map +1 -1
  125. package/dist/preview/encoding/canvasEncoder.js.map +1 -1
  126. package/dist/preview/encoding/mainThreadEncoder.js +1 -1
  127. package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
  128. package/dist/preview/previewSettings.js.map +1 -1
  129. package/dist/preview/previewTypes.js.map +1 -1
  130. package/dist/preview/renderElementToCanvas.js.map +1 -1
  131. package/dist/preview/renderTimegroupToCanvas.js +2 -44
  132. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  133. package/dist/preview/renderTimegroupToVideo.js +2 -2
  134. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  135. package/dist/preview/renderVideoToVideo.js +2 -2
  136. package/dist/preview/renderVideoToVideo.js.map +1 -1
  137. package/dist/preview/renderers.js.map +1 -1
  138. package/dist/preview/rendering/ScaleConfig.js.map +1 -1
  139. package/dist/preview/rendering/loadImage.js.map +1 -1
  140. package/dist/preview/rendering/renderToImageNative.js.map +1 -1
  141. package/dist/preview/rendering/serializeTimelineDirect.js +1 -1
  142. package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
  143. package/dist/preview/statsTrackingStrategy.js.map +1 -1
  144. package/dist/preview/workers/WorkerPool.js.map +1 -1
  145. package/dist/render/EFRenderAPI.js.map +1 -1
  146. package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
  147. package/dist/utils/LRUCache.js.map +1 -1
  148. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"loadImage.js","names":[],"sources":["../../../src/preview/rendering/loadImage.ts"],"sourcesContent":["import { defaultProfiler } from \"../RenderProfiler.js\";\n\n/**\n * Load an image from a data URI. Returns a Promise that resolves when loaded.\n */\nexport function loadImageFromDataUri(\n dataUri: string,\n): Promise<HTMLImageElement> {\n const img = new Image();\n const imageLoadStart = performance.now();\n\n return new Promise<HTMLImageElement>((resolve, reject) => {\n img.onload = () => {\n defaultProfiler.addTime(\"imageLoad\", performance.now() - imageLoadStart);\n resolve(img);\n };\n img.onerror = reject;\n img.src = dataUri;\n });\n}\n"],"mappings":";;;;;;AAKA,SAAgB,qBACd,SAC2B;CAC3B,MAAM,MAAM,IAAI,OAAO;CACvB,MAAM,iBAAiB,YAAY,KAAK;AAExC,QAAO,IAAI,SAA2B,SAAS,WAAW;AACxD,MAAI,eAAe;AACjB,mBAAgB,QAAQ,aAAa,YAAY,KAAK,GAAG,eAAe;AACxE,WAAQ,IAAI;;AAEd,MAAI,UAAU;AACd,MAAI,MAAM;GACV"}
1
+ {"version":3,"file":"loadImage.js","names":[],"sources":["../../../src/preview/rendering/loadImage.ts"],"sourcesContent":["import { defaultProfiler } from \"../RenderProfiler.js\";\n\n/**\n * Load an image from a data URI. Returns a Promise that resolves when loaded.\n */\nexport function loadImageFromDataUri(dataUri: string): Promise<HTMLImageElement> {\n const img = new Image();\n const imageLoadStart = performance.now();\n\n return new Promise<HTMLImageElement>((resolve, reject) => {\n img.onload = () => {\n defaultProfiler.addTime(\"imageLoad\", performance.now() - imageLoadStart);\n resolve(img);\n };\n img.onerror = reject;\n img.src = dataUri;\n });\n}\n"],"mappings":";;;;;;AAKA,SAAgB,qBAAqB,SAA4C;CAC/E,MAAM,MAAM,IAAI,OAAO;CACvB,MAAM,iBAAiB,YAAY,KAAK;AAExC,QAAO,IAAI,SAA2B,SAAS,WAAW;AACxD,MAAI,eAAe;AACjB,mBAAgB,QAAQ,aAAa,YAAY,KAAK,GAAG,eAAe;AACxE,WAAQ,IAAI;;AAEd,MAAI,UAAU;AACd,MAAI,MAAM;GACV"}
@@ -1 +1 @@
1
- {"version":3,"file":"renderToImageNative.js","names":["captureCanvas: HTMLCanvasElement","dpr"],"sources":["../../../src/preview/rendering/renderToImageNative.ts"],"sourcesContent":["/**\n * Native HTML-in-Canvas rendering using drawElementImage API.\n */\n\nimport type {\n HtmlInCanvasContext,\n HtmlInCanvasElement,\n NativeRenderOptions,\n} from \"./types.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\n\n/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */\nconst _layoutInitializedCanvases = new WeakSet<HTMLCanvasElement>();\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise((resolve) => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Create a canvas element with proper DPR handling.\n * Buffer size is based on renderWidth/renderHeight (internal resolution).\n * CSS size is based on fullWidth/fullHeight (logical display size).\n */\nexport function createDprCanvas(options: {\n renderWidth: number;\n renderHeight: number;\n scale: number;\n dpr?: number;\n fullWidth: number;\n fullHeight: number;\n}): HTMLCanvasElement {\n const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;\n const dpr = options.dpr ?? window.devicePixelRatio ?? 1;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(renderWidth * scale * dpr);\n canvas.height = Math.floor(renderHeight * scale * dpr);\n canvas.style.width = `${Math.floor(fullWidth * scale)}px`;\n canvas.style.height = `${Math.floor(fullHeight * scale)}px`;\n\n return canvas;\n}\n\n/**\n * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).\n * This is much faster than the foreignObject approach and avoids canvas tainting.\n *\n * Note: The native API renders at device pixel ratio, so we capture at DPR scale\n * and then downsample to logical pixels to match the foreignObject path's output.\n *\n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options (skipWait for batch mode)\n *\n * @see https://github.com/WICG/html-in-canvas\n */\nexport async function renderToImageNative(\n container: HTMLElement,\n width: number,\n height: number,\n options: NativeRenderOptions = {},\n): Promise<HTMLCanvasElement> {\n const t0 = performance.now();\n const { reuseCanvas, skipDprScaling = false } = options;\n // Use 1x DPR when skipDprScaling is true (for video export) - 4x fewer pixels!\n const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;\n\n // Use provided canvas or create new one\n let captureCanvas: HTMLCanvasElement;\n let shouldCleanup = false;\n\n if (reuseCanvas) {\n captureCanvas = reuseCanvas;\n\n // Ensure canvas dimensions match (both attribute and CSS)\n const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;\n const targetWidth = Math.floor(width * dpr);\n const targetHeight = Math.floor(height * dpr);\n\n // Set attribute dimensions (pixel buffer size)\n if (captureCanvas.width !== targetWidth) {\n captureCanvas.width = targetWidth;\n }\n if (captureCanvas.height !== targetHeight) {\n captureCanvas.height = targetHeight;\n }\n\n // Ensure CSS dimensions and positioning (same as non-reuse path)\n // This ensures consistent behavior and avoids layout issues\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n\n // Ensure layoutsubtree is set (required for drawElementImage)\n if (!captureCanvas.hasAttribute(\"layoutsubtree\")) {\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n }\n\n // Ensure canvas is in DOM (required for drawElementImage layout)\n if (!captureCanvas.parentNode) {\n document.body.appendChild(captureCanvas);\n }\n\n // Ensure container is child of canvas\n if (container.parentElement !== captureCanvas) {\n captureCanvas.appendChild(container);\n }\n\n // Ensure container is visible (not display: none) for layout\n // drawElementImage requires the element to be laid out\n const containerStyle = getComputedStyle(container);\n if (containerStyle.display === \"none\") {\n container.style.display = \"block\";\n }\n\n // Force synchronous layout ONLY on first use with this canvas\n // For batch rendering (video export), repeated layout forces are expensive\n // We only need to force layout once to ensure everything is ready\n if (!_layoutInitializedCanvases.has(captureCanvas)) {\n void captureCanvas.offsetHeight;\n void container.offsetHeight;\n getComputedStyle(captureCanvas).opacity;\n getComputedStyle(container).opacity;\n _layoutInitializedCanvases.add(captureCanvas);\n }\n } else {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.width = Math.floor(width * dpr);\n captureCanvas.height = Math.floor(height * dpr);\n\n // Enable HTML-in-Canvas mode via layoutsubtree attribute/property\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n\n captureCanvas.appendChild(container);\n\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n document.body.appendChild(captureCanvas);\n shouldCleanup = true;\n }\n\n const t1 = performance.now();\n defaultProfiler.addTime(\"setup\", t1 - t0);\n\n try {\n // Force style calculation to ensure CSS is computed before capture\n // This ensures both canvas and container are laid out (required for drawElementImage)\n getComputedStyle(container).opacity;\n\n // When reusing canvas with layoutsubtree, wait for initial layout (first use only)\n // Use a WeakSet to track canvases that have been initialized\n if (\n reuseCanvas &&\n (captureCanvas as any).layoutSubtree &&\n !_layoutInitializedCanvases.has(captureCanvas)\n ) {\n await waitForFrame();\n _layoutInitializedCanvases.add(captureCanvas);\n\n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n\n const ctx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n ctx.drawElementImage(container, 0, 0);\n } finally {\n // Only clean up if we created the canvas\n if (shouldCleanup && captureCanvas.parentNode) {\n captureCanvas.parentNode.removeChild(captureCanvas);\n }\n }\n\n const t2 = performance.now();\n defaultProfiler.addTime(\"draw\", t2 - t1);\n\n // If DPR is 1, no downsampling needed - return as-is\n if (dpr === 1) {\n defaultProfiler.incrementRenderCount();\n return captureCanvas;\n }\n\n // Downsample to logical pixel dimensions to match foreignObject path output\n // This ensures consistent behavior regardless of which rendering path is used\n const outputCanvas = document.createElement(\"canvas\");\n outputCanvas.width = width;\n outputCanvas.height = height;\n\n const outputCtx = outputCanvas.getContext(\"2d\")!;\n // Draw the DPR-scaled capture onto the 1x output canvas\n outputCtx.drawImage(\n captureCanvas,\n 0,\n 0,\n captureCanvas.width,\n captureCanvas.height, // source (full DPR capture)\n 0,\n 0,\n width,\n height, // destination (logical pixels)\n );\n\n const t3 = performance.now();\n defaultProfiler.addTime(\"downsample\", t3 - t2);\n defaultProfiler.incrementRenderCount();\n\n return outputCanvas;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"renderToImageNative.js","names":["captureCanvas: HTMLCanvasElement","dpr"],"sources":["../../../src/preview/rendering/renderToImageNative.ts"],"sourcesContent":["/**\n * Native HTML-in-Canvas rendering using drawElementImage API.\n */\n\nimport type { HtmlInCanvasContext, HtmlInCanvasElement, NativeRenderOptions } from \"./types.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\n\n/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */\nconst _layoutInitializedCanvases = new WeakSet<HTMLCanvasElement>();\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise((resolve) => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Create a canvas element with proper DPR handling.\n * Buffer size is based on renderWidth/renderHeight (internal resolution).\n * CSS size is based on fullWidth/fullHeight (logical display size).\n */\nexport function createDprCanvas(options: {\n renderWidth: number;\n renderHeight: number;\n scale: number;\n dpr?: number;\n fullWidth: number;\n fullHeight: number;\n}): HTMLCanvasElement {\n const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;\n const dpr = options.dpr ?? window.devicePixelRatio ?? 1;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(renderWidth * scale * dpr);\n canvas.height = Math.floor(renderHeight * scale * dpr);\n canvas.style.width = `${Math.floor(fullWidth * scale)}px`;\n canvas.style.height = `${Math.floor(fullHeight * scale)}px`;\n\n return canvas;\n}\n\n/**\n * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).\n * This is much faster than the foreignObject approach and avoids canvas tainting.\n *\n * Note: The native API renders at device pixel ratio, so we capture at DPR scale\n * and then downsample to logical pixels to match the foreignObject path's output.\n *\n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options (skipWait for batch mode)\n *\n * @see https://github.com/WICG/html-in-canvas\n */\nexport async function renderToImageNative(\n container: HTMLElement,\n width: number,\n height: number,\n options: NativeRenderOptions = {},\n): Promise<HTMLCanvasElement> {\n const t0 = performance.now();\n const { reuseCanvas, skipDprScaling = false } = options;\n // Use 1x DPR when skipDprScaling is true (for video export) - 4x fewer pixels!\n const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;\n\n // Use provided canvas or create new one\n let captureCanvas: HTMLCanvasElement;\n let shouldCleanup = false;\n\n if (reuseCanvas) {\n captureCanvas = reuseCanvas;\n\n // Ensure canvas dimensions match (both attribute and CSS)\n const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;\n const targetWidth = Math.floor(width * dpr);\n const targetHeight = Math.floor(height * dpr);\n\n // Set attribute dimensions (pixel buffer size)\n if (captureCanvas.width !== targetWidth) {\n captureCanvas.width = targetWidth;\n }\n if (captureCanvas.height !== targetHeight) {\n captureCanvas.height = targetHeight;\n }\n\n // Ensure CSS dimensions and positioning (same as non-reuse path)\n // This ensures consistent behavior and avoids layout issues\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n\n // Ensure layoutsubtree is set (required for drawElementImage)\n if (!captureCanvas.hasAttribute(\"layoutsubtree\")) {\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n }\n\n // Ensure canvas is in DOM (required for drawElementImage layout)\n if (!captureCanvas.parentNode) {\n document.body.appendChild(captureCanvas);\n }\n\n // Ensure container is child of canvas\n if (container.parentElement !== captureCanvas) {\n captureCanvas.appendChild(container);\n }\n\n // Ensure container is visible (not display: none) for layout\n // drawElementImage requires the element to be laid out\n const containerStyle = getComputedStyle(container);\n if (containerStyle.display === \"none\") {\n container.style.display = \"block\";\n }\n\n // Force synchronous layout ONLY on first use with this canvas\n // For batch rendering (video export), repeated layout forces are expensive\n // We only need to force layout once to ensure everything is ready\n if (!_layoutInitializedCanvases.has(captureCanvas)) {\n void captureCanvas.offsetHeight;\n void container.offsetHeight;\n void getComputedStyle(captureCanvas).opacity;\n void getComputedStyle(container).opacity;\n _layoutInitializedCanvases.add(captureCanvas);\n }\n } else {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.width = Math.floor(width * dpr);\n captureCanvas.height = Math.floor(height * dpr);\n\n // Enable HTML-in-Canvas mode via layoutsubtree attribute/property\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n\n captureCanvas.appendChild(container);\n\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n document.body.appendChild(captureCanvas);\n shouldCleanup = true;\n }\n\n const t1 = performance.now();\n defaultProfiler.addTime(\"setup\", t1 - t0);\n\n try {\n // Force style calculation to ensure CSS is computed before capture\n // This ensures both canvas and container are laid out (required for drawElementImage)\n void getComputedStyle(container).opacity;\n\n // When reusing canvas with layoutsubtree, wait for initial layout (first use only)\n // Use a WeakSet to track canvases that have been initialized\n if (\n reuseCanvas &&\n (captureCanvas as any).layoutSubtree &&\n !_layoutInitializedCanvases.has(captureCanvas)\n ) {\n await waitForFrame();\n _layoutInitializedCanvases.add(captureCanvas);\n\n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n\n const ctx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n ctx.drawElementImage(container, 0, 0);\n } finally {\n // Only clean up if we created the canvas\n if (shouldCleanup && captureCanvas.parentNode) {\n captureCanvas.parentNode.removeChild(captureCanvas);\n }\n }\n\n const t2 = performance.now();\n defaultProfiler.addTime(\"draw\", t2 - t1);\n\n // If DPR is 1, no downsampling needed - return as-is\n if (dpr === 1) {\n defaultProfiler.incrementRenderCount();\n return captureCanvas;\n }\n\n // Downsample to logical pixel dimensions to match foreignObject path output\n // This ensures consistent behavior regardless of which rendering path is used\n const outputCanvas = document.createElement(\"canvas\");\n outputCanvas.width = width;\n outputCanvas.height = height;\n\n const outputCtx = outputCanvas.getContext(\"2d\")!;\n // Draw the DPR-scaled capture onto the 1x output canvas\n outputCtx.drawImage(\n captureCanvas,\n 0,\n 0,\n captureCanvas.width,\n captureCanvas.height, // source (full DPR capture)\n 0,\n 0,\n width,\n height, // destination (logical pixels)\n );\n\n const t3 = performance.now();\n defaultProfiler.addTime(\"downsample\", t3 - t2);\n defaultProfiler.incrementRenderCount();\n\n return outputCanvas;\n}\n"],"mappings":"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"}
@@ -372,7 +372,7 @@ function serializeImageAsCanvas(sourceElement, img, parts, canvasJobs, options)
372
372
  const ctx = canvas.getContext("2d");
373
373
  if (ctx) try {
374
374
  ctx.drawImage(img, 0, 0);
375
- } catch (e) {
375
+ } catch (_e) {
376
376
  return;
377
377
  }
378
378
  serializeCanvas(sourceElement, canvas, parts, canvasJobs, options);
@@ -1 +1 @@
1
- {"version":3,"file":"serializeTimelineDirect.js","names":["rules: string[]","styleParts: string[]","styleStr","parts: Array<string | Promise<string>>","canvasJobs: CanvasJob[]"],"sources":["../../../src/preview/rendering/serializeTimelineDirect.ts"],"sourcesContent":["/**\n * Direct timeline serialization - no intermediate passive structure.\n *\n * Walks the timeline DOM once and builds XML string directly with promise parts\n * for async canvas encoding. 3x faster than DOM creation + XMLSerializer.\n *\n * Architecture:\n * 1. Walk timeline recursively\n * 2. Build array of string parts (some are promises for canvas encoding)\n * 3. Handle shadow DOM by serializing shadow content instead of light DOM\n * 4. Await all promises\n * 5. Join parts into final XML\n */\n\nimport { encodeCanvasesInParallel } from \"../encoding/canvasEncoder.js\";\nimport type { RenderContext } from \"../RenderContext.js\";\nimport { isTemporal, isVisibleAtTime } from \"../previewTypes.js\";\nimport { ScaleConfig } from \"./ScaleConfig.js\";\n\n/**\n * Collect document styles for shadow DOM injection.\n */\nfunction collectDocumentStyles(): string {\n const rules: string[] = [];\n try {\n for (const sheet of document.styleSheets) {\n try {\n if (sheet.cssRules) {\n for (const rule of sheet.cssRules) {\n rules.push(rule.cssText);\n }\n }\n } catch {\n // Expected: cross-origin stylesheets block cssRules access\n }\n }\n } catch (e) {\n console.warn(\n \"[collectDocumentStyles] Failed to access document.styleSheets:\",\n e,\n );\n }\n return rules.join(\"\\n\");\n}\n\n/**\n * Elements to skip entirely when serializing.\n * NOTE: SLOT is NOT skipped - it's handled specially to serialize light DOM children.\n */\nconst SKIP_TAGS = new Set([\n \"EF-AUDIO\",\n \"EF-THUMBNAIL-STRIP\",\n \"EF-FILMSTRIP\",\n \"EF-TIMELINE\",\n \"EF-WORKBENCH\",\n \"SCRIPT\",\n \"STYLE\",\n \"TEMPLATE\",\n]);\n\n/**\n * HTML void elements - these cannot have children and must be self-closing in XHTML.\n * Using `<br />` instead of `<br></br>`.\n */\nconst VOID_ELEMENTS = new Set([\n \"area\",\n \"base\",\n \"br\",\n \"col\",\n \"embed\",\n \"hr\",\n \"img\",\n \"input\",\n \"link\",\n \"meta\",\n \"param\",\n \"source\",\n \"track\",\n \"wbr\",\n]);\n\n/**\n * CSS properties to serialize as inline styles.\n */\nconst SERIALIZED_STYLE_PROPERTIES = [\n \"display\",\n \"visibility\",\n \"opacity\",\n \"position\",\n \"top\",\n \"right\",\n \"bottom\",\n \"left\",\n \"zIndex\",\n \"width\",\n \"height\",\n \"minWidth\",\n \"minHeight\",\n \"maxWidth\",\n \"maxHeight\",\n \"flexGrow\",\n \"flexShrink\",\n \"flexBasis\",\n \"flexDirection\",\n \"flexWrap\",\n \"justifyContent\",\n \"alignItems\",\n \"alignContent\",\n \"alignSelf\",\n \"gap\",\n \"gridTemplate\",\n \"gridColumn\",\n \"gridRow\",\n \"gridArea\",\n \"margin\",\n \"padding\",\n \"boxSizing\",\n \"border\",\n \"borderTop\",\n \"borderRight\",\n \"borderBottom\",\n \"borderLeft\",\n \"borderRadius\",\n \"background\",\n \"color\",\n \"boxShadow\",\n \"filter\",\n \"backdropFilter\",\n \"clipPath\",\n \"fontFamily\",\n \"fontSize\",\n \"fontWeight\",\n \"fontStyle\",\n \"fontVariant\",\n \"textAlign\",\n \"textDecoration\",\n \"textShadow\",\n \"textTransform\",\n \"letterSpacing\",\n \"wordSpacing\",\n \"whiteSpace\",\n \"textOverflow\",\n \"lineHeight\",\n \"verticalAlign\",\n \"transform\",\n \"transformOrigin\",\n \"transformStyle\",\n \"perspective\",\n \"perspectiveOrigin\",\n \"backfaceVisibility\",\n \"cursor\",\n \"pointerEvents\",\n \"userSelect\",\n \"overflow\",\n \"objectFit\",\n \"objectPosition\",\n] as const;\n\n/**\n * Caption child elements that should preserve display:none.\n * These use display:none for content visibility, not temporal visibility.\n */\nconst CAPTION_CHILD_TAGS = new Set([\n \"EF-CAPTIONS-ACTIVE-WORD\",\n \"EF-CAPTIONS-BEFORE-ACTIVE-WORD\",\n \"EF-CAPTIONS-AFTER-ACTIVE-WORD\",\n \"EF-CAPTIONS-SEGMENT\",\n]);\n\ninterface SerializationOptions {\n renderContext?: RenderContext;\n canvasScale: number;\n timeMs: number;\n}\n\ninterface InternalSerializationOptions {\n renderContext?: RenderContext;\n timeMs: number;\n scaleConfig: ScaleConfig;\n sourceMap: WeakMap<HTMLCanvasElement, Element>;\n}\n\ninterface CanvasJob {\n canvas: HTMLCanvasElement;\n sourceElement: Element;\n promiseIndex: number;\n}\n\n/**\n * Escape special XML characters.\n */\nfunction escapeXML(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\n/**\n * Resolve the natural display value for an element that has display:none\n * set as an inline style (e.g., from temporal visibility via updateAnimations).\n *\n * Temporarily removes the inline display override so getComputedStyle falls\n * through to the element's stylesheet rules (including shadow DOM :host styles),\n * reads the natural value, then restores the override.\n */\nfunction resolveNaturalDisplay(element: Element): string {\n const htmlEl = element as HTMLElement;\n const inlineDisplay = htmlEl.style?.getPropertyValue(\"display\");\n if (inlineDisplay === \"none\" && htmlEl.style) {\n htmlEl.style.removeProperty(\"display\");\n const natural = getComputedStyle(element).getPropertyValue(\"display\");\n htmlEl.style.setProperty(\"display\", \"none\");\n return natural || \"block\";\n }\n return \"block\";\n}\n\n/**\n * Serialize computed styles as inline style string.\n * Handles display:none recovery for non-caption elements by resolving\n * the element's natural display value from its stylesheet rules.\n * @param element - The element to serialize styles for\n * @param styles - Optional pre-computed CSSStyleDeclaration (avoids redundant getComputedStyle calls)\n */\nfunction serializeComputedStyles(\n element: Element,\n styles?: CSSStyleDeclaration,\n): string {\n const computed = styles ?? getComputedStyle(element);\n const styleParts: string[] = [];\n const tagName = element.tagName;\n const isCaptionChild = CAPTION_CHILD_TAGS.has(tagName);\n\n // Check if the element has explicit width/height in its inline style.\n // For elements that auto-size to content (inline, inline-block text),\n // serializing the computed \"used\" pixel width/height would lock them to\n // exact dimensions that may not match the foreignObject rendering context,\n // causing text wrapping when font metrics differ slightly.\n const htmlEl = element as HTMLElement;\n const hasExplicitWidth = !!htmlEl.style?.getPropertyValue(\"width\");\n const hasExplicitHeight = !!htmlEl.style?.getPropertyValue(\"height\");\n\n for (const prop of SERIALIZED_STYLE_PROPERTIES) {\n // Convert camelCase to kebab-case first\n const kebab = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n const value = computed.getPropertyValue(kebab);\n\n // Skip only truly empty values\n if (!value || value === \"\") {\n continue;\n }\n\n // Handle display property specially\n let finalValue = value;\n if (prop === \"display\") {\n // For non-caption elements, recover the natural display value when display:none\n // was set by the temporal visibility system (updateAnimations). This prevents\n // inline elements (like ef-text-segment) from being serialized as display:block.\n if (value === \"none\" && !isCaptionChild) {\n finalValue = resolveNaturalDisplay(element);\n }\n }\n\n // Force visibility:visible - the source container may have visibility:hidden\n // for off-screen rendering, but we want the serialized output to be visible\n if (prop === \"visibility\") {\n finalValue = \"visible\";\n }\n\n // Skip the proxy-mode sentinel value: inset(100%) hides the element\n // off-screen while keeping it in the layout. All other clip-path values\n // are legitimate visual effects and must be preserved.\n if (prop === \"clipPath\" && value === \"inset(100%)\") {\n continue;\n }\n\n // Skip width/height when not explicitly set on the element.\n // getComputedStyle returns \"used\" pixel values for width/height even when\n // the specified value is auto. Serializing these pixel values locks\n // content-sized elements (text segments, inline-block spans) to exact\n // dimensions, which breaks when the foreignObject context renders text\n // with different font metrics.\n if (prop === \"width\" && !hasExplicitWidth) {\n continue;\n }\n if (prop === \"height\" && !hasExplicitHeight) {\n continue;\n }\n\n styleParts.push(`${kebab}:${finalValue}`);\n }\n\n // Disable animations/transitions to prevent re-animation\n styleParts.push(\"animation:none\", \"transition:none\");\n\n return styleParts.join(\";\");\n}\n\n/**\n * Serialize element attributes (excluding style, id, xmlns, event handlers).\n */\nfunction serializeAttributes(\n element: Element,\n parts: Array<string | Promise<string>>,\n): void {\n for (const attr of element.attributes) {\n const name = attr.name.toLowerCase();\n // Skip: id, style, xmlns (namespace handled separately), event handlers\n if (\n name === \"id\" ||\n name === \"style\" ||\n name === \"xmlns\" ||\n name.startsWith(\"on\")\n ) {\n continue;\n }\n parts.push(` ${attr.name}=\"${escapeXML(attr.value)}\"`);\n }\n}\n\n/**\n * Check if a canvas element should preserve alpha channel.\n * EF-WAVEFORM always needs alpha, EF-IMAGE checks hasAlpha property.\n * EF-SURFACE needs alpha because:\n * 1. Without a target, its canvas is transparent and CSS background should show through\n * 2. The target element may have transparent content\n * Raw canvas elements must preserve alpha - we don't know what they contain.\n */\nfunction shouldPreserveAlpha(sourceElement: Element): boolean {\n const tagName = sourceElement.tagName;\n if (tagName === \"EF-WAVEFORM\") {\n return true;\n }\n if (tagName === \"EF-SURFACE\") {\n // Surface needs alpha to allow CSS background to show through empty areas\n return true;\n }\n if (tagName === \"EF-IMAGE\") {\n return (\n \"hasAlpha\" in sourceElement && (sourceElement as any).hasAlpha === true\n );\n }\n // Raw canvas elements must preserve alpha\n if (sourceElement instanceof HTMLCanvasElement) {\n return true;\n }\n return false;\n}\n\n/**\n * Find the capture proxy canvas for an offscreen-rendered canvas.\n * When a canvas is transferred to offscreen via transferControlToOffscreen(),\n * the main thread can no longer read pixels from it. OffscreenCompositionCanvas\n * creates a hidden capture canvas (marked with data-offscreen-capture) that\n * receives ImageBitmap frames from the worker.\n */\nfunction findCaptureProxy(canvas: HTMLCanvasElement): HTMLCanvasElement | null {\n const container = canvas.parentElement;\n if (!container) return null;\n return container.querySelector('canvas[data-offscreen-capture=\"true\"]');\n}\n\n/**\n * Read pixels directly from a WebGL canvas's drawing buffer via gl.readPixels().\n *\n * drawImage(webglCanvas) reads from the compositor's \"presented\" surface, which\n * is only refreshed during requestAnimationFrame / compositing cycles. In hidden\n * browser tabs, compositing is suspended, so drawImage returns stale pixels even\n * though gl.render() produced new content in the drawing buffer.\n *\n * readPixels() reads from the drawing buffer directly, bypassing the compositor.\n *\n * Returns null for non-WebGL canvases (getContext returns null when a different\n * context type is already active).\n */\nfunction readWebGLPixels(canvas: HTMLCanvasElement): Uint8ClampedArray | null {\n const gl = (canvas.getContext(\"webgl2\") ??\n canvas.getContext(\"webgl\")) as WebGLRenderingContext | null;\n if (!gl) return null;\n\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return null;\n\n // Ensure we read from the drawing buffer, not a leftover FBO\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n\n const pixels = new Uint8Array(width * height * 4);\n gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);\n\n // readPixels returns rows bottom-to-top; flip to top-to-bottom for ImageData\n const rowSize = width * 4;\n const halfHeight = Math.floor(height / 2);\n const temp = new Uint8Array(rowSize);\n for (let y = 0; y < halfHeight; y++) {\n const topOffset = y * rowSize;\n const bottomOffset = (height - 1 - y) * rowSize;\n temp.set(pixels.subarray(topOffset, topOffset + rowSize));\n pixels.set(\n pixels.subarray(bottomOffset, bottomOffset + rowSize),\n topOffset,\n );\n pixels.set(temp, bottomOffset);\n }\n\n return new Uint8ClampedArray(pixels.buffer);\n}\n\n/**\n * Create a snapshot copy of a canvas's current pixels.\n * This captures the pixels synchronously before any async encoding,\n * preventing race conditions where the source canvas is modified.\n *\n * For WebGL canvases, uses gl.readPixels() to bypass the compositor's\n * presentation layer (which is suspended in hidden browser tabs).\n *\n * For offscreen-rendered canvases, this automatically uses the capture proxy\n * canvas instead of the transferred display canvas.\n */\nfunction snapshotCanvas(\n canvas: HTMLCanvasElement,\n scale: number,\n preserveAlpha: boolean,\n): HTMLCanvasElement {\n // If this canvas was transferred to offscreen, use its capture proxy\n const captureProxy = findCaptureProxy(canvas);\n const sourceCanvas = captureProxy ?? canvas;\n\n const targetWidth = Math.max(1, Math.floor(sourceCanvas.width * scale));\n const targetHeight = Math.max(1, Math.floor(sourceCanvas.height * scale));\n\n const copy = document.createElement(\"canvas\");\n copy.width = targetWidth;\n copy.height = targetHeight;\n\n if (preserveAlpha) {\n copy.dataset.preserveAlpha = \"true\";\n }\n\n const ctx = copy.getContext(\"2d\");\n if (ctx && sourceCanvas.width > 0 && sourceCanvas.height > 0) {\n // Try reading directly from WebGL drawing buffer (bypasses compositor)\n // Only needed when page is hidden - compositor is suspended in hidden tabs\n const useGlBypass = document.hidden;\n const glPixels = useGlBypass ? readWebGLPixels(sourceCanvas) : null;\n if (glPixels) {\n const srcW = sourceCanvas.width;\n const srcH = sourceCanvas.height;\n const imageData = new ImageData(\n glPixels as unknown as Uint8ClampedArray<ArrayBuffer>,\n srcW,\n srcH,\n );\n\n if (targetWidth === srcW && targetHeight === srcH) {\n ctx.putImageData(imageData, 0, 0);\n } else {\n // putImageData doesn't scale — bounce through a temp canvas\n const temp = document.createElement(\"canvas\");\n temp.width = srcW;\n temp.height = srcH;\n temp.getContext(\"2d\")!.putImageData(imageData, 0, 0);\n ctx.drawImage(temp, 0, 0, targetWidth, targetHeight);\n }\n } else {\n // Non-WebGL canvas: drawImage is synchronous and correct\n ctx.drawImage(sourceCanvas, 0, 0, targetWidth, targetHeight);\n }\n }\n\n return copy;\n}\n\n/**\n * Serialize a canvas element as an <img> with base64 data URL.\n * Creates a snapshot of current pixels before async encoding to prevent race conditions.\n *\n * OPTIMIZATION: Calculate optimal encoding resolution based on:\n * 1. CSS display size (how big it actually appears)\n * 2. Video export scale (output resolution multiplier)\n * 3. Quality multiplier (for sharpness, default 1.5x)\n */\nfunction serializeCanvas(\n sourceElement: Element,\n canvas: HTMLCanvasElement,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n): void {\n // If this canvas was transferred to offscreen, use its capture proxy\n const captureProxy = findCaptureProxy(canvas);\n const sourceCanvas = captureProxy ?? canvas;\n\n // Use intrinsic canvas dimensions, not computed styles (which may be zoom-affected)\n const width = sourceCanvas.width;\n const height = sourceCanvas.height;\n\n // Skip empty canvases\n if (width === 0 || height === 0) {\n return;\n }\n\n // Get computed style once and reuse\n const computedStyle = getComputedStyle(sourceElement);\n const styleStr = serializeComputedStyles(sourceElement, computedStyle);\n\n // Get computed dimensions from source element (respects CSS like w-[420px])\n const computedWidth = computedStyle.width;\n const computedHeight = computedStyle.height;\n\n // Preserve the source element's object-fit and object-position for correct scaling.\n // These CSS properties control how the canvas content fits its container and must be\n // carried through to the serialized <img> to maintain visual fidelity.\n const styleParts = styleStr\n ? styleStr.split(\";\").filter((s) => s.trim())\n : [];\n\n // Remove width/height from computed styles (we'll set them explicitly from computed dimensions)\n const filteredParts = styleParts.filter((s) => {\n const trimmed = s.trim();\n return !trimmed.startsWith(\"width:\") && !trimmed.startsWith(\"height:\");\n });\n\n // Use host element dimensions if available, otherwise fall back to canvas natural dimensions\n const displayWidth = computedWidth || `${width}px`;\n const displayHeight = computedHeight || `${height}px`;\n\n filteredParts.push(`width:${displayWidth}`);\n filteredParts.push(`height:${displayHeight}`);\n filteredParts.push(`display:block`);\n\n const finalStyle = filteredParts.join(\";\");\n\n // Check if we need to preserve alpha channel\n const preserveAlpha = shouldPreserveAlpha(sourceElement);\n\n // CRITICAL: Calculate optimal encoding scale BEFORE creating snapshot.\n // This prevents encoding at full resolution when CSS display size is much smaller.\n let optimalScale = options.scaleConfig.exportScale; // Start with export scale as fallback\n\n try {\n const cssWidth = parseFloat(computedWidth) || sourceCanvas.width;\n const cssHeight = parseFloat(computedHeight) || sourceCanvas.height;\n\n // Use ScaleConfig to compute optimal canvas scale\n optimalScale = options.scaleConfig.computeCanvasScale({\n naturalWidth: sourceCanvas.width,\n naturalHeight: sourceCanvas.height,\n displayWidth: cssWidth,\n displayHeight: cssHeight,\n });\n } catch (e) {\n // Fallback to export scale if we can't get computed style\n console.warn(\n `[serializeCanvas] Failed to get computed style for ${sourceElement.tagName}:`,\n e,\n );\n }\n\n // CRITICAL: Create a snapshot of canvas pixels SYNCHRONOUSLY before any async work.\n // This prevents race conditions where concurrent renders overwrite the shared\n // shadow canvas while encoding is in progress.\n // Note: snapshotCanvas already handles finding the capture proxy internally\n const snapshot = snapshotCanvas(canvas, optimalScale, preserveAlpha);\n\n // Open img tag with all styles from source element\n parts.push(`<img style=\"${escapeXML(finalStyle)}\" src=\"`);\n\n // Kick off async encoding of the SNAPSHOT (not the live canvas)\n const promiseIndex = parts.length;\n options.sourceMap.set(snapshot, sourceElement);\n\n // Snapshot is already scaled, so encode at 1.0 scale\n const encodePromise = encodeCanvasesInParallel([snapshot], {\n scale: 1.0,\n renderContext: options.renderContext,\n sourceMap: options.sourceMap,\n }).then((results) => results[0]?.dataUrl || \"\");\n\n parts.push(encodePromise);\n canvasJobs.push({ canvas: snapshot, sourceElement, promiseIndex });\n\n // Close img tag\n parts.push('\" />');\n}\n\n/**\n * Serialize an image element as a canvas (for shadow DOM img elements).\n */\nfunction serializeImageAsCanvas(\n sourceElement: Element,\n img: HTMLImageElement,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n): void {\n // Convert img to canvas for serialization\n const canvas = document.createElement(\"canvas\");\n canvas.width = img.naturalWidth;\n canvas.height = img.naturalHeight;\n\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n try {\n ctx.drawImage(img, 0, 0);\n } catch (e) {\n // Cross-origin image - skip\n return;\n }\n }\n\n serializeCanvas(sourceElement, canvas, parts, canvasJobs, options);\n}\n\n/**\n * Serialize slotted light DOM children of a host element.\n */\nfunction serializeSlottedContent(\n slotHost: Element,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n parentIsSVG: boolean,\n): void {\n for (const slottedChild of slotHost.childNodes) {\n if (slottedChild.nodeType === Node.TEXT_NODE) {\n const text = slottedChild.textContent;\n if (text && text.length > 0) {\n parts.push(escapeXML(text));\n }\n } else if (slottedChild.nodeType === Node.ELEMENT_NODE) {\n serializeElement(\n slottedChild as Element,\n parts,\n canvasJobs,\n options,\n parentIsSVG,\n null,\n );\n }\n }\n}\n\n/**\n * Recursively serialize an element and its children to XML parts.\n * @param slotHost - When serializing inside shadow DOM, the custom element whose light DOM children should be serialized for slots\n */\nfunction serializeElement(\n element: Element,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n parentIsSVG = false,\n slotHost: Element | null = null,\n): void {\n // Skip certain elements\n if (SKIP_TAGS.has(element.tagName)) {\n return;\n }\n\n // Handle SLOT elements - serialize light DOM children of the slot host\n if (element.tagName === \"SLOT\" && slotHost) {\n serializeSlottedContent(slotHost, parts, canvasJobs, options, parentIsSVG);\n return;\n }\n\n // Check temporal visibility - skip elements outside their time bounds\n // This is non-destructive (doesn't modify DOM)\n // NOTE: Ancestor checking is unnecessary - serializeElement walks top-down,\n // so if a parent is temporally invisible, its children are never visited\n if (!isVisibleAtTime(element, options.timeMs)) {\n return;\n }\n\n // Respect updateAnimations' visibility decision for temporal elements.\n // isVisibleAtTime uses inclusive end bounds, but updateAnimations uses\n // exclusive end for mid-composition elements (VisibilityPolicy). When\n // updateAnimations has set display:none, that is the authoritative decision.\n if (\n isTemporal(element) &&\n (element as HTMLElement).style?.getPropertyValue(\"display\") === \"none\"\n ) {\n return;\n }\n\n // Custom element with shadow DOM?\n const isCustom = element.tagName.includes(\"-\");\n if (isCustom && element.shadowRoot) {\n const shadowCanvas = element.shadowRoot.querySelector(\"canvas\");\n if (shadowCanvas) {\n serializeCanvas(element, shadowCanvas, parts, canvasJobs, options);\n return;\n }\n\n const shadowImg = element.shadowRoot.querySelector(\"img\");\n if (shadowImg?.complete && shadowImg.naturalWidth > 0) {\n serializeImageAsCanvas(element, shadowImg, parts, canvasJobs, options);\n return;\n }\n\n // Serialize custom element with its styles, then shadow DOM content inside\n // Use span for inline/inline-block/inline-flex elements to preserve inline behavior\n const computedStyle = getComputedStyle(element);\n let computedDisplay = computedStyle.display;\n // If display:none was set by temporal visibility, resolve the natural display\n // to determine the correct container tag (span vs div)\n if (computedDisplay === \"none\") {\n computedDisplay = resolveNaturalDisplay(element);\n }\n const isInline =\n computedDisplay === \"inline\" ||\n computedDisplay === \"inline-block\" ||\n computedDisplay === \"inline-flex\";\n const containerTag = isInline ? \"span\" : \"div\";\n\n let styleStr = serializeComputedStyles(element, computedStyle);\n\n parts.push(`<${containerTag}`);\n\n // Copy data attributes and class from custom element\n for (const attr of element.attributes) {\n const name = attr.name.toLowerCase();\n if (name === \"class\" || name.startsWith(\"data-\")) {\n parts.push(` ${attr.name}=\"${escapeXML(attr.value)}\"`);\n }\n }\n\n if (styleStr) {\n parts.push(` style=\"${escapeXML(styleStr)}\"`);\n }\n parts.push(\">\");\n\n // Serialize shadow DOM content with this element as the slot host\n for (const child of element.shadowRoot.childNodes) {\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent;\n if (text && text.length > 0) {\n parts.push(escapeXML(text));\n }\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n // Pass this element as slotHost so nested SLOTs can access light DOM children\n serializeElement(\n child as Element,\n parts,\n canvasJobs,\n options,\n parentIsSVG,\n element,\n );\n }\n }\n\n parts.push(`</${containerTag}>`);\n return;\n }\n\n // Raw canvas in light DOM\n if (element instanceof HTMLCanvasElement) {\n serializeCanvas(element, element, parts, canvasJobs, options);\n return;\n }\n\n // Standard element - serialize to XHTML\n const tagName = element.tagName.toLowerCase();\n const isSVG = element instanceof SVGElement;\n const isVoid = VOID_ELEMENTS.has(tagName);\n\n // Open tag with namespace (only add xmlns for root SVG elements, not children)\n if (isSVG && !parentIsSVG) {\n // Root SVG element - needs xmlns declaration\n parts.push(`<${tagName} xmlns=\"http://www.w3.org/2000/svg\"`);\n } else {\n parts.push(`<${tagName}`);\n }\n\n // Attributes\n serializeAttributes(element, parts);\n\n // Computed styles as inline style attribute\n const styleStr = serializeComputedStyles(element);\n if (styleStr) {\n parts.push(` style=\"${escapeXML(styleStr)}\"`);\n }\n\n // Void elements: self-close with /> (XHTML requirement)\n if (isVoid) {\n parts.push(\" />\");\n return;\n }\n\n parts.push(\">\");\n\n // Children (shadow or light)\n const children = element.shadowRoot?.childNodes || element.childNodes;\n for (const child of children) {\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent;\n if (text && text.length > 0) {\n parts.push(escapeXML(text));\n }\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n // Preserve slotHost when recursing into standard elements inside shadow DOM\n serializeElement(\n child as Element,\n parts,\n canvasJobs,\n options,\n isSVG,\n slotHost,\n );\n }\n }\n\n // Close tag\n parts.push(`</${tagName}>`);\n}\n\n/**\n * TextEncoder instance for SVG-to-base64 encoding.\n * encode() converts to UTF-8 bytes in a single native call, then we\n * base64-encode the bytes. ~33% overhead vs ~200% for percent-encoding.\n */\nconst textEncoder = new TextEncoder();\n\n/**\n * Synchronous DOM capture phase. Walks the element tree, snapshots canvas\n * pixels, and kicks off async encoding. Returns parts array containing\n * string fragments and encoding promises.\n *\n * After this function returns, the source element's DOM is no longer\n * referenced — the clone can safely be seeked to the next frame.\n *\n * SCALING ARCHITECTURE (unified via ScaleConfig):\n *\n * ScaleConfig centralizes all scaling logic and provides:\n * 1. Output SVG dimensions (width * exportScale, height * exportScale)\n * 2. DOM scaling wrapper (CSS transform:scale when exportScale < 1)\n * 3. Per-canvas optimal encoding scale via computeCanvasScale()\n *\n * Canvas scaling is independent from DOM scaling because:\n * - Canvas elements have intrinsic pixel dimensions and can be downsampled\n * efficiently before encoding (prevents encoding 1920px at full resolution\n * when displayed at 420px)\n * - DOM content has no intrinsic resolution and must be scaled via CSS\n * transforms, which the browser handles during SVG foreignObject rendering\n *\n * Example: 1920x1080 @ 0.5 export scale\n * - Output SVG: 960x540\n * - DOM wrapper: transform:scale(0.5) on 1920x1080 content\n * - Canvas (1920px displayed at 420px): encoded at ~0.16x (315px)\n * via computeCanvasScale(420/1920 * 0.5 * 1.5 quality = 0.164)\n */\nexport function captureElementParts(\n element: Element,\n width: number,\n height: number,\n options: SerializationOptions,\n): Array<string | Promise<string>> {\n const parts: Array<string | Promise<string>> = [];\n const canvasJobs: CanvasJob[] = [];\n const sourceMap = new WeakMap<HTMLCanvasElement, Element>();\n\n // Create ScaleConfig to centralize all scaling logic\n const scaleConfig = ScaleConfig.fromOptions(\n width,\n height,\n options.canvasScale,\n );\n\n const documentStyles =\n options.renderContext?.getCachedDocumentStyles() ?? collectDocumentStyles();\n if (options.renderContext && documentStyles) {\n options.renderContext.setCachedDocumentStyles(documentStyles);\n }\n\n parts.push(\n `<div xmlns=\"http://www.w3.org/1999/xhtml\" ` +\n `style=\"width:${scaleConfig.outputWidth}px;height:${scaleConfig.outputHeight}px;overflow:hidden;position:relative;\">`,\n );\n\n if (documentStyles) {\n parts.push(`<style type=\"text/css\"><![CDATA[${documentStyles}]]></style>`);\n }\n\n // Apply DOM scaling wrapper if needed\n const domTransform = scaleConfig.getDOMTransform();\n if (domTransform) {\n const wrapperDims = scaleConfig.getDOMWrapperDimensions();\n parts.push(\n `<div style=\"transform:${domTransform};transform-origin:0 0;` +\n `width:${wrapperDims.width}px;height:${wrapperDims.height}px;\">`,\n );\n }\n\n // Create internal options with ScaleConfig\n const internalOptions: InternalSerializationOptions = {\n renderContext: options.renderContext,\n timeMs: options.timeMs,\n scaleConfig,\n sourceMap,\n };\n\n serializeElement(element, parts, canvasJobs, internalOptions);\n\n if (domTransform) {\n parts.push(\"</div>\");\n }\n\n parts.push(\"</div>\");\n\n return parts;\n}\n\n/**\n * Serialize any element directly to XHTML string.\n *\n * @param element - The element to serialize (timegroup, temporal element, or plain DOM)\n * @param width - Output width\n * @param height - Output height\n * @param options - Serialization options (renderContext, canvasScale, timeMs)\n * @returns XHTML string with all canvases encoded as base64 data URLs\n */\nexport async function serializeElementToXHTML(\n element: Element,\n width: number,\n height: number,\n options: SerializationOptions,\n): Promise<string> {\n const parts = captureElementParts(element, width, height, options);\n const resolvedParts = await Promise.all(parts);\n return resolvedParts.join(\"\");\n}\n\n/**\n * Synchronous capture with deferred data URI encoding.\n *\n * Walks the DOM and snapshots canvas pixels synchronously, then returns\n * a promise that resolves to the SVG data URI once async canvas-to-base64\n * encoding completes. The source element is NOT referenced after this\n * function returns — the caller can immediately mutate/seek the clone.\n */\nexport function captureTimelineToDataUri(\n element: Element,\n width: number,\n height: number,\n options: SerializationOptions,\n): Promise<string> {\n // Create ScaleConfig to compute scaled dimensions\n const scaleConfig = ScaleConfig.fromOptions(\n width,\n height,\n options.canvasScale,\n );\n\n const parts = captureElementParts(element, width, height, options);\n\n return Promise.all(parts).then((resolvedParts) => {\n const xhtml = resolvedParts.join(\"\");\n const svg =\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${scaleConfig.outputWidth}\" height=\"${scaleConfig.outputHeight}\">` +\n `<foreignObject x=\"0\" y=\"0\" width=\"${scaleConfig.outputWidth}\" height=\"${scaleConfig.outputHeight}\">${xhtml}</foreignObject>` +\n `</svg>`;\n // Encode SVG to base64 data URI inline (avoids module-level function reference issues)\n const bytes = textEncoder.encode(svg);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i += 8192) {\n binary += String.fromCharCode.apply(\n null,\n bytes.subarray(i, i + 8192) as unknown as number[],\n );\n }\n return `data:image/svg+xml;base64,${btoa(binary)}`;\n });\n}\n"],"mappings":";;;;;;;;AAsBA,SAAS,wBAAgC;CACvC,MAAMA,QAAkB,EAAE;AAC1B,KAAI;AACF,OAAK,MAAM,SAAS,SAAS,YAC3B,KAAI;AACF,OAAI,MAAM,SACR,MAAK,MAAM,QAAQ,MAAM,SACvB,OAAM,KAAK,KAAK,QAAQ;UAGtB;UAIH,GAAG;AACV,UAAQ,KACN,kEACA,EACD;;AAEH,QAAO,MAAM,KAAK,KAAK;;;;;;AAOzB,MAAM,YAAY,IAAI,IAAI;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;AAMF,MAAM,gBAAgB,IAAI,IAAI;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;AAKF,MAAM,8BAA8B;CAClC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;AAMD,MAAM,qBAAqB,IAAI,IAAI;CACjC;CACA;CACA;CACA;CACD,CAAC;;;;AAwBF,SAAS,UAAU,KAAqB;AACtC,QAAO,IACJ,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,SAAS,CACvB,QAAQ,MAAM,SAAS;;;;;;;;;;AAW5B,SAAS,sBAAsB,SAA0B;CACvD,MAAM,SAAS;AAEf,KADsB,OAAO,OAAO,iBAAiB,UAAU,KACzC,UAAU,OAAO,OAAO;AAC5C,SAAO,MAAM,eAAe,UAAU;EACtC,MAAM,UAAU,iBAAiB,QAAQ,CAAC,iBAAiB,UAAU;AACrE,SAAO,MAAM,YAAY,WAAW,OAAO;AAC3C,SAAO,WAAW;;AAEpB,QAAO;;;;;;;;;AAUT,SAAS,wBACP,SACA,QACQ;CACR,MAAM,WAAW,UAAU,iBAAiB,QAAQ;CACpD,MAAMC,aAAuB,EAAE;CAC/B,MAAM,UAAU,QAAQ;CACxB,MAAM,iBAAiB,mBAAmB,IAAI,QAAQ;CAOtD,MAAM,SAAS;CACf,MAAM,mBAAmB,CAAC,CAAC,OAAO,OAAO,iBAAiB,QAAQ;CAClE,MAAM,oBAAoB,CAAC,CAAC,OAAO,OAAO,iBAAiB,SAAS;AAEpE,MAAK,MAAM,QAAQ,6BAA6B;EAE9C,MAAM,QAAQ,KAAK,QAAQ,WAAW,MAAM,IAAI,EAAE,aAAa,GAAG;EAClE,MAAM,QAAQ,SAAS,iBAAiB,MAAM;AAG9C,MAAI,CAAC,SAAS,UAAU,GACtB;EAIF,IAAI,aAAa;AACjB,MAAI,SAAS,WAIX;OAAI,UAAU,UAAU,CAAC,eACvB,cAAa,sBAAsB,QAAQ;;AAM/C,MAAI,SAAS,aACX,cAAa;AAMf,MAAI,SAAS,cAAc,UAAU,cACnC;AASF,MAAI,SAAS,WAAW,CAAC,iBACvB;AAEF,MAAI,SAAS,YAAY,CAAC,kBACxB;AAGF,aAAW,KAAK,GAAG,MAAM,GAAG,aAAa;;AAI3C,YAAW,KAAK,kBAAkB,kBAAkB;AAEpD,QAAO,WAAW,KAAK,IAAI;;;;;AAM7B,SAAS,oBACP,SACA,OACM;AACN,MAAK,MAAM,QAAQ,QAAQ,YAAY;EACrC,MAAM,OAAO,KAAK,KAAK,aAAa;AAEpC,MACE,SAAS,QACT,SAAS,WACT,SAAS,WACT,KAAK,WAAW,KAAK,CAErB;AAEF,QAAM,KAAK,IAAI,KAAK,KAAK,IAAI,UAAU,KAAK,MAAM,CAAC,GAAG;;;;;;;;;;;AAY1D,SAAS,oBAAoB,eAAiC;CAC5D,MAAM,UAAU,cAAc;AAC9B,KAAI,YAAY,cACd,QAAO;AAET,KAAI,YAAY,aAEd,QAAO;AAET,KAAI,YAAY,WACd,QACE,cAAc,iBAAkB,cAAsB,aAAa;AAIvE,KAAI,yBAAyB,kBAC3B,QAAO;AAET,QAAO;;;;;;;;;AAUT,SAAS,iBAAiB,QAAqD;CAC7E,MAAM,YAAY,OAAO;AACzB,KAAI,CAAC,UAAW,QAAO;AACvB,QAAO,UAAU,cAAc,0CAAwC;;;;;;;;;;;;;;;AAgBzE,SAAS,gBAAgB,QAAqD;CAC5E,MAAM,KAAM,OAAO,WAAW,SAAS,IACrC,OAAO,WAAW,QAAQ;AAC5B,KAAI,CAAC,GAAI,QAAO;CAEhB,MAAM,QAAQ,OAAO;CACrB,MAAM,SAAS,OAAO;AACtB,KAAI,UAAU,KAAK,WAAW,EAAG,QAAO;AAGxC,IAAG,gBAAgB,GAAG,aAAa,KAAK;CAExC,MAAM,SAAS,IAAI,WAAW,QAAQ,SAAS,EAAE;AACjD,IAAG,WAAW,GAAG,GAAG,OAAO,QAAQ,GAAG,MAAM,GAAG,eAAe,OAAO;CAGrE,MAAM,UAAU,QAAQ;CACxB,MAAM,aAAa,KAAK,MAAM,SAAS,EAAE;CACzC,MAAM,OAAO,IAAI,WAAW,QAAQ;AACpC,MAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAAK;EACnC,MAAM,YAAY,IAAI;EACtB,MAAM,gBAAgB,SAAS,IAAI,KAAK;AACxC,OAAK,IAAI,OAAO,SAAS,WAAW,YAAY,QAAQ,CAAC;AACzD,SAAO,IACL,OAAO,SAAS,cAAc,eAAe,QAAQ,EACrD,UACD;AACD,SAAO,IAAI,MAAM,aAAa;;AAGhC,QAAO,IAAI,kBAAkB,OAAO,OAAO;;;;;;;;;;;;;AAc7C,SAAS,eACP,QACA,OACA,eACmB;CAGnB,MAAM,eADe,iBAAiB,OAAO,IACR;CAErC,MAAM,cAAc,KAAK,IAAI,GAAG,KAAK,MAAM,aAAa,QAAQ,MAAM,CAAC;CACvE,MAAM,eAAe,KAAK,IAAI,GAAG,KAAK,MAAM,aAAa,SAAS,MAAM,CAAC;CAEzE,MAAM,OAAO,SAAS,cAAc,SAAS;AAC7C,MAAK,QAAQ;AACb,MAAK,SAAS;AAEd,KAAI,cACF,MAAK,QAAQ,gBAAgB;CAG/B,MAAM,MAAM,KAAK,WAAW,KAAK;AACjC,KAAI,OAAO,aAAa,QAAQ,KAAK,aAAa,SAAS,GAAG;EAI5D,MAAM,WADc,SAAS,SACE,gBAAgB,aAAa,GAAG;AAC/D,MAAI,UAAU;GACZ,MAAM,OAAO,aAAa;GAC1B,MAAM,OAAO,aAAa;GAC1B,MAAM,YAAY,IAAI,UACpB,UACA,MACA,KACD;AAED,OAAI,gBAAgB,QAAQ,iBAAiB,KAC3C,KAAI,aAAa,WAAW,GAAG,EAAE;QAC5B;IAEL,MAAM,OAAO,SAAS,cAAc,SAAS;AAC7C,SAAK,QAAQ;AACb,SAAK,SAAS;AACd,SAAK,WAAW,KAAK,CAAE,aAAa,WAAW,GAAG,EAAE;AACpD,QAAI,UAAU,MAAM,GAAG,GAAG,aAAa,aAAa;;QAItD,KAAI,UAAU,cAAc,GAAG,GAAG,aAAa,aAAa;;AAIhE,QAAO;;;;;;;;;;;AAYT,SAAS,gBACP,eACA,QACA,OACA,YACA,SACM;CAGN,MAAM,eADe,iBAAiB,OAAO,IACR;CAGrC,MAAM,QAAQ,aAAa;CAC3B,MAAM,SAAS,aAAa;AAG5B,KAAI,UAAU,KAAK,WAAW,EAC5B;CAIF,MAAM,gBAAgB,iBAAiB,cAAc;CACrD,MAAM,WAAW,wBAAwB,eAAe,cAAc;CAGtE,MAAM,gBAAgB,cAAc;CACpC,MAAM,iBAAiB,cAAc;CAUrC,MAAM,iBALa,WACf,SAAS,MAAM,IAAI,CAAC,QAAQ,MAAM,EAAE,MAAM,CAAC,GAC3C,EAAE,EAG2B,QAAQ,MAAM;EAC7C,MAAM,UAAU,EAAE,MAAM;AACxB,SAAO,CAAC,QAAQ,WAAW,SAAS,IAAI,CAAC,QAAQ,WAAW,UAAU;GACtE;CAGF,MAAM,eAAe,iBAAiB,GAAG,MAAM;CAC/C,MAAM,gBAAgB,kBAAkB,GAAG,OAAO;AAElD,eAAc,KAAK,SAAS,eAAe;AAC3C,eAAc,KAAK,UAAU,gBAAgB;AAC7C,eAAc,KAAK,gBAAgB;CAEnC,MAAM,aAAa,cAAc,KAAK,IAAI;CAG1C,MAAM,gBAAgB,oBAAoB,cAAc;CAIxD,IAAI,eAAe,QAAQ,YAAY;AAEvC,KAAI;EACF,MAAM,WAAW,WAAW,cAAc,IAAI,aAAa;EAC3D,MAAM,YAAY,WAAW,eAAe,IAAI,aAAa;AAG7D,iBAAe,QAAQ,YAAY,mBAAmB;GACpD,cAAc,aAAa;GAC3B,eAAe,aAAa;GAC5B,cAAc;GACd,eAAe;GAChB,CAAC;UACK,GAAG;AAEV,UAAQ,KACN,sDAAsD,cAAc,QAAQ,IAC5E,EACD;;CAOH,MAAM,WAAW,eAAe,QAAQ,cAAc,cAAc;AAGpE,OAAM,KAAK,eAAe,UAAU,WAAW,CAAC,SAAS;CAGzD,MAAM,eAAe,MAAM;AAC3B,SAAQ,UAAU,IAAI,UAAU,cAAc;CAG9C,MAAM,gBAAgB,yBAAyB,CAAC,SAAS,EAAE;EACzD,OAAO;EACP,eAAe,QAAQ;EACvB,WAAW,QAAQ;EACpB,CAAC,CAAC,MAAM,YAAY,QAAQ,IAAI,WAAW,GAAG;AAE/C,OAAM,KAAK,cAAc;AACzB,YAAW,KAAK;EAAE,QAAQ;EAAU;EAAe;EAAc,CAAC;AAGlE,OAAM,KAAK,QAAO;;;;;AAMpB,SAAS,uBACP,eACA,KACA,OACA,YACA,SACM;CAEN,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,QAAO,QAAQ,IAAI;AACnB,QAAO,SAAS,IAAI;CAEpB,MAAM,MAAM,OAAO,WAAW,KAAK;AACnC,KAAI,IACF,KAAI;AACF,MAAI,UAAU,KAAK,GAAG,EAAE;UACjB,GAAG;AAEV;;AAIJ,iBAAgB,eAAe,QAAQ,OAAO,YAAY,QAAQ;;;;;AAMpE,SAAS,wBACP,UACA,OACA,YACA,SACA,aACM;AACN,MAAK,MAAM,gBAAgB,SAAS,WAClC,KAAI,aAAa,aAAa,KAAK,WAAW;EAC5C,MAAM,OAAO,aAAa;AAC1B,MAAI,QAAQ,KAAK,SAAS,EACxB,OAAM,KAAK,UAAU,KAAK,CAAC;YAEpB,aAAa,aAAa,KAAK,aACxC,kBACE,cACA,OACA,YACA,SACA,aACA,KACD;;;;;;AASP,SAAS,iBACP,SACA,OACA,YACA,SACA,cAAc,OACd,WAA2B,MACrB;AAEN,KAAI,UAAU,IAAI,QAAQ,QAAQ,CAChC;AAIF,KAAI,QAAQ,YAAY,UAAU,UAAU;AAC1C,0BAAwB,UAAU,OAAO,YAAY,SAAS,YAAY;AAC1E;;AAOF,KAAI,CAAC,gBAAgB,SAAS,QAAQ,OAAO,CAC3C;AAOF,KACE,WAAW,QAAQ,IAClB,QAAwB,OAAO,iBAAiB,UAAU,KAAK,OAEhE;AAKF,KADiB,QAAQ,QAAQ,SAAS,IAAI,IAC9B,QAAQ,YAAY;EAClC,MAAM,eAAe,QAAQ,WAAW,cAAc,SAAS;AAC/D,MAAI,cAAc;AAChB,mBAAgB,SAAS,cAAc,OAAO,YAAY,QAAQ;AAClE;;EAGF,MAAM,YAAY,QAAQ,WAAW,cAAc,MAAM;AACzD,MAAI,WAAW,YAAY,UAAU,eAAe,GAAG;AACrD,0BAAuB,SAAS,WAAW,OAAO,YAAY,QAAQ;AACtE;;EAKF,MAAM,gBAAgB,iBAAiB,QAAQ;EAC/C,IAAI,kBAAkB,cAAc;AAGpC,MAAI,oBAAoB,OACtB,mBAAkB,sBAAsB,QAAQ;EAMlD,MAAM,eAHJ,oBAAoB,YACpB,oBAAoB,kBACpB,oBAAoB,gBACU,SAAS;EAEzC,IAAIC,aAAW,wBAAwB,SAAS,cAAc;AAE9D,QAAM,KAAK,IAAI,eAAe;AAG9B,OAAK,MAAM,QAAQ,QAAQ,YAAY;GACrC,MAAM,OAAO,KAAK,KAAK,aAAa;AACpC,OAAI,SAAS,WAAW,KAAK,WAAW,QAAQ,CAC9C,OAAM,KAAK,IAAI,KAAK,KAAK,IAAI,UAAU,KAAK,MAAM,CAAC,GAAG;;AAI1D,MAAIA,WACF,OAAM,KAAK,WAAW,UAAUA,WAAS,CAAC,GAAG;AAE/C,QAAM,KAAK,IAAI;AAGf,OAAK,MAAM,SAAS,QAAQ,WAAW,WACrC,KAAI,MAAM,aAAa,KAAK,WAAW;GACrC,MAAM,OAAO,MAAM;AACnB,OAAI,QAAQ,KAAK,SAAS,EACxB,OAAM,KAAK,UAAU,KAAK,CAAC;aAEpB,MAAM,aAAa,KAAK,aAEjC,kBACE,OACA,OACA,YACA,SACA,aACA,QACD;AAIL,QAAM,KAAK,KAAK,aAAa,GAAG;AAChC;;AAIF,KAAI,mBAAmB,mBAAmB;AACxC,kBAAgB,SAAS,SAAS,OAAO,YAAY,QAAQ;AAC7D;;CAIF,MAAM,UAAU,QAAQ,QAAQ,aAAa;CAC7C,MAAM,QAAQ,mBAAmB;CACjC,MAAM,SAAS,cAAc,IAAI,QAAQ;AAGzC,KAAI,SAAS,CAAC,YAEZ,OAAM,KAAK,IAAI,QAAQ,qCAAqC;KAE5D,OAAM,KAAK,IAAI,UAAU;AAI3B,qBAAoB,SAAS,MAAM;CAGnC,MAAM,WAAW,wBAAwB,QAAQ;AACjD,KAAI,SACF,OAAM,KAAK,WAAW,UAAU,SAAS,CAAC,GAAG;AAI/C,KAAI,QAAQ;AACV,QAAM,KAAK,MAAM;AACjB;;AAGF,OAAM,KAAK,IAAI;CAGf,MAAM,WAAW,QAAQ,YAAY,cAAc,QAAQ;AAC3D,MAAK,MAAM,SAAS,SAClB,KAAI,MAAM,aAAa,KAAK,WAAW;EACrC,MAAM,OAAO,MAAM;AACnB,MAAI,QAAQ,KAAK,SAAS,EACxB,OAAM,KAAK,UAAU,KAAK,CAAC;YAEpB,MAAM,aAAa,KAAK,aAEjC,kBACE,OACA,OACA,YACA,SACA,OACA,SACD;AAKL,OAAM,KAAK,KAAK,QAAQ,GAAG;;;;;;;AAQ7B,MAAM,cAAc,IAAI,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BrC,SAAgB,oBACd,SACA,OACA,QACA,SACiC;CACjC,MAAMC,QAAyC,EAAE;CACjD,MAAMC,aAA0B,EAAE;CAClC,MAAM,4BAAY,IAAI,SAAqC;CAG3D,MAAM,cAAc,YAAY,YAC9B,OACA,QACA,QAAQ,YACT;CAED,MAAM,iBACJ,QAAQ,eAAe,yBAAyB,IAAI,uBAAuB;AAC7E,KAAI,QAAQ,iBAAiB,eAC3B,SAAQ,cAAc,wBAAwB,eAAe;AAG/D,OAAM,KACJ,0DACkB,YAAY,YAAY,YAAY,YAAY,aAAa,yCAChF;AAED,KAAI,eACF,OAAM,KAAK,mCAAmC,eAAe,aAAa;CAI5E,MAAM,eAAe,YAAY,iBAAiB;AAClD,KAAI,cAAc;EAChB,MAAM,cAAc,YAAY,yBAAyB;AACzD,QAAM,KACJ,yBAAyB,aAAa,8BAC3B,YAAY,MAAM,YAAY,YAAY,OAAO,OAC7D;;AAWH,kBAAiB,SAAS,OAAO,YAPqB;EACpD,eAAe,QAAQ;EACvB,QAAQ,QAAQ;EAChB;EACA;EACD,CAE4D;AAE7D,KAAI,aACF,OAAM,KAAK,SAAS;AAGtB,OAAM,KAAK,SAAS;AAEpB,QAAO;;;;;;;;;;AA+BT,SAAgB,yBACd,SACA,OACA,QACA,SACiB;CAEjB,MAAM,cAAc,YAAY,YAC9B,OACA,QACA,QAAQ,YACT;CAED,MAAM,QAAQ,oBAAoB,SAAS,OAAO,QAAQ,QAAQ;AAElE,QAAO,QAAQ,IAAI,MAAM,CAAC,MAAM,kBAAkB;EAChD,MAAM,QAAQ,cAAc,KAAK,GAAG;EACpC,MAAM,MACJ,kDAAkD,YAAY,YAAY,YAAY,YAAY,aAAa,sCAC1E,YAAY,YAAY,YAAY,YAAY,aAAa,IAAI,MAAM;EAG9G,MAAM,QAAQ,YAAY,OAAO,IAAI;EACrC,IAAI,SAAS;AACb,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,KACrC,WAAU,OAAO,aAAa,MAC5B,MACA,MAAM,SAAS,GAAG,IAAI,KAAK,CAC5B;AAEH,SAAO,6BAA6B,KAAK,OAAO;GAChD"}
1
+ {"version":3,"file":"serializeTimelineDirect.js","names":["rules: string[]","styleParts: string[]","styleStr","parts: Array<string | Promise<string>>","canvasJobs: CanvasJob[]"],"sources":["../../../src/preview/rendering/serializeTimelineDirect.ts"],"sourcesContent":["/**\n * Direct timeline serialization - no intermediate passive structure.\n *\n * Walks the timeline DOM once and builds XML string directly with promise parts\n * for async canvas encoding. 3x faster than DOM creation + XMLSerializer.\n *\n * Architecture:\n * 1. Walk timeline recursively\n * 2. Build array of string parts (some are promises for canvas encoding)\n * 3. Handle shadow DOM by serializing shadow content instead of light DOM\n * 4. Await all promises\n * 5. Join parts into final XML\n */\n\nimport { encodeCanvasesInParallel } from \"../encoding/canvasEncoder.js\";\nimport type { RenderContext } from \"../RenderContext.js\";\nimport { isTemporal, isVisibleAtTime } from \"../previewTypes.js\";\nimport { ScaleConfig } from \"./ScaleConfig.js\";\n\n/**\n * Collect document styles for shadow DOM injection.\n */\nfunction collectDocumentStyles(): string {\n const rules: string[] = [];\n try {\n for (const sheet of document.styleSheets) {\n try {\n if (sheet.cssRules) {\n for (const rule of sheet.cssRules) {\n rules.push(rule.cssText);\n }\n }\n } catch {\n // Expected: cross-origin stylesheets block cssRules access\n }\n }\n } catch (e) {\n console.warn(\"[collectDocumentStyles] Failed to access document.styleSheets:\", e);\n }\n return rules.join(\"\\n\");\n}\n\n/**\n * Elements to skip entirely when serializing.\n * NOTE: SLOT is NOT skipped - it's handled specially to serialize light DOM children.\n */\nconst SKIP_TAGS = new Set([\n \"EF-AUDIO\",\n \"EF-THUMBNAIL-STRIP\",\n \"EF-FILMSTRIP\",\n \"EF-TIMELINE\",\n \"EF-WORKBENCH\",\n \"SCRIPT\",\n \"STYLE\",\n \"TEMPLATE\",\n]);\n\n/**\n * HTML void elements - these cannot have children and must be self-closing in XHTML.\n * Using `<br />` instead of `<br></br>`.\n */\nconst VOID_ELEMENTS = new Set([\n \"area\",\n \"base\",\n \"br\",\n \"col\",\n \"embed\",\n \"hr\",\n \"img\",\n \"input\",\n \"link\",\n \"meta\",\n \"param\",\n \"source\",\n \"track\",\n \"wbr\",\n]);\n\n/**\n * CSS properties to serialize as inline styles.\n */\nconst SERIALIZED_STYLE_PROPERTIES = [\n \"display\",\n \"visibility\",\n \"opacity\",\n \"position\",\n \"top\",\n \"right\",\n \"bottom\",\n \"left\",\n \"zIndex\",\n \"width\",\n \"height\",\n \"minWidth\",\n \"minHeight\",\n \"maxWidth\",\n \"maxHeight\",\n \"flexGrow\",\n \"flexShrink\",\n \"flexBasis\",\n \"flexDirection\",\n \"flexWrap\",\n \"justifyContent\",\n \"alignItems\",\n \"alignContent\",\n \"alignSelf\",\n \"gap\",\n \"gridTemplate\",\n \"gridColumn\",\n \"gridRow\",\n \"gridArea\",\n \"margin\",\n \"padding\",\n \"boxSizing\",\n \"border\",\n \"borderTop\",\n \"borderRight\",\n \"borderBottom\",\n \"borderLeft\",\n \"borderRadius\",\n \"background\",\n \"color\",\n \"boxShadow\",\n \"filter\",\n \"backdropFilter\",\n \"clipPath\",\n \"fontFamily\",\n \"fontSize\",\n \"fontWeight\",\n \"fontStyle\",\n \"fontVariant\",\n \"textAlign\",\n \"textDecoration\",\n \"textShadow\",\n \"textTransform\",\n \"letterSpacing\",\n \"wordSpacing\",\n \"whiteSpace\",\n \"textOverflow\",\n \"lineHeight\",\n \"verticalAlign\",\n \"transform\",\n \"transformOrigin\",\n \"transformStyle\",\n \"perspective\",\n \"perspectiveOrigin\",\n \"backfaceVisibility\",\n \"cursor\",\n \"pointerEvents\",\n \"userSelect\",\n \"overflow\",\n \"objectFit\",\n \"objectPosition\",\n] as const;\n\n/**\n * Caption child elements that should preserve display:none.\n * These use display:none for content visibility, not temporal visibility.\n */\nconst CAPTION_CHILD_TAGS = new Set([\n \"EF-CAPTIONS-ACTIVE-WORD\",\n \"EF-CAPTIONS-BEFORE-ACTIVE-WORD\",\n \"EF-CAPTIONS-AFTER-ACTIVE-WORD\",\n \"EF-CAPTIONS-SEGMENT\",\n]);\n\ninterface SerializationOptions {\n renderContext?: RenderContext;\n canvasScale: number;\n timeMs: number;\n}\n\ninterface InternalSerializationOptions {\n renderContext?: RenderContext;\n timeMs: number;\n scaleConfig: ScaleConfig;\n sourceMap: WeakMap<HTMLCanvasElement, Element>;\n}\n\ninterface CanvasJob {\n canvas: HTMLCanvasElement;\n sourceElement: Element;\n promiseIndex: number;\n}\n\n/**\n * Escape special XML characters.\n */\nfunction escapeXML(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\n/**\n * Resolve the natural display value for an element that has display:none\n * set as an inline style (e.g., from temporal visibility via updateAnimations).\n *\n * Temporarily removes the inline display override so getComputedStyle falls\n * through to the element's stylesheet rules (including shadow DOM :host styles),\n * reads the natural value, then restores the override.\n */\nfunction resolveNaturalDisplay(element: Element): string {\n const htmlEl = element as HTMLElement;\n const inlineDisplay = htmlEl.style?.getPropertyValue(\"display\");\n if (inlineDisplay === \"none\" && htmlEl.style) {\n htmlEl.style.removeProperty(\"display\");\n const natural = getComputedStyle(element).getPropertyValue(\"display\");\n htmlEl.style.setProperty(\"display\", \"none\");\n return natural || \"block\";\n }\n return \"block\";\n}\n\n/**\n * Serialize computed styles as inline style string.\n * Handles display:none recovery for non-caption elements by resolving\n * the element's natural display value from its stylesheet rules.\n * @param element - The element to serialize styles for\n * @param styles - Optional pre-computed CSSStyleDeclaration (avoids redundant getComputedStyle calls)\n */\nfunction serializeComputedStyles(element: Element, styles?: CSSStyleDeclaration): string {\n const computed = styles ?? getComputedStyle(element);\n const styleParts: string[] = [];\n const tagName = element.tagName;\n const isCaptionChild = CAPTION_CHILD_TAGS.has(tagName);\n\n // Check if the element has explicit width/height in its inline style.\n // For elements that auto-size to content (inline, inline-block text),\n // serializing the computed \"used\" pixel width/height would lock them to\n // exact dimensions that may not match the foreignObject rendering context,\n // causing text wrapping when font metrics differ slightly.\n const htmlEl = element as HTMLElement;\n const hasExplicitWidth = !!htmlEl.style?.getPropertyValue(\"width\");\n const hasExplicitHeight = !!htmlEl.style?.getPropertyValue(\"height\");\n\n for (const prop of SERIALIZED_STYLE_PROPERTIES) {\n // Convert camelCase to kebab-case first\n const kebab = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n const value = computed.getPropertyValue(kebab);\n\n // Skip only truly empty values\n if (!value || value === \"\") {\n continue;\n }\n\n // Handle display property specially\n let finalValue = value;\n if (prop === \"display\") {\n // For non-caption elements, recover the natural display value when display:none\n // was set by the temporal visibility system (updateAnimations). This prevents\n // inline elements (like ef-text-segment) from being serialized as display:block.\n if (value === \"none\" && !isCaptionChild) {\n finalValue = resolveNaturalDisplay(element);\n }\n }\n\n // Force visibility:visible - the source container may have visibility:hidden\n // for off-screen rendering, but we want the serialized output to be visible\n if (prop === \"visibility\") {\n finalValue = \"visible\";\n }\n\n // Skip the proxy-mode sentinel value: inset(100%) hides the element\n // off-screen while keeping it in the layout. All other clip-path values\n // are legitimate visual effects and must be preserved.\n if (prop === \"clipPath\" && value === \"inset(100%)\") {\n continue;\n }\n\n // Skip width/height when not explicitly set on the element.\n // getComputedStyle returns \"used\" pixel values for width/height even when\n // the specified value is auto. Serializing these pixel values locks\n // content-sized elements (text segments, inline-block spans) to exact\n // dimensions, which breaks when the foreignObject context renders text\n // with different font metrics.\n if (prop === \"width\" && !hasExplicitWidth) {\n continue;\n }\n if (prop === \"height\" && !hasExplicitHeight) {\n continue;\n }\n\n styleParts.push(`${kebab}:${finalValue}`);\n }\n\n // Disable animations/transitions to prevent re-animation\n styleParts.push(\"animation:none\", \"transition:none\");\n\n return styleParts.join(\";\");\n}\n\n/**\n * Serialize element attributes (excluding style, id, xmlns, event handlers).\n */\nfunction serializeAttributes(element: Element, parts: Array<string | Promise<string>>): void {\n for (const attr of element.attributes) {\n const name = attr.name.toLowerCase();\n // Skip: id, style, xmlns (namespace handled separately), event handlers\n if (name === \"id\" || name === \"style\" || name === \"xmlns\" || name.startsWith(\"on\")) {\n continue;\n }\n parts.push(` ${attr.name}=\"${escapeXML(attr.value)}\"`);\n }\n}\n\n/**\n * Check if a canvas element should preserve alpha channel.\n * EF-WAVEFORM always needs alpha, EF-IMAGE checks hasAlpha property.\n * EF-SURFACE needs alpha because:\n * 1. Without a target, its canvas is transparent and CSS background should show through\n * 2. The target element may have transparent content\n * Raw canvas elements must preserve alpha - we don't know what they contain.\n */\nfunction shouldPreserveAlpha(sourceElement: Element): boolean {\n const tagName = sourceElement.tagName;\n if (tagName === \"EF-WAVEFORM\") {\n return true;\n }\n if (tagName === \"EF-SURFACE\") {\n // Surface needs alpha to allow CSS background to show through empty areas\n return true;\n }\n if (tagName === \"EF-IMAGE\") {\n return \"hasAlpha\" in sourceElement && (sourceElement as any).hasAlpha === true;\n }\n // Raw canvas elements must preserve alpha\n if (sourceElement instanceof HTMLCanvasElement) {\n return true;\n }\n return false;\n}\n\n/**\n * Find the capture proxy canvas for an offscreen-rendered canvas.\n * When a canvas is transferred to offscreen via transferControlToOffscreen(),\n * the main thread can no longer read pixels from it. OffscreenCompositionCanvas\n * creates a hidden capture canvas (marked with data-offscreen-capture) that\n * receives ImageBitmap frames from the worker.\n */\nfunction findCaptureProxy(canvas: HTMLCanvasElement): HTMLCanvasElement | null {\n const container = canvas.parentElement;\n if (!container) return null;\n return container.querySelector('canvas[data-offscreen-capture=\"true\"]');\n}\n\n/**\n * Read pixels directly from a WebGL canvas's drawing buffer via gl.readPixels().\n *\n * drawImage(webglCanvas) reads from the compositor's \"presented\" surface, which\n * is only refreshed during requestAnimationFrame / compositing cycles. In hidden\n * browser tabs, compositing is suspended, so drawImage returns stale pixels even\n * though gl.render() produced new content in the drawing buffer.\n *\n * readPixels() reads from the drawing buffer directly, bypassing the compositor.\n *\n * Returns null for non-WebGL canvases (getContext returns null when a different\n * context type is already active).\n */\nfunction readWebGLPixels(canvas: HTMLCanvasElement): Uint8ClampedArray | null {\n const gl = (canvas.getContext(\"webgl2\") ??\n canvas.getContext(\"webgl\")) as WebGLRenderingContext | null;\n if (!gl) return null;\n\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return null;\n\n // Ensure we read from the drawing buffer, not a leftover FBO\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n\n const pixels = new Uint8Array(width * height * 4);\n gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);\n\n // readPixels returns rows bottom-to-top; flip to top-to-bottom for ImageData\n const rowSize = width * 4;\n const halfHeight = Math.floor(height / 2);\n const temp = new Uint8Array(rowSize);\n for (let y = 0; y < halfHeight; y++) {\n const topOffset = y * rowSize;\n const bottomOffset = (height - 1 - y) * rowSize;\n temp.set(pixels.subarray(topOffset, topOffset + rowSize));\n pixels.set(pixels.subarray(bottomOffset, bottomOffset + rowSize), topOffset);\n pixels.set(temp, bottomOffset);\n }\n\n return new Uint8ClampedArray(pixels.buffer);\n}\n\n/**\n * Create a snapshot copy of a canvas's current pixels.\n * This captures the pixels synchronously before any async encoding,\n * preventing race conditions where the source canvas is modified.\n *\n * For WebGL canvases, uses gl.readPixels() to bypass the compositor's\n * presentation layer (which is suspended in hidden browser tabs).\n *\n * For offscreen-rendered canvases, this automatically uses the capture proxy\n * canvas instead of the transferred display canvas.\n */\nfunction snapshotCanvas(\n canvas: HTMLCanvasElement,\n scale: number,\n preserveAlpha: boolean,\n): HTMLCanvasElement {\n // If this canvas was transferred to offscreen, use its capture proxy\n const captureProxy = findCaptureProxy(canvas);\n const sourceCanvas = captureProxy ?? canvas;\n\n const targetWidth = Math.max(1, Math.floor(sourceCanvas.width * scale));\n const targetHeight = Math.max(1, Math.floor(sourceCanvas.height * scale));\n\n const copy = document.createElement(\"canvas\");\n copy.width = targetWidth;\n copy.height = targetHeight;\n\n if (preserveAlpha) {\n copy.dataset.preserveAlpha = \"true\";\n }\n\n const ctx = copy.getContext(\"2d\");\n if (ctx && sourceCanvas.width > 0 && sourceCanvas.height > 0) {\n // Try reading directly from WebGL drawing buffer (bypasses compositor)\n // Only needed when page is hidden - compositor is suspended in hidden tabs\n const useGlBypass = document.hidden;\n const glPixels = useGlBypass ? readWebGLPixels(sourceCanvas) : null;\n if (glPixels) {\n const srcW = sourceCanvas.width;\n const srcH = sourceCanvas.height;\n const imageData = new ImageData(\n glPixels as unknown as Uint8ClampedArray<ArrayBuffer>,\n srcW,\n srcH,\n );\n\n if (targetWidth === srcW && targetHeight === srcH) {\n ctx.putImageData(imageData, 0, 0);\n } else {\n // putImageData doesn't scale — bounce through a temp canvas\n const temp = document.createElement(\"canvas\");\n temp.width = srcW;\n temp.height = srcH;\n temp.getContext(\"2d\")!.putImageData(imageData, 0, 0);\n ctx.drawImage(temp, 0, 0, targetWidth, targetHeight);\n }\n } else {\n // Non-WebGL canvas: drawImage is synchronous and correct\n ctx.drawImage(sourceCanvas, 0, 0, targetWidth, targetHeight);\n }\n }\n\n return copy;\n}\n\n/**\n * Serialize a canvas element as an <img> with base64 data URL.\n * Creates a snapshot of current pixels before async encoding to prevent race conditions.\n *\n * OPTIMIZATION: Calculate optimal encoding resolution based on:\n * 1. CSS display size (how big it actually appears)\n * 2. Video export scale (output resolution multiplier)\n * 3. Quality multiplier (for sharpness, default 1.5x)\n */\nfunction serializeCanvas(\n sourceElement: Element,\n canvas: HTMLCanvasElement,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n): void {\n // If this canvas was transferred to offscreen, use its capture proxy\n const captureProxy = findCaptureProxy(canvas);\n const sourceCanvas = captureProxy ?? canvas;\n\n // Use intrinsic canvas dimensions, not computed styles (which may be zoom-affected)\n const width = sourceCanvas.width;\n const height = sourceCanvas.height;\n\n // Skip empty canvases\n if (width === 0 || height === 0) {\n return;\n }\n\n // Get computed style once and reuse\n const computedStyle = getComputedStyle(sourceElement);\n const styleStr = serializeComputedStyles(sourceElement, computedStyle);\n\n // Get computed dimensions from source element (respects CSS like w-[420px])\n const computedWidth = computedStyle.width;\n const computedHeight = computedStyle.height;\n\n // Preserve the source element's object-fit and object-position for correct scaling.\n // These CSS properties control how the canvas content fits its container and must be\n // carried through to the serialized <img> to maintain visual fidelity.\n const styleParts = styleStr ? styleStr.split(\";\").filter((s) => s.trim()) : [];\n\n // Remove width/height from computed styles (we'll set them explicitly from computed dimensions)\n const filteredParts = styleParts.filter((s) => {\n const trimmed = s.trim();\n return !trimmed.startsWith(\"width:\") && !trimmed.startsWith(\"height:\");\n });\n\n // Use host element dimensions if available, otherwise fall back to canvas natural dimensions\n const displayWidth = computedWidth || `${width}px`;\n const displayHeight = computedHeight || `${height}px`;\n\n filteredParts.push(`width:${displayWidth}`);\n filteredParts.push(`height:${displayHeight}`);\n filteredParts.push(`display:block`);\n\n const finalStyle = filteredParts.join(\";\");\n\n // Check if we need to preserve alpha channel\n const preserveAlpha = shouldPreserveAlpha(sourceElement);\n\n // CRITICAL: Calculate optimal encoding scale BEFORE creating snapshot.\n // This prevents encoding at full resolution when CSS display size is much smaller.\n let optimalScale = options.scaleConfig.exportScale; // Start with export scale as fallback\n\n try {\n const cssWidth = parseFloat(computedWidth) || sourceCanvas.width;\n const cssHeight = parseFloat(computedHeight) || sourceCanvas.height;\n\n // Use ScaleConfig to compute optimal canvas scale\n optimalScale = options.scaleConfig.computeCanvasScale({\n naturalWidth: sourceCanvas.width,\n naturalHeight: sourceCanvas.height,\n displayWidth: cssWidth,\n displayHeight: cssHeight,\n });\n } catch (e) {\n // Fallback to export scale if we can't get computed style\n console.warn(`[serializeCanvas] Failed to get computed style for ${sourceElement.tagName}:`, e);\n }\n\n // CRITICAL: Create a snapshot of canvas pixels SYNCHRONOUSLY before any async work.\n // This prevents race conditions where concurrent renders overwrite the shared\n // shadow canvas while encoding is in progress.\n // Note: snapshotCanvas already handles finding the capture proxy internally\n const snapshot = snapshotCanvas(canvas, optimalScale, preserveAlpha);\n\n // Open img tag with all styles from source element\n parts.push(`<img style=\"${escapeXML(finalStyle)}\" src=\"`);\n\n // Kick off async encoding of the SNAPSHOT (not the live canvas)\n const promiseIndex = parts.length;\n options.sourceMap.set(snapshot, sourceElement);\n\n // Snapshot is already scaled, so encode at 1.0 scale\n const encodePromise = encodeCanvasesInParallel([snapshot], {\n scale: 1.0,\n renderContext: options.renderContext,\n sourceMap: options.sourceMap,\n }).then((results) => results[0]?.dataUrl || \"\");\n\n parts.push(encodePromise);\n canvasJobs.push({ canvas: snapshot, sourceElement, promiseIndex });\n\n // Close img tag\n parts.push('\" />');\n}\n\n/**\n * Serialize an image element as a canvas (for shadow DOM img elements).\n */\nfunction serializeImageAsCanvas(\n sourceElement: Element,\n img: HTMLImageElement,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n): void {\n // Convert img to canvas for serialization\n const canvas = document.createElement(\"canvas\");\n canvas.width = img.naturalWidth;\n canvas.height = img.naturalHeight;\n\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n try {\n ctx.drawImage(img, 0, 0);\n } catch (_e) {\n // Cross-origin image - skip\n return;\n }\n }\n\n serializeCanvas(sourceElement, canvas, parts, canvasJobs, options);\n}\n\n/**\n * Serialize slotted light DOM children of a host element.\n */\nfunction serializeSlottedContent(\n slotHost: Element,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n parentIsSVG: boolean,\n): void {\n for (const slottedChild of slotHost.childNodes) {\n if (slottedChild.nodeType === Node.TEXT_NODE) {\n const text = slottedChild.textContent;\n if (text && text.length > 0) {\n parts.push(escapeXML(text));\n }\n } else if (slottedChild.nodeType === Node.ELEMENT_NODE) {\n serializeElement(slottedChild as Element, parts, canvasJobs, options, parentIsSVG, null);\n }\n }\n}\n\n/**\n * Recursively serialize an element and its children to XML parts.\n * @param slotHost - When serializing inside shadow DOM, the custom element whose light DOM children should be serialized for slots\n */\nfunction serializeElement(\n element: Element,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n parentIsSVG = false,\n slotHost: Element | null = null,\n): void {\n // Skip certain elements\n if (SKIP_TAGS.has(element.tagName)) {\n return;\n }\n\n // Handle SLOT elements - serialize light DOM children of the slot host\n if (element.tagName === \"SLOT\" && slotHost) {\n serializeSlottedContent(slotHost, parts, canvasJobs, options, parentIsSVG);\n return;\n }\n\n // Check temporal visibility - skip elements outside their time bounds\n // This is non-destructive (doesn't modify DOM)\n // NOTE: Ancestor checking is unnecessary - serializeElement walks top-down,\n // so if a parent is temporally invisible, its children are never visited\n if (!isVisibleAtTime(element, options.timeMs)) {\n return;\n }\n\n // Respect updateAnimations' visibility decision for temporal elements.\n // isVisibleAtTime uses inclusive end bounds, but updateAnimations uses\n // exclusive end for mid-composition elements (VisibilityPolicy). When\n // updateAnimations has set display:none, that is the authoritative decision.\n if (\n isTemporal(element) &&\n (element as HTMLElement).style?.getPropertyValue(\"display\") === \"none\"\n ) {\n return;\n }\n\n // Custom element with shadow DOM?\n const isCustom = element.tagName.includes(\"-\");\n if (isCustom && element.shadowRoot) {\n const shadowCanvas = element.shadowRoot.querySelector(\"canvas\");\n if (shadowCanvas) {\n serializeCanvas(element, shadowCanvas, parts, canvasJobs, options);\n return;\n }\n\n const shadowImg = element.shadowRoot.querySelector(\"img\");\n if (shadowImg?.complete && shadowImg.naturalWidth > 0) {\n serializeImageAsCanvas(element, shadowImg, parts, canvasJobs, options);\n return;\n }\n\n // Serialize custom element with its styles, then shadow DOM content inside\n // Use span for inline/inline-block/inline-flex elements to preserve inline behavior\n const computedStyle = getComputedStyle(element);\n let computedDisplay = computedStyle.display;\n // If display:none was set by temporal visibility, resolve the natural display\n // to determine the correct container tag (span vs div)\n if (computedDisplay === \"none\") {\n computedDisplay = resolveNaturalDisplay(element);\n }\n const isInline =\n computedDisplay === \"inline\" ||\n computedDisplay === \"inline-block\" ||\n computedDisplay === \"inline-flex\";\n const containerTag = isInline ? \"span\" : \"div\";\n\n let styleStr = serializeComputedStyles(element, computedStyle);\n\n parts.push(`<${containerTag}`);\n\n // Copy data attributes and class from custom element\n for (const attr of element.attributes) {\n const name = attr.name.toLowerCase();\n if (name === \"class\" || name.startsWith(\"data-\")) {\n parts.push(` ${attr.name}=\"${escapeXML(attr.value)}\"`);\n }\n }\n\n if (styleStr) {\n parts.push(` style=\"${escapeXML(styleStr)}\"`);\n }\n parts.push(\">\");\n\n // Serialize shadow DOM content with this element as the slot host\n for (const child of element.shadowRoot.childNodes) {\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent;\n if (text && text.length > 0) {\n parts.push(escapeXML(text));\n }\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n // Pass this element as slotHost so nested SLOTs can access light DOM children\n serializeElement(child as Element, parts, canvasJobs, options, parentIsSVG, element);\n }\n }\n\n parts.push(`</${containerTag}>`);\n return;\n }\n\n // Raw canvas in light DOM\n if (element instanceof HTMLCanvasElement) {\n serializeCanvas(element, element, parts, canvasJobs, options);\n return;\n }\n\n // Standard element - serialize to XHTML\n const tagName = element.tagName.toLowerCase();\n const isSVG = element instanceof SVGElement;\n const isVoid = VOID_ELEMENTS.has(tagName);\n\n // Open tag with namespace (only add xmlns for root SVG elements, not children)\n if (isSVG && !parentIsSVG) {\n // Root SVG element - needs xmlns declaration\n parts.push(`<${tagName} xmlns=\"http://www.w3.org/2000/svg\"`);\n } else {\n parts.push(`<${tagName}`);\n }\n\n // Attributes\n serializeAttributes(element, parts);\n\n // Computed styles as inline style attribute\n const styleStr = serializeComputedStyles(element);\n if (styleStr) {\n parts.push(` style=\"${escapeXML(styleStr)}\"`);\n }\n\n // Void elements: self-close with /> (XHTML requirement)\n if (isVoid) {\n parts.push(\" />\");\n return;\n }\n\n parts.push(\">\");\n\n // Children (shadow or light)\n const children = element.shadowRoot?.childNodes || element.childNodes;\n for (const child of children) {\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent;\n if (text && text.length > 0) {\n parts.push(escapeXML(text));\n }\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n // Preserve slotHost when recursing into standard elements inside shadow DOM\n serializeElement(child as Element, parts, canvasJobs, options, isSVG, slotHost);\n }\n }\n\n // Close tag\n parts.push(`</${tagName}>`);\n}\n\n/**\n * TextEncoder instance for SVG-to-base64 encoding.\n * encode() converts to UTF-8 bytes in a single native call, then we\n * base64-encode the bytes. ~33% overhead vs ~200% for percent-encoding.\n */\nconst textEncoder = new TextEncoder();\n\n/**\n * Synchronous DOM capture phase. Walks the element tree, snapshots canvas\n * pixels, and kicks off async encoding. Returns parts array containing\n * string fragments and encoding promises.\n *\n * After this function returns, the source element's DOM is no longer\n * referenced — the clone can safely be seeked to the next frame.\n *\n * SCALING ARCHITECTURE (unified via ScaleConfig):\n *\n * ScaleConfig centralizes all scaling logic and provides:\n * 1. Output SVG dimensions (width * exportScale, height * exportScale)\n * 2. DOM scaling wrapper (CSS transform:scale when exportScale < 1)\n * 3. Per-canvas optimal encoding scale via computeCanvasScale()\n *\n * Canvas scaling is independent from DOM scaling because:\n * - Canvas elements have intrinsic pixel dimensions and can be downsampled\n * efficiently before encoding (prevents encoding 1920px at full resolution\n * when displayed at 420px)\n * - DOM content has no intrinsic resolution and must be scaled via CSS\n * transforms, which the browser handles during SVG foreignObject rendering\n *\n * Example: 1920x1080 @ 0.5 export scale\n * - Output SVG: 960x540\n * - DOM wrapper: transform:scale(0.5) on 1920x1080 content\n * - Canvas (1920px displayed at 420px): encoded at ~0.16x (315px)\n * via computeCanvasScale(420/1920 * 0.5 * 1.5 quality = 0.164)\n */\nexport function captureElementParts(\n element: Element,\n width: number,\n height: number,\n options: SerializationOptions,\n): Array<string | Promise<string>> {\n const parts: Array<string | Promise<string>> = [];\n const canvasJobs: CanvasJob[] = [];\n const sourceMap = new WeakMap<HTMLCanvasElement, Element>();\n\n // Create ScaleConfig to centralize all scaling logic\n const scaleConfig = ScaleConfig.fromOptions(width, height, options.canvasScale);\n\n const documentStyles =\n options.renderContext?.getCachedDocumentStyles() ?? collectDocumentStyles();\n if (options.renderContext && documentStyles) {\n options.renderContext.setCachedDocumentStyles(documentStyles);\n }\n\n parts.push(\n `<div xmlns=\"http://www.w3.org/1999/xhtml\" ` +\n `style=\"width:${scaleConfig.outputWidth}px;height:${scaleConfig.outputHeight}px;overflow:hidden;position:relative;\">`,\n );\n\n if (documentStyles) {\n parts.push(`<style type=\"text/css\"><![CDATA[${documentStyles}]]></style>`);\n }\n\n // Apply DOM scaling wrapper if needed\n const domTransform = scaleConfig.getDOMTransform();\n if (domTransform) {\n const wrapperDims = scaleConfig.getDOMWrapperDimensions();\n parts.push(\n `<div style=\"transform:${domTransform};transform-origin:0 0;` +\n `width:${wrapperDims.width}px;height:${wrapperDims.height}px;\">`,\n );\n }\n\n // Create internal options with ScaleConfig\n const internalOptions: InternalSerializationOptions = {\n renderContext: options.renderContext,\n timeMs: options.timeMs,\n scaleConfig,\n sourceMap,\n };\n\n serializeElement(element, parts, canvasJobs, internalOptions);\n\n if (domTransform) {\n parts.push(\"</div>\");\n }\n\n parts.push(\"</div>\");\n\n return parts;\n}\n\n/**\n * Serialize any element directly to XHTML string.\n *\n * @param element - The element to serialize (timegroup, temporal element, or plain DOM)\n * @param width - Output width\n * @param height - Output height\n * @param options - Serialization options (renderContext, canvasScale, timeMs)\n * @returns XHTML string with all canvases encoded as base64 data URLs\n */\nexport async function serializeElementToXHTML(\n element: Element,\n width: number,\n height: number,\n options: SerializationOptions,\n): Promise<string> {\n const parts = captureElementParts(element, width, height, options);\n const resolvedParts = await Promise.all(parts);\n return resolvedParts.join(\"\");\n}\n\n/**\n * Synchronous capture with deferred data URI encoding.\n *\n * Walks the DOM and snapshots canvas pixels synchronously, then returns\n * a promise that resolves to the SVG data URI once async canvas-to-base64\n * encoding completes. The source element is NOT referenced after this\n * function returns — the caller can immediately mutate/seek the clone.\n */\nexport function captureTimelineToDataUri(\n element: Element,\n width: number,\n height: number,\n options: SerializationOptions,\n): Promise<string> {\n // Create ScaleConfig to compute scaled dimensions\n const scaleConfig = ScaleConfig.fromOptions(width, height, options.canvasScale);\n\n const parts = captureElementParts(element, width, height, options);\n\n return Promise.all(parts).then((resolvedParts) => {\n const xhtml = resolvedParts.join(\"\");\n const svg =\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${scaleConfig.outputWidth}\" height=\"${scaleConfig.outputHeight}\">` +\n `<foreignObject x=\"0\" y=\"0\" width=\"${scaleConfig.outputWidth}\" height=\"${scaleConfig.outputHeight}\">${xhtml}</foreignObject>` +\n `</svg>`;\n // Encode SVG to base64 data URI inline (avoids module-level function reference issues)\n const bytes = textEncoder.encode(svg);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i += 8192) {\n binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192) as unknown as number[]);\n }\n return `data:image/svg+xml;base64,${btoa(binary)}`;\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"statsTrackingStrategy.js","names":[],"sources":["../../src/preview/statsTrackingStrategy.ts"],"sourcesContent":["/**\n * Stats tracking strategy for different presentation modes.\n *\n * Uses strategy pattern to encapsulate mode-specific stats tracking logic,\n * allowing each mode to report what stats it supports and provide its own implementation.\n */\n\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { AdaptiveResolutionTracker } from \"./AdaptiveResolutionTracker.js\";\nimport type { CanvasPreviewResult } from \"./renderTimegroupToCanvas.js\";\nimport type { PreviewPresentationMode } from \"./previewSettings.js\";\n\n/**\n * Stat types that can be tracked.\n */\nexport type StatType =\n | \"fps\"\n | \"renderTime\"\n | \"headroom\"\n | \"resolution\"\n | \"resolutionScale\"\n | \"cpuPressure\"\n | \"adaptiveResolution\";\n\n/**\n * Playback statistics for display.\n */\nexport interface PlaybackStats {\n fps: number;\n avgRenderTime: number | null; // null if not measurable\n headroom: number | null; // null if not applicable\n pressureState: string;\n pressureHistory: string[];\n renderWidth: number;\n renderHeight: number;\n resolutionScale: number | null; // null if not applicable\n samplesAtCurrentScale?: number; // only for adaptive resolution\n canScaleUp?: boolean; // only for adaptive resolution\n canScaleDown?: boolean; // only for adaptive resolution\n}\n\n/**\n * Strategy interface for tracking stats in different presentation modes.\n */\nexport interface StatsTrackingStrategy {\n /** Start tracking stats (called when mode is initialized and stats are enabled) */\n start(): void;\n /** Stop tracking stats (called when mode stops or stats are disabled) */\n stop(): void;\n /** Get current stats, or null if not available */\n getStats(): PlaybackStats | null;\n /** Check if this strategy supports a specific stat type */\n supportsStat(stat: StatType): boolean;\n /**\n * Record render timing (optional - only implemented by canvas stats).\n * Called by EFWorkbench after each canvas refresh.\n */\n recordRenderTime?(renderTimeMs: number, timestamp: number): void;\n}\n\n/**\n * Canvas mode stats tracking strategy.\n * Tracks all stats including render time, headroom, resolution scale, and adaptive resolution.\n *\n * This strategy is PASSIVE - it receives render timing from EFWorkbench rather than\n * driving its own render loop. This prevents race conditions and ensures accurate measurements.\n */\nexport class CanvasStatsStrategy implements StatsTrackingStrategy {\n private adaptiveTracker: AdaptiveResolutionTracker;\n private readonly compositionWidth: number;\n private readonly compositionHeight: number;\n private getResolutionScale: () => number;\n private isAtRest: () => boolean;\n private isExporting: () => boolean;\n\n private lastStatsUpdateTime = 0;\n private currentStats: PlaybackStats | null = null;\n\n constructor(options: {\n canvasPreviewResult: CanvasPreviewResult;\n adaptiveTracker: AdaptiveResolutionTracker;\n compositionWidth: number;\n compositionHeight: number;\n getResolutionScale: () => number;\n isAtRest: () => boolean;\n isExporting: () => boolean;\n }) {\n // Note: canvasPreviewResult no longer needed since we don't call refresh()\n this.adaptiveTracker = options.adaptiveTracker;\n this.compositionWidth = options.compositionWidth;\n this.compositionHeight = options.compositionHeight;\n this.getResolutionScale = options.getResolutionScale;\n this.isAtRest = options.isAtRest;\n this.isExporting = options.isExporting;\n }\n\n start(): void {\n // Initialize stats update time\n this.lastStatsUpdateTime = performance.now();\n }\n\n stop(): void {\n this.currentStats = null;\n }\n\n /**\n * Record render timing from EFWorkbench's render loop.\n * This is called after each successful canvas refresh.\n */\n recordRenderTime(renderTimeMs: number, timestamp: number): void {\n // Skip during export\n if (this.isExporting()) return;\n\n // Only record frame timing when in motion (playing/scrubbing)\n // This prevents inflated stats at rest and focuses tracking on actual playback\n if (!this.isAtRest()) {\n this.adaptiveTracker.recordFrame(renderTimeMs, timestamp);\n }\n\n // Update playback stats every 100ms (10 times per second)\n if (timestamp - this.lastStatsUpdateTime > 100) {\n this.lastStatsUpdateTime = timestamp;\n // Get CURRENT resolution from the canvas result (may have changed dynamically)\n const currentScale = this.getResolutionScale();\n const renderWidth = Math.floor(this.compositionWidth * currentScale);\n const renderHeight = Math.floor(this.compositionHeight * currentScale);\n this.updateStats(renderWidth, renderHeight, currentScale);\n }\n }\n\n getStats(): PlaybackStats | null {\n return this.currentStats;\n }\n\n supportsStat(_stat: StatType): boolean {\n // Canvas mode supports all stats\n return true;\n }\n\n private updateStats(\n renderWidth: number,\n renderHeight: number,\n resolutionScale: number,\n ): void {\n const trackerStats = this.adaptiveTracker.getStats();\n\n this.currentStats = {\n fps: trackerStats.fps,\n avgRenderTime: trackerStats.avgRenderTime,\n headroom: trackerStats.headroom,\n pressureState: trackerStats.pressureState,\n pressureHistory: trackerStats.pressureHistory,\n renderWidth,\n renderHeight,\n resolutionScale,\n samplesAtCurrentScale: trackerStats.samplesAtCurrentScale,\n canScaleUp: trackerStats.canScaleUp,\n canScaleDown: trackerStats.canScaleDown,\n };\n }\n}\n\n/**\n * DOM mode stats tracking strategy.\n * Tracks FPS, resolution, CPU pressure, and frame seek time.\n */\nexport class DomStatsStrategy implements StatsTrackingStrategy {\n private timegroup: EFTimegroup;\n private adaptiveTracker: AdaptiveResolutionTracker;\n\n private animationFrame: number | null = null;\n private lastFrameTime = 0;\n private frameIntervals: number[] = [];\n private lastStatsUpdateTime = 0;\n private currentStats: PlaybackStats | null = null;\n\n // Frame seek time tracking\n private seekStartTime = 0;\n private seekTimes: number[] = [];\n private frameTaskCleanup: (() => void) | null = null;\n\n private readonly ROLLING_WINDOW_SIZE = 30; // ~1 second at 30fps\n private readonly TARGET_FRAME_TIME_MS = 33.33; // 30fps target\n\n constructor(options: {\n timegroup: EFTimegroup;\n adaptiveTracker: AdaptiveResolutionTracker;\n }) {\n this.timegroup = options.timegroup;\n this.adaptiveTracker = options.adaptiveTracker;\n }\n\n start(): void {\n if (this.animationFrame !== null) return;\n\n // Track frame seek times using frameTask callback\n // This measures how long it takes for the timegroup to fully update after a seek\n const frameTaskCallback = async () => {\n if (this.seekStartTime > 0) {\n const seekTime = performance.now() - this.seekStartTime;\n this.seekTimes.push(seekTime);\n if (this.seekTimes.length > this.ROLLING_WINDOW_SIZE) {\n this.seekTimes.shift();\n }\n this.seekStartTime = 0; // Reset after recording\n }\n };\n\n this.timegroup.addFrameTask(frameTaskCallback);\n this.frameTaskCleanup = () => {\n // Note: EFTimegroup doesn't have removeFrameTask, but this is fine\n // The callback will be cleaned up when the timegroup is destroyed\n };\n\n // Track currentTimeMs changes to detect seeks\n let lastCurrentTimeMs = this.timegroup.currentTimeMs;\n const checkSeek = () => {\n const currentTimeMs = this.timegroup.currentTimeMs;\n if (currentTimeMs !== lastCurrentTimeMs) {\n // Seek detected - start timing\n this.seekStartTime = performance.now();\n lastCurrentTimeMs = currentTimeMs;\n }\n };\n\n const loop = (timestamp: number) => {\n if (this.animationFrame === null) return; // Stopped\n\n // Check for seeks\n checkSeek();\n\n // Track frame intervals for FPS calculation\n if (this.lastFrameTime > 0) {\n const interval = timestamp - this.lastFrameTime;\n this.frameIntervals.push(interval);\n if (this.frameIntervals.length > this.ROLLING_WINDOW_SIZE) {\n this.frameIntervals.shift();\n }\n }\n this.lastFrameTime = timestamp;\n\n // Update stats every 100ms (10 times per second)\n if (timestamp - this.lastStatsUpdateTime > 100) {\n this.lastStatsUpdateTime = timestamp;\n this.updateStats();\n }\n\n this.animationFrame = requestAnimationFrame(loop);\n };\n\n this.animationFrame = requestAnimationFrame(loop);\n }\n\n stop(): void {\n if (this.animationFrame !== null) {\n cancelAnimationFrame(this.animationFrame);\n this.animationFrame = null;\n }\n if (this.frameTaskCleanup) {\n this.frameTaskCleanup();\n this.frameTaskCleanup = null;\n }\n this.lastFrameTime = 0;\n this.frameIntervals = [];\n this.seekStartTime = 0;\n this.seekTimes = [];\n this.currentStats = null;\n }\n\n getStats(): PlaybackStats | null {\n return this.currentStats;\n }\n\n supportsStat(stat: StatType): boolean {\n // DOM mode supports: fps, resolution, cpuPressure, renderTime (seek time), headroom\n // Does NOT support: resolutionScale, adaptiveResolution\n return (\n stat === \"fps\" ||\n stat === \"resolution\" ||\n stat === \"cpuPressure\" ||\n stat === \"renderTime\" ||\n stat === \"headroom\"\n );\n }\n\n private updateStats(): void {\n // Calculate FPS from frame intervals\n const avgFrameInterval =\n this.frameIntervals.length > 0\n ? this.frameIntervals.reduce((a, b) => a + b, 0) /\n this.frameIntervals.length\n : 16.67;\n const fps = avgFrameInterval > 0 ? 1000 / avgFrameInterval : 0;\n\n // Calculate average seek time (frame update time)\n const avgSeekTime =\n this.seekTimes.length > 0\n ? this.seekTimes.reduce((a, b) => a + b, 0) / this.seekTimes.length\n : 0;\n\n // Calculate headroom (positive = faster than target, negative = slower)\n const headroom =\n avgSeekTime > 0 ? this.TARGET_FRAME_TIME_MS - avgSeekTime : 0;\n\n // Get CPU pressure from adaptive tracker\n const trackerStats = this.adaptiveTracker.getStats();\n\n // Calculate displayed resolution from timegroup bounding rect\n const rect = this.timegroup.getBoundingClientRect();\n const renderWidth = Math.round(rect.width);\n const renderHeight = Math.round(rect.height);\n\n this.currentStats = {\n fps,\n avgRenderTime: avgSeekTime > 0 ? avgSeekTime : null,\n headroom: avgSeekTime > 0 ? headroom : null,\n pressureState: trackerStats.pressureState,\n pressureHistory: trackerStats.pressureHistory,\n renderWidth,\n renderHeight,\n resolutionScale: null, // Not applicable in DOM mode\n };\n }\n}\n\n/**\n * Factory function to create the appropriate stats tracking strategy for a presentation mode.\n * Returns null for modes that don't support stats tracking.\n */\nexport function createStatsTrackingStrategy(\n mode: PreviewPresentationMode,\n options: {\n timegroup: EFTimegroup;\n adaptiveTracker: AdaptiveResolutionTracker;\n canvasPreviewResult?: CanvasPreviewResult | null;\n compositionWidth: number;\n compositionHeight: number;\n getResolutionScale?: () => number;\n isAtRest?: () => boolean;\n isExporting?: () => boolean;\n },\n): StatsTrackingStrategy | null {\n switch (mode) {\n case \"canvas\":\n if (\n !options.canvasPreviewResult ||\n !options.getResolutionScale ||\n !options.isAtRest ||\n !options.isExporting\n ) {\n return null;\n }\n return new CanvasStatsStrategy({\n canvasPreviewResult: options.canvasPreviewResult,\n adaptiveTracker: options.adaptiveTracker,\n compositionWidth: options.compositionWidth,\n compositionHeight: options.compositionHeight,\n getResolutionScale: options.getResolutionScale,\n isAtRest: options.isAtRest,\n isExporting: options.isExporting,\n });\n\n case \"dom\":\n return new DomStatsStrategy({\n timegroup: options.timegroup,\n adaptiveTracker: options.adaptiveTracker,\n });\n\n case \"canvas\":\n return null;\n\n default:\n return null;\n }\n}\n"],"mappings":";;;;;AAsKA,IAAa,mBAAb,MAA+D;CAkB7D,YAAY,SAGT;wBAjBqC;uBAChB;wBACW,EAAE;6BACP;sBACe;uBAGrB;mBACM,EAAE;0BACgB;6BAET;8BACC;AAMtC,OAAK,YAAY,QAAQ;AACzB,OAAK,kBAAkB,QAAQ;;CAGjC,QAAc;AACZ,MAAI,KAAK,mBAAmB,KAAM;EAIlC,MAAM,oBAAoB,YAAY;AACpC,OAAI,KAAK,gBAAgB,GAAG;IAC1B,MAAM,WAAW,YAAY,KAAK,GAAG,KAAK;AAC1C,SAAK,UAAU,KAAK,SAAS;AAC7B,QAAI,KAAK,UAAU,SAAS,KAAK,oBAC/B,MAAK,UAAU,OAAO;AAExB,SAAK,gBAAgB;;;AAIzB,OAAK,UAAU,aAAa,kBAAkB;AAC9C,OAAK,yBAAyB;EAM9B,IAAI,oBAAoB,KAAK,UAAU;EACvC,MAAM,kBAAkB;GACtB,MAAM,gBAAgB,KAAK,UAAU;AACrC,OAAI,kBAAkB,mBAAmB;AAEvC,SAAK,gBAAgB,YAAY,KAAK;AACtC,wBAAoB;;;EAIxB,MAAM,QAAQ,cAAsB;AAClC,OAAI,KAAK,mBAAmB,KAAM;AAGlC,cAAW;AAGX,OAAI,KAAK,gBAAgB,GAAG;IAC1B,MAAM,WAAW,YAAY,KAAK;AAClC,SAAK,eAAe,KAAK,SAAS;AAClC,QAAI,KAAK,eAAe,SAAS,KAAK,oBACpC,MAAK,eAAe,OAAO;;AAG/B,QAAK,gBAAgB;AAGrB,OAAI,YAAY,KAAK,sBAAsB,KAAK;AAC9C,SAAK,sBAAsB;AAC3B,SAAK,aAAa;;AAGpB,QAAK,iBAAiB,sBAAsB,KAAK;;AAGnD,OAAK,iBAAiB,sBAAsB,KAAK;;CAGnD,OAAa;AACX,MAAI,KAAK,mBAAmB,MAAM;AAChC,wBAAqB,KAAK,eAAe;AACzC,QAAK,iBAAiB;;AAExB,MAAI,KAAK,kBAAkB;AACzB,QAAK,kBAAkB;AACvB,QAAK,mBAAmB;;AAE1B,OAAK,gBAAgB;AACrB,OAAK,iBAAiB,EAAE;AACxB,OAAK,gBAAgB;AACrB,OAAK,YAAY,EAAE;AACnB,OAAK,eAAe;;CAGtB,WAAiC;AAC/B,SAAO,KAAK;;CAGd,aAAa,MAAyB;AAGpC,SACE,SAAS,SACT,SAAS,gBACT,SAAS,iBACT,SAAS,gBACT,SAAS;;CAIb,AAAQ,cAAoB;EAE1B,MAAM,mBACJ,KAAK,eAAe,SAAS,IACzB,KAAK,eAAe,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAC9C,KAAK,eAAe,SACpB;EACN,MAAM,MAAM,mBAAmB,IAAI,MAAO,mBAAmB;EAG7D,MAAM,cACJ,KAAK,UAAU,SAAS,IACpB,KAAK,UAAU,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,UAAU,SAC3D;EAGN,MAAM,WACJ,cAAc,IAAI,KAAK,uBAAuB,cAAc;EAG9D,MAAM,eAAe,KAAK,gBAAgB,UAAU;EAGpD,MAAM,OAAO,KAAK,UAAU,uBAAuB;EACnD,MAAM,cAAc,KAAK,MAAM,KAAK,MAAM;EAC1C,MAAM,eAAe,KAAK,MAAM,KAAK,OAAO;AAE5C,OAAK,eAAe;GAClB;GACA,eAAe,cAAc,IAAI,cAAc;GAC/C,UAAU,cAAc,IAAI,WAAW;GACvC,eAAe,aAAa;GAC5B,iBAAiB,aAAa;GAC9B;GACA;GACA,iBAAiB;GAClB"}
1
+ {"version":3,"file":"statsTrackingStrategy.js","names":[],"sources":["../../src/preview/statsTrackingStrategy.ts"],"sourcesContent":["/**\n * Stats tracking strategy for different presentation modes.\n *\n * Uses strategy pattern to encapsulate mode-specific stats tracking logic,\n * allowing each mode to report what stats it supports and provide its own implementation.\n */\n\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { AdaptiveResolutionTracker } from \"./AdaptiveResolutionTracker.js\";\nimport type { CanvasPreviewResult } from \"./renderTimegroupToCanvas.js\";\nimport type { PreviewPresentationMode } from \"./previewSettings.js\";\n\n/**\n * Stat types that can be tracked.\n */\nexport type StatType =\n | \"fps\"\n | \"renderTime\"\n | \"headroom\"\n | \"resolution\"\n | \"resolutionScale\"\n | \"cpuPressure\"\n | \"adaptiveResolution\";\n\n/**\n * Playback statistics for display.\n */\nexport interface PlaybackStats {\n fps: number;\n avgRenderTime: number | null; // null if not measurable\n headroom: number | null; // null if not applicable\n pressureState: string;\n pressureHistory: string[];\n renderWidth: number;\n renderHeight: number;\n resolutionScale: number | null; // null if not applicable\n samplesAtCurrentScale?: number; // only for adaptive resolution\n canScaleUp?: boolean; // only for adaptive resolution\n canScaleDown?: boolean; // only for adaptive resolution\n}\n\n/**\n * Strategy interface for tracking stats in different presentation modes.\n */\nexport interface StatsTrackingStrategy {\n /** Start tracking stats (called when mode is initialized and stats are enabled) */\n start(): void;\n /** Stop tracking stats (called when mode stops or stats are disabled) */\n stop(): void;\n /** Get current stats, or null if not available */\n getStats(): PlaybackStats | null;\n /** Check if this strategy supports a specific stat type */\n supportsStat(stat: StatType): boolean;\n /**\n * Record render timing (optional - only implemented by canvas stats).\n * Called by EFWorkbench after each canvas refresh.\n */\n recordRenderTime?(renderTimeMs: number, timestamp: number): void;\n}\n\n/**\n * Canvas mode stats tracking strategy.\n * Tracks all stats including render time, headroom, resolution scale, and adaptive resolution.\n *\n * This strategy is PASSIVE - it receives render timing from EFWorkbench rather than\n * driving its own render loop. This prevents race conditions and ensures accurate measurements.\n */\nexport class CanvasStatsStrategy implements StatsTrackingStrategy {\n private adaptiveTracker: AdaptiveResolutionTracker;\n private readonly compositionWidth: number;\n private readonly compositionHeight: number;\n private getResolutionScale: () => number;\n private isAtRest: () => boolean;\n private isExporting: () => boolean;\n\n private lastStatsUpdateTime = 0;\n private currentStats: PlaybackStats | null = null;\n\n constructor(options: {\n canvasPreviewResult: CanvasPreviewResult;\n adaptiveTracker: AdaptiveResolutionTracker;\n compositionWidth: number;\n compositionHeight: number;\n getResolutionScale: () => number;\n isAtRest: () => boolean;\n isExporting: () => boolean;\n }) {\n // Note: canvasPreviewResult no longer needed since we don't call refresh()\n this.adaptiveTracker = options.adaptiveTracker;\n this.compositionWidth = options.compositionWidth;\n this.compositionHeight = options.compositionHeight;\n this.getResolutionScale = options.getResolutionScale;\n this.isAtRest = options.isAtRest;\n this.isExporting = options.isExporting;\n }\n\n start(): void {\n // Initialize stats update time\n this.lastStatsUpdateTime = performance.now();\n }\n\n stop(): void {\n this.currentStats = null;\n }\n\n /**\n * Record render timing from EFWorkbench's render loop.\n * This is called after each successful canvas refresh.\n */\n recordRenderTime(renderTimeMs: number, timestamp: number): void {\n // Skip during export\n if (this.isExporting()) return;\n\n // Only record frame timing when in motion (playing/scrubbing)\n // This prevents inflated stats at rest and focuses tracking on actual playback\n if (!this.isAtRest()) {\n this.adaptiveTracker.recordFrame(renderTimeMs, timestamp);\n }\n\n // Update playback stats every 100ms (10 times per second)\n if (timestamp - this.lastStatsUpdateTime > 100) {\n this.lastStatsUpdateTime = timestamp;\n // Get CURRENT resolution from the canvas result (may have changed dynamically)\n const currentScale = this.getResolutionScale();\n const renderWidth = Math.floor(this.compositionWidth * currentScale);\n const renderHeight = Math.floor(this.compositionHeight * currentScale);\n this.updateStats(renderWidth, renderHeight, currentScale);\n }\n }\n\n getStats(): PlaybackStats | null {\n return this.currentStats;\n }\n\n supportsStat(_stat: StatType): boolean {\n // Canvas mode supports all stats\n return true;\n }\n\n private updateStats(renderWidth: number, renderHeight: number, resolutionScale: number): void {\n const trackerStats = this.adaptiveTracker.getStats();\n\n this.currentStats = {\n fps: trackerStats.fps,\n avgRenderTime: trackerStats.avgRenderTime,\n headroom: trackerStats.headroom,\n pressureState: trackerStats.pressureState,\n pressureHistory: trackerStats.pressureHistory,\n renderWidth,\n renderHeight,\n resolutionScale,\n samplesAtCurrentScale: trackerStats.samplesAtCurrentScale,\n canScaleUp: trackerStats.canScaleUp,\n canScaleDown: trackerStats.canScaleDown,\n };\n }\n}\n\n/**\n * DOM mode stats tracking strategy.\n * Tracks FPS, resolution, CPU pressure, and frame seek time.\n */\nexport class DomStatsStrategy implements StatsTrackingStrategy {\n private timegroup: EFTimegroup;\n private adaptiveTracker: AdaptiveResolutionTracker;\n\n private animationFrame: number | null = null;\n private lastFrameTime = 0;\n private frameIntervals: number[] = [];\n private lastStatsUpdateTime = 0;\n private currentStats: PlaybackStats | null = null;\n\n // Frame seek time tracking\n private seekStartTime = 0;\n private seekTimes: number[] = [];\n private frameTaskCleanup: (() => void) | null = null;\n\n private readonly ROLLING_WINDOW_SIZE = 30; // ~1 second at 30fps\n private readonly TARGET_FRAME_TIME_MS = 33.33; // 30fps target\n\n constructor(options: { timegroup: EFTimegroup; adaptiveTracker: AdaptiveResolutionTracker }) {\n this.timegroup = options.timegroup;\n this.adaptiveTracker = options.adaptiveTracker;\n }\n\n start(): void {\n if (this.animationFrame !== null) return;\n\n // Track frame seek times using frameTask callback\n // This measures how long it takes for the timegroup to fully update after a seek\n const frameTaskCallback = async () => {\n if (this.seekStartTime > 0) {\n const seekTime = performance.now() - this.seekStartTime;\n this.seekTimes.push(seekTime);\n if (this.seekTimes.length > this.ROLLING_WINDOW_SIZE) {\n this.seekTimes.shift();\n }\n this.seekStartTime = 0; // Reset after recording\n }\n };\n\n this.timegroup.addFrameTask(frameTaskCallback);\n this.frameTaskCleanup = () => {\n // Note: EFTimegroup doesn't have removeFrameTask, but this is fine\n // The callback will be cleaned up when the timegroup is destroyed\n };\n\n // Track currentTimeMs changes to detect seeks\n let lastCurrentTimeMs = this.timegroup.currentTimeMs;\n const checkSeek = () => {\n const currentTimeMs = this.timegroup.currentTimeMs;\n if (currentTimeMs !== lastCurrentTimeMs) {\n // Seek detected - start timing\n this.seekStartTime = performance.now();\n lastCurrentTimeMs = currentTimeMs;\n }\n };\n\n const loop = (timestamp: number) => {\n if (this.animationFrame === null) return; // Stopped\n\n // Check for seeks\n checkSeek();\n\n // Track frame intervals for FPS calculation\n if (this.lastFrameTime > 0) {\n const interval = timestamp - this.lastFrameTime;\n this.frameIntervals.push(interval);\n if (this.frameIntervals.length > this.ROLLING_WINDOW_SIZE) {\n this.frameIntervals.shift();\n }\n }\n this.lastFrameTime = timestamp;\n\n // Update stats every 100ms (10 times per second)\n if (timestamp - this.lastStatsUpdateTime > 100) {\n this.lastStatsUpdateTime = timestamp;\n this.updateStats();\n }\n\n this.animationFrame = requestAnimationFrame(loop);\n };\n\n this.animationFrame = requestAnimationFrame(loop);\n }\n\n stop(): void {\n if (this.animationFrame !== null) {\n cancelAnimationFrame(this.animationFrame);\n this.animationFrame = null;\n }\n if (this.frameTaskCleanup) {\n this.frameTaskCleanup();\n this.frameTaskCleanup = null;\n }\n this.lastFrameTime = 0;\n this.frameIntervals = [];\n this.seekStartTime = 0;\n this.seekTimes = [];\n this.currentStats = null;\n }\n\n getStats(): PlaybackStats | null {\n return this.currentStats;\n }\n\n supportsStat(stat: StatType): boolean {\n // DOM mode supports: fps, resolution, cpuPressure, renderTime (seek time), headroom\n // Does NOT support: resolutionScale, adaptiveResolution\n return (\n stat === \"fps\" ||\n stat === \"resolution\" ||\n stat === \"cpuPressure\" ||\n stat === \"renderTime\" ||\n stat === \"headroom\"\n );\n }\n\n private updateStats(): void {\n // Calculate FPS from frame intervals\n const avgFrameInterval =\n this.frameIntervals.length > 0\n ? this.frameIntervals.reduce((a, b) => a + b, 0) / this.frameIntervals.length\n : 16.67;\n const fps = avgFrameInterval > 0 ? 1000 / avgFrameInterval : 0;\n\n // Calculate average seek time (frame update time)\n const avgSeekTime =\n this.seekTimes.length > 0\n ? this.seekTimes.reduce((a, b) => a + b, 0) / this.seekTimes.length\n : 0;\n\n // Calculate headroom (positive = faster than target, negative = slower)\n const headroom = avgSeekTime > 0 ? this.TARGET_FRAME_TIME_MS - avgSeekTime : 0;\n\n // Get CPU pressure from adaptive tracker\n const trackerStats = this.adaptiveTracker.getStats();\n\n // Calculate displayed resolution from timegroup bounding rect\n const rect = this.timegroup.getBoundingClientRect();\n const renderWidth = Math.round(rect.width);\n const renderHeight = Math.round(rect.height);\n\n this.currentStats = {\n fps,\n avgRenderTime: avgSeekTime > 0 ? avgSeekTime : null,\n headroom: avgSeekTime > 0 ? headroom : null,\n pressureState: trackerStats.pressureState,\n pressureHistory: trackerStats.pressureHistory,\n renderWidth,\n renderHeight,\n resolutionScale: null, // Not applicable in DOM mode\n };\n }\n}\n\n/**\n * Factory function to create the appropriate stats tracking strategy for a presentation mode.\n * Returns null for modes that don't support stats tracking.\n */\nexport function createStatsTrackingStrategy(\n mode: PreviewPresentationMode,\n options: {\n timegroup: EFTimegroup;\n adaptiveTracker: AdaptiveResolutionTracker;\n canvasPreviewResult?: CanvasPreviewResult | null;\n compositionWidth: number;\n compositionHeight: number;\n getResolutionScale?: () => number;\n isAtRest?: () => boolean;\n isExporting?: () => boolean;\n },\n): StatsTrackingStrategy | null {\n switch (mode) {\n case \"canvas\":\n if (\n !options.canvasPreviewResult ||\n !options.getResolutionScale ||\n !options.isAtRest ||\n !options.isExporting\n ) {\n return null;\n }\n return new CanvasStatsStrategy({\n canvasPreviewResult: options.canvasPreviewResult,\n adaptiveTracker: options.adaptiveTracker,\n compositionWidth: options.compositionWidth,\n compositionHeight: options.compositionHeight,\n getResolutionScale: options.getResolutionScale,\n isAtRest: options.isAtRest,\n isExporting: options.isExporting,\n });\n\n case \"dom\":\n return new DomStatsStrategy({\n timegroup: options.timegroup,\n adaptiveTracker: options.adaptiveTracker,\n });\n\n default:\n return null;\n }\n}\n"],"mappings":";;;;;AAkKA,IAAa,mBAAb,MAA+D;CAkB7D,YAAY,SAAiF;wBAdrD;uBAChB;wBACW,EAAE;6BACP;sBACe;uBAGrB;mBACM,EAAE;0BACgB;6BAET;8BACC;AAGtC,OAAK,YAAY,QAAQ;AACzB,OAAK,kBAAkB,QAAQ;;CAGjC,QAAc;AACZ,MAAI,KAAK,mBAAmB,KAAM;EAIlC,MAAM,oBAAoB,YAAY;AACpC,OAAI,KAAK,gBAAgB,GAAG;IAC1B,MAAM,WAAW,YAAY,KAAK,GAAG,KAAK;AAC1C,SAAK,UAAU,KAAK,SAAS;AAC7B,QAAI,KAAK,UAAU,SAAS,KAAK,oBAC/B,MAAK,UAAU,OAAO;AAExB,SAAK,gBAAgB;;;AAIzB,OAAK,UAAU,aAAa,kBAAkB;AAC9C,OAAK,yBAAyB;EAM9B,IAAI,oBAAoB,KAAK,UAAU;EACvC,MAAM,kBAAkB;GACtB,MAAM,gBAAgB,KAAK,UAAU;AACrC,OAAI,kBAAkB,mBAAmB;AAEvC,SAAK,gBAAgB,YAAY,KAAK;AACtC,wBAAoB;;;EAIxB,MAAM,QAAQ,cAAsB;AAClC,OAAI,KAAK,mBAAmB,KAAM;AAGlC,cAAW;AAGX,OAAI,KAAK,gBAAgB,GAAG;IAC1B,MAAM,WAAW,YAAY,KAAK;AAClC,SAAK,eAAe,KAAK,SAAS;AAClC,QAAI,KAAK,eAAe,SAAS,KAAK,oBACpC,MAAK,eAAe,OAAO;;AAG/B,QAAK,gBAAgB;AAGrB,OAAI,YAAY,KAAK,sBAAsB,KAAK;AAC9C,SAAK,sBAAsB;AAC3B,SAAK,aAAa;;AAGpB,QAAK,iBAAiB,sBAAsB,KAAK;;AAGnD,OAAK,iBAAiB,sBAAsB,KAAK;;CAGnD,OAAa;AACX,MAAI,KAAK,mBAAmB,MAAM;AAChC,wBAAqB,KAAK,eAAe;AACzC,QAAK,iBAAiB;;AAExB,MAAI,KAAK,kBAAkB;AACzB,QAAK,kBAAkB;AACvB,QAAK,mBAAmB;;AAE1B,OAAK,gBAAgB;AACrB,OAAK,iBAAiB,EAAE;AACxB,OAAK,gBAAgB;AACrB,OAAK,YAAY,EAAE;AACnB,OAAK,eAAe;;CAGtB,WAAiC;AAC/B,SAAO,KAAK;;CAGd,aAAa,MAAyB;AAGpC,SACE,SAAS,SACT,SAAS,gBACT,SAAS,iBACT,SAAS,gBACT,SAAS;;CAIb,AAAQ,cAAoB;EAE1B,MAAM,mBACJ,KAAK,eAAe,SAAS,IACzB,KAAK,eAAe,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,eAAe,SACrE;EACN,MAAM,MAAM,mBAAmB,IAAI,MAAO,mBAAmB;EAG7D,MAAM,cACJ,KAAK,UAAU,SAAS,IACpB,KAAK,UAAU,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,UAAU,SAC3D;EAGN,MAAM,WAAW,cAAc,IAAI,KAAK,uBAAuB,cAAc;EAG7E,MAAM,eAAe,KAAK,gBAAgB,UAAU;EAGpD,MAAM,OAAO,KAAK,UAAU,uBAAuB;EACnD,MAAM,cAAc,KAAK,MAAM,KAAK,MAAM;EAC1C,MAAM,eAAe,KAAK,MAAM,KAAK,OAAO;AAE5C,OAAK,eAAe;GAClB;GACA,eAAe,cAAc,IAAI,cAAc;GAC/C,UAAU,cAAc,IAAI,WAAW;GACvC,eAAe,aAAa;GAC5B,iBAAiB,aAAa;GAC9B;GACA;GACA,iBAAiB;GAClB"}
@@ -1 +1 @@
1
- {"version":3,"file":"WorkerPool.js","names":["poolSize: number","testTimeout: number | null","testHandler: ((event: MessageEvent) => void) | null"],"sources":["../../../src/preview/workers/WorkerPool.ts"],"sourcesContent":["/**\n * Worker pool for parallel task execution.\n * Manages a pool of workers and distributes tasks across them.\n */\n\nimport { logger } from \"../logger.js\";\n\n// Constants\nconst WORKER_INIT_TEST_TIMEOUT_MS = 2000;\n\ninterface QueuedTask<T> {\n resolve: (value: T) => void;\n reject: (error: Error) => void;\n task: (worker: Worker) => Promise<T>;\n}\n\nexport class WorkerPool {\n private workers: Worker[] = [];\n private availableWorkers: Worker[] = [];\n private taskQueue: QueuedTask<unknown>[] = [];\n private isTerminated = false;\n private workerUrl: string;\n\n constructor(\n workerScriptUrl: string,\n private poolSize: number = navigator.hardwareConcurrency || 4,\n ) {\n this.workerUrl = workerScriptUrl;\n\n // Check browser support first, then initialize workers\n if (this.hasBrowserSupport()) {\n this.initializeWorkers();\n }\n }\n\n /**\n * Check if browser supports workers (before initialization).\n */\n private hasBrowserSupport(): boolean {\n return (\n typeof Worker !== \"undefined\" &&\n typeof OffscreenCanvas !== \"undefined\" &&\n typeof createImageBitmap !== \"undefined\"\n );\n }\n\n private initializeWorkers(): void {\n for (let i = 0; i < this.poolSize; i++) {\n try {\n // Create worker from URL (typically a blob URL from inlined worker code)\n const worker = new Worker(this.workerUrl, { type: \"module\" });\n\n // Test if worker is responding - cleanup handler after confirmation\n let testTimeout: number | null = null;\n let testHandler: ((event: MessageEvent) => void) | null = null;\n\n const cleanupTest = () => {\n if (testTimeout !== null) {\n clearTimeout(testTimeout);\n testTimeout = null;\n }\n if (testHandler !== null) {\n worker.removeEventListener(\"message\", testHandler);\n testHandler = null;\n }\n };\n\n testTimeout = window.setTimeout(() => {\n cleanupTest();\n }, WORKER_INIT_TEST_TIMEOUT_MS);\n\n testHandler = (event: MessageEvent) => {\n // Check if this is a test response (worker startup message)\n if (\n event.data &&\n typeof event.data === \"string\" &&\n event.data.includes(\"encoderWorker\")\n ) {\n cleanupTest();\n }\n };\n worker.addEventListener(\"message\", testHandler);\n\n worker.onerror = (error) => {\n cleanupTest();\n logger.error(`[WorkerPool] Worker ${i} error:`, {\n message: error.message,\n filename: error.filename,\n lineno: error.lineno,\n colno: error.colno,\n });\n };\n worker.onmessageerror = (error) => {\n logger.error(`[WorkerPool] Worker ${i} message error:`, error);\n };\n this.workers.push(worker);\n this.availableWorkers.push(worker);\n } catch (error) {\n logger.error(\n `[WorkerPool] Failed to create worker ${i}:`,\n error instanceof Error ? error.message : String(error),\n );\n }\n }\n if (this.workers.length === 0) {\n logger.error(\n `[WorkerPool] Failed to create any workers. URL: ${this.workerUrl}`,\n );\n logger.error(`[WorkerPool] Browser support check:`, {\n Worker: typeof Worker !== \"undefined\",\n OffscreenCanvas: typeof OffscreenCanvas !== \"undefined\",\n createImageBitmap: typeof createImageBitmap !== \"undefined\",\n });\n }\n }\n\n /**\n * Get the number of workers in the pool.\n */\n get workerCount(): number {\n return this.workers.length;\n }\n\n /**\n * Check if workers are available and initialized.\n */\n isAvailable(): boolean {\n return (\n this.hasBrowserSupport() && this.workers.length > 0 && !this.isTerminated\n );\n }\n\n /**\n * Execute a task using an available worker from the pool.\n */\n async execute<T>(task: (worker: Worker) => Promise<T>): Promise<T> {\n if (this.isTerminated) {\n throw new Error(\"WorkerPool has been terminated\");\n }\n\n // If workers aren't available, this will be handled by the caller's fallback\n if (!this.isAvailable()) {\n throw new Error(\"Workers not available\");\n }\n\n return new Promise<T>((resolve, reject) => {\n this.taskQueue.push({\n resolve: resolve as (value: unknown) => void,\n reject,\n task,\n });\n this.processQueue();\n });\n }\n\n private processQueue(): void {\n // Process tasks while we have available workers and queued tasks\n while (this.availableWorkers.length > 0 && this.taskQueue.length > 0) {\n const worker = this.availableWorkers.shift();\n const queuedTask = this.taskQueue.shift();\n\n if (!worker || !queuedTask) {\n // Safety check - should not happen but prevents crashes\n break;\n }\n\n const { resolve, reject, task } = queuedTask;\n\n // Execute the task\n task(worker)\n .then((result) => {\n resolve(result);\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n })\n .catch((error) => {\n reject(error instanceof Error ? error : new Error(String(error)));\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n });\n }\n }\n\n /**\n * Terminate all workers and clear the task queue.\n */\n terminate(): void {\n this.isTerminated = true;\n\n // Reject all pending tasks\n for (const { reject } of this.taskQueue) {\n reject(new Error(\"WorkerPool terminated\"));\n }\n this.taskQueue = [];\n\n // Terminate all workers\n for (const worker of this.workers) {\n worker.terminate();\n }\n this.workers = [];\n this.availableWorkers = [];\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"WorkerPool.js","names":["poolSize: number","testTimeout: number | null","testHandler: ((event: MessageEvent) => void) | null"],"sources":["../../../src/preview/workers/WorkerPool.ts"],"sourcesContent":["/**\n * Worker pool for parallel task execution.\n * Manages a pool of workers and distributes tasks across them.\n */\n\nimport { logger } from \"../logger.js\";\n\n// Constants\nconst WORKER_INIT_TEST_TIMEOUT_MS = 2000;\n\ninterface QueuedTask<T> {\n resolve: (value: T) => void;\n reject: (error: Error) => void;\n task: (worker: Worker) => Promise<T>;\n}\n\nexport class WorkerPool {\n private workers: Worker[] = [];\n private availableWorkers: Worker[] = [];\n private taskQueue: QueuedTask<unknown>[] = [];\n private isTerminated = false;\n private workerUrl: string;\n\n constructor(\n workerScriptUrl: string,\n private poolSize: number = navigator.hardwareConcurrency || 4,\n ) {\n this.workerUrl = workerScriptUrl;\n\n // Check browser support first, then initialize workers\n if (this.hasBrowserSupport()) {\n this.initializeWorkers();\n }\n }\n\n /**\n * Check if browser supports workers (before initialization).\n */\n private hasBrowserSupport(): boolean {\n return (\n typeof Worker !== \"undefined\" &&\n typeof OffscreenCanvas !== \"undefined\" &&\n typeof createImageBitmap !== \"undefined\"\n );\n }\n\n private initializeWorkers(): void {\n for (let i = 0; i < this.poolSize; i++) {\n try {\n // Create worker from URL (typically a blob URL from inlined worker code)\n const worker = new Worker(this.workerUrl, { type: \"module\" });\n\n // Test if worker is responding - cleanup handler after confirmation\n let testTimeout: number | null = null;\n let testHandler: ((event: MessageEvent) => void) | null = null;\n\n const cleanupTest = () => {\n if (testTimeout !== null) {\n clearTimeout(testTimeout);\n testTimeout = null;\n }\n if (testHandler !== null) {\n worker.removeEventListener(\"message\", testHandler);\n testHandler = null;\n }\n };\n\n testTimeout = window.setTimeout(() => {\n cleanupTest();\n }, WORKER_INIT_TEST_TIMEOUT_MS);\n\n testHandler = (event: MessageEvent) => {\n // Check if this is a test response (worker startup message)\n if (\n event.data &&\n typeof event.data === \"string\" &&\n event.data.includes(\"encoderWorker\")\n ) {\n cleanupTest();\n }\n };\n worker.addEventListener(\"message\", testHandler);\n\n worker.onerror = (error) => {\n cleanupTest();\n logger.error(`[WorkerPool] Worker ${i} error:`, {\n message: error.message,\n filename: error.filename,\n lineno: error.lineno,\n colno: error.colno,\n });\n };\n worker.onmessageerror = (error) => {\n logger.error(`[WorkerPool] Worker ${i} message error:`, error);\n };\n this.workers.push(worker);\n this.availableWorkers.push(worker);\n } catch (error) {\n logger.error(\n `[WorkerPool] Failed to create worker ${i}:`,\n error instanceof Error ? error.message : String(error),\n );\n }\n }\n if (this.workers.length === 0) {\n logger.error(`[WorkerPool] Failed to create any workers. URL: ${this.workerUrl}`);\n logger.error(`[WorkerPool] Browser support check:`, {\n Worker: typeof Worker !== \"undefined\",\n OffscreenCanvas: typeof OffscreenCanvas !== \"undefined\",\n createImageBitmap: typeof createImageBitmap !== \"undefined\",\n });\n }\n }\n\n /**\n * Get the number of workers in the pool.\n */\n get workerCount(): number {\n return this.workers.length;\n }\n\n /**\n * Check if workers are available and initialized.\n */\n isAvailable(): boolean {\n return this.hasBrowserSupport() && this.workers.length > 0 && !this.isTerminated;\n }\n\n /**\n * Execute a task using an available worker from the pool.\n */\n async execute<T>(task: (worker: Worker) => Promise<T>): Promise<T> {\n if (this.isTerminated) {\n throw new Error(\"WorkerPool has been terminated\");\n }\n\n // If workers aren't available, this will be handled by the caller's fallback\n if (!this.isAvailable()) {\n throw new Error(\"Workers not available\");\n }\n\n return new Promise<T>((resolve, reject) => {\n this.taskQueue.push({\n resolve: resolve as (value: unknown) => void,\n reject,\n task,\n });\n this.processQueue();\n });\n }\n\n private processQueue(): void {\n // Process tasks while we have available workers and queued tasks\n while (this.availableWorkers.length > 0 && this.taskQueue.length > 0) {\n const worker = this.availableWorkers.shift();\n const queuedTask = this.taskQueue.shift();\n\n if (!worker || !queuedTask) {\n // Safety check - should not happen but prevents crashes\n break;\n }\n\n const { resolve, reject, task } = queuedTask;\n\n // Execute the task\n task(worker)\n .then((result) => {\n resolve(result);\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n })\n .catch((error) => {\n reject(error instanceof Error ? error : new Error(String(error)));\n // Return worker to pool\n this.availableWorkers.push(worker);\n // Process next task\n this.processQueue();\n });\n }\n }\n\n /**\n * Terminate all workers and clear the task queue.\n */\n terminate(): void {\n this.isTerminated = true;\n\n // Reject all pending tasks\n for (const { reject } of this.taskQueue) {\n reject(new Error(\"WorkerPool terminated\"));\n }\n this.taskQueue = [];\n\n // Terminate all workers\n for (const worker of this.workers) {\n worker.terminate();\n }\n this.workers = [];\n this.availableWorkers = [];\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"EFRenderAPI.js","names":["api: IEFRenderAPI"],"sources":["../../src/render/EFRenderAPI.ts"],"sourcesContent":["/**\n * Window API for programmatic video rendering.\n *\n * Exposes renderTimegroupToVideo for use from Playwright/CLI.\n * Supports streaming output and custom data injection.\n */\n\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { EFWorkbench } from \"../gui/EFWorkbench.js\";\nimport { getRenderInfo, type RenderInfo } from \"../getRenderInfo.js\";\n// Import only types - actual function loaded dynamically\nimport type {\n RenderToVideoOptions,\n RenderProgress,\n} from \"../preview/renderTimegroupToVideo.types.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface IEFRenderAPI {\n /**\n * Render with streaming output (calls window.onRenderChunk for each chunk).\n * Use this for CLI/Playwright to avoid memory buffering.\n */\n renderStreaming(options?: RenderToVideoOptions): Promise<void>;\n\n /**\n * Render and return buffer (for shorter videos or in-browser use).\n * Returns the video as Uint8Array.\n */\n render(options?: RenderToVideoOptions): Promise<Uint8Array>;\n\n /**\n * Get render info (dimensions, duration, assets).\n * Same as the exported getRenderInfo function.\n */\n getRenderInfo(): Promise<RenderInfo>;\n\n /**\n * Check if SDK is ready for rendering.\n * Returns true if a root timegroup is found.\n */\n isReady(): boolean;\n}\n\ndeclare global {\n interface Window {\n EF_RENDER?: IEFRenderAPI;\n EF_RENDER_DATA?: Record<string, unknown>;\n onRenderChunk?: (chunk: Uint8Array) => void; // Set by Playwright\n onRenderProgress?: (progress: RenderProgress) => void; // Optional progress callback\n }\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\nfunction findRootTimegroup(): EFTimegroup | null {\n // Try to find timegroup from workbench first\n const workbench = document.querySelector(\n \"ef-workbench\",\n ) as EFWorkbench | null;\n if (workbench) {\n const timegroup = workbench.querySelector(\n \"ef-timegroup\",\n ) as EFTimegroup | null;\n if (timegroup) {\n return timegroup;\n }\n }\n\n // Fallback: find first root timegroup\n const rootTimegroup = document.querySelector(\n \"ef-timegroup\",\n ) as EFTimegroup | null;\n return rootTimegroup;\n}\n\nfunction setWorkbenchRendering(rendering: boolean): void {\n const workbench = document.querySelector(\n \"ef-workbench\",\n ) as EFWorkbench | null;\n if (workbench) {\n workbench.rendering = rendering;\n }\n}\n\nasync function waitForTimegroupDimensions(\n timegroup: EFTimegroup,\n): Promise<void> {\n await Promise.all(\n Array.from(document.styleSheets).map((sheet) => {\n if (sheet.href) {\n const link = Array.from(\n document.querySelectorAll('link[rel=\"stylesheet\"]'),\n ).find((l) => (l as HTMLLinkElement).href === sheet.href);\n if (link && !(link as HTMLLinkElement).sheet) {\n return new Promise((resolve) => {\n link.addEventListener(\"load\", resolve);\n link.addEventListener(\"error\", resolve);\n });\n }\n }\n return Promise.resolve();\n }),\n );\n\n // Force layout immediately after stylesheets load\n void timegroup.offsetHeight;\n\n const rect = timegroup.getBoundingClientRect();\n const hasOffset = timegroup.offsetWidth > 0 && timegroup.offsetHeight > 0;\n const hasRect = rect.width > 0 && rect.height > 0;\n const computedWidth = getComputedStyle(timegroup).width;\n const computedHeight = getComputedStyle(timegroup).height;\n const hasComputed =\n parseFloat(computedWidth) > 0 && parseFloat(computedHeight) > 0;\n\n if (!hasOffset && !hasRect && !hasComputed) {\n throw new Error(\n `Timegroup has no dimensions (${timegroup.offsetWidth}x${timegroup.offsetHeight}). ` +\n `Computed styles: width=${computedWidth}, height=${computedHeight}. ` +\n `Classes: \"${timegroup.className}\". ` +\n `\\n\\nTailwind CSS did not generate styles for these classes. ` +\n `Check that:\\n` +\n `1. Your Tailwind config 'content' array includes the HTML file\\n` +\n `2. Tailwind CSS is properly configured in your project\\n` +\n `3. The dev server successfully compiled CSS (check for Tailwind warnings above)`,\n );\n }\n}\n\nconst api: IEFRenderAPI = {\n async renderStreaming(options: RenderToVideoOptions = {}): Promise<void> {\n const timegroup = findRootTimegroup();\n if (!timegroup) {\n throw new Error(\"No ef-timegroup found. Cannot render.\");\n }\n\n // Check if window.onRenderChunk is available\n if (typeof window === \"undefined\" || !window.onRenderChunk) {\n throw new Error(\n \"window.onRenderChunk is not set. \" +\n \"Call page.exposeFunction('onRenderChunk', callback) from Playwright first.\",\n );\n }\n\n // Hide workbench UI during render\n setWorkbenchRendering(true);\n\n try {\n // Wait for timegroup to have dimensions\n await waitForTimegroupDimensions(timegroup);\n\n // Wait for media to be ready\n await timegroup.waitForMediaDurations();\n\n // Create custom writable stream that calls window.onRenderChunk\n const chunkWriter = new WritableStream<Uint8Array>({\n write(chunk: Uint8Array) {\n if (window.onRenderChunk) {\n window.onRenderChunk(chunk);\n }\n },\n });\n\n // Merge progress callback if window.onRenderProgress is set\n const onProgress = options.onProgress || window.onRenderProgress;\n\n // Render with custom stream\n // Dynamic import to avoid loading render utilities during module initialization\n const { renderTimegroupToVideo } =\n await import(\"../preview/renderTimegroupToVideo.js\");\n await renderTimegroupToVideo(timegroup, {\n ...options,\n customWritableStream: chunkWriter,\n onProgress,\n returnBuffer: false,\n });\n } finally {\n // Restore workbench UI\n setWorkbenchRendering(false);\n }\n },\n\n async render(options: RenderToVideoOptions = {}): Promise<Uint8Array> {\n const timegroup = findRootTimegroup();\n if (!timegroup) {\n throw new Error(\"No ef-timegroup found. Cannot render.\");\n }\n\n // Hide workbench UI during render\n setWorkbenchRendering(true);\n\n try {\n // Wait for timegroup to have dimensions\n await waitForTimegroupDimensions(timegroup);\n\n // Wait for media to be ready\n await timegroup.waitForMediaDurations();\n\n // Merge progress callback if window.onRenderProgress is set\n const onProgress = options.onProgress || window.onRenderProgress;\n\n // Dynamic import to avoid loading render utilities during module initialization\n const { renderTimegroupToVideo } =\n await import(\"../preview/renderTimegroupToVideo.js\");\n const buffer = await renderTimegroupToVideo(timegroup, {\n ...options,\n returnBuffer: true,\n onProgress,\n });\n\n if (!buffer) {\n throw new Error(\"Render failed: no buffer returned\");\n }\n\n return buffer;\n } finally {\n // Restore workbench UI\n setWorkbenchRendering(false);\n }\n },\n\n async getRenderInfo(): Promise<RenderInfo> {\n return getRenderInfo();\n },\n\n isReady(): boolean {\n return findRootTimegroup() !== null;\n },\n};\n\n// Export and register on window\nif (typeof window !== \"undefined\") {\n window.EF_RENDER = api;\n}\n\nexport { api as EFRenderAPI };\nexport type { IEFRenderAPI as EFRenderAPIInterface };\n"],"mappings":";;;AA2DA,SAAS,oBAAwC;CAE/C,MAAM,YAAY,SAAS,cACzB,eACD;AACD,KAAI,WAAW;EACb,MAAM,YAAY,UAAU,cAC1B,eACD;AACD,MAAI,UACF,QAAO;;AAQX,QAHsB,SAAS,cAC7B,eACD;;AAIH,SAAS,sBAAsB,WAA0B;CACvD,MAAM,YAAY,SAAS,cACzB,eACD;AACD,KAAI,UACF,WAAU,YAAY;;AAI1B,eAAe,2BACb,WACe;AACf,OAAM,QAAQ,IACZ,MAAM,KAAK,SAAS,YAAY,CAAC,KAAK,UAAU;AAC9C,MAAI,MAAM,MAAM;GACd,MAAM,OAAO,MAAM,KACjB,SAAS,iBAAiB,2BAAyB,CACpD,CAAC,MAAM,MAAO,EAAsB,SAAS,MAAM,KAAK;AACzD,OAAI,QAAQ,CAAE,KAAyB,MACrC,QAAO,IAAI,SAAS,YAAY;AAC9B,SAAK,iBAAiB,QAAQ,QAAQ;AACtC,SAAK,iBAAiB,SAAS,QAAQ;KACvC;;AAGN,SAAO,QAAQ,SAAS;GACxB,CACH;AAGD,CAAK,UAAU;CAEf,MAAM,OAAO,UAAU,uBAAuB;CAC9C,MAAM,YAAY,UAAU,cAAc,KAAK,UAAU,eAAe;CACxE,MAAM,UAAU,KAAK,QAAQ,KAAK,KAAK,SAAS;CAChD,MAAM,gBAAgB,iBAAiB,UAAU,CAAC;CAClD,MAAM,iBAAiB,iBAAiB,UAAU,CAAC;AAInD,KAAI,CAAC,aAAa,CAAC,WAAW,EAF5B,WAAW,cAAc,GAAG,KAAK,WAAW,eAAe,GAAG,GAG9D,OAAM,IAAI,MACR,gCAAgC,UAAU,YAAY,GAAG,UAAU,aAAa,4BACpD,cAAc,WAAW,eAAe,cACrD,UAAU,UAAU,qRAMpC;;AAIL,MAAMA,MAAoB;CACxB,MAAM,gBAAgB,UAAgC,EAAE,EAAiB;EACvE,MAAM,YAAY,mBAAmB;AACrC,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,wCAAwC;AAI1D,MAAI,OAAO,WAAW,eAAe,CAAC,OAAO,cAC3C,OAAM,IAAI,MACR,8GAED;AAIH,wBAAsB,KAAK;AAE3B,MAAI;AAEF,SAAM,2BAA2B,UAAU;AAG3C,SAAM,UAAU,uBAAuB;GAGvC,MAAM,cAAc,IAAI,eAA2B,EACjD,MAAM,OAAmB;AACvB,QAAI,OAAO,cACT,QAAO,cAAc,MAAM;MAGhC,CAAC;GAGF,MAAM,aAAa,QAAQ,cAAc,OAAO;GAIhD,MAAM,EAAE,2BACN,MAAM,OAAO;AACf,SAAM,uBAAuB,WAAW;IACtC,GAAG;IACH,sBAAsB;IACtB;IACA,cAAc;IACf,CAAC;YACM;AAER,yBAAsB,MAAM;;;CAIhC,MAAM,OAAO,UAAgC,EAAE,EAAuB;EACpE,MAAM,YAAY,mBAAmB;AACrC,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,wCAAwC;AAI1D,wBAAsB,KAAK;AAE3B,MAAI;AAEF,SAAM,2BAA2B,UAAU;AAG3C,SAAM,UAAU,uBAAuB;GAGvC,MAAM,aAAa,QAAQ,cAAc,OAAO;GAGhD,MAAM,EAAE,2BACN,MAAM,OAAO;GACf,MAAM,SAAS,MAAM,uBAAuB,WAAW;IACrD,GAAG;IACH,cAAc;IACd;IACD,CAAC;AAEF,OAAI,CAAC,OACH,OAAM,IAAI,MAAM,oCAAoC;AAGtD,UAAO;YACC;AAER,yBAAsB,MAAM;;;CAIhC,MAAM,gBAAqC;AACzC,SAAO,eAAe;;CAGxB,UAAmB;AACjB,SAAO,mBAAmB,KAAK;;CAElC;AAGD,IAAI,OAAO,WAAW,YACpB,QAAO,YAAY"}
1
+ {"version":3,"file":"EFRenderAPI.js","names":["api: IEFRenderAPI"],"sources":["../../src/render/EFRenderAPI.ts"],"sourcesContent":["/**\n * Window API for programmatic video rendering.\n *\n * Exposes renderTimegroupToVideo for use from Playwright/CLI.\n * Supports streaming output and custom data injection.\n */\n\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { EFWorkbench } from \"../gui/EFWorkbench.js\";\nimport { getRenderInfo, type RenderInfo } from \"../getRenderInfo.js\";\n// Import only types - actual function loaded dynamically\nimport type {\n RenderToVideoOptions,\n RenderProgress,\n} from \"../preview/renderTimegroupToVideo.types.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface IEFRenderAPI {\n /**\n * Render with streaming output (calls window.onRenderChunk for each chunk).\n * Use this for CLI/Playwright to avoid memory buffering.\n */\n renderStreaming(options?: RenderToVideoOptions): Promise<void>;\n\n /**\n * Render and return buffer (for shorter videos or in-browser use).\n * Returns the video as Uint8Array.\n */\n render(options?: RenderToVideoOptions): Promise<Uint8Array>;\n\n /**\n * Get render info (dimensions, duration, assets).\n * Same as the exported getRenderInfo function.\n */\n getRenderInfo(): Promise<RenderInfo>;\n\n /**\n * Check if SDK is ready for rendering.\n * Returns true if a root timegroup is found.\n */\n isReady(): boolean;\n}\n\ndeclare global {\n interface Window {\n EF_RENDER?: IEFRenderAPI;\n EF_RENDER_DATA?: Record<string, unknown>;\n onRenderChunk?: (chunk: Uint8Array) => void; // Set by Playwright\n onRenderProgress?: (progress: RenderProgress) => void; // Optional progress callback\n }\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\nfunction findRootTimegroup(): EFTimegroup | null {\n // Try to find timegroup from workbench first\n const workbench = document.querySelector(\"ef-workbench\") as EFWorkbench | null;\n if (workbench) {\n const timegroup = workbench.querySelector(\"ef-timegroup\") as EFTimegroup | null;\n if (timegroup) {\n return timegroup;\n }\n }\n\n // Fallback: find first root timegroup\n const rootTimegroup = document.querySelector(\"ef-timegroup\") as EFTimegroup | null;\n return rootTimegroup;\n}\n\nfunction setWorkbenchRendering(rendering: boolean): void {\n const workbench = document.querySelector(\"ef-workbench\") as EFWorkbench | null;\n if (workbench) {\n workbench.rendering = rendering;\n }\n}\n\nasync function waitForTimegroupDimensions(timegroup: EFTimegroup): Promise<void> {\n await Promise.all(\n Array.from(document.styleSheets).map((sheet) => {\n if (sheet.href) {\n const link = Array.from(document.querySelectorAll('link[rel=\"stylesheet\"]')).find(\n (l) => (l as HTMLLinkElement).href === sheet.href,\n );\n if (link && !(link as HTMLLinkElement).sheet) {\n return new Promise((resolve) => {\n link.addEventListener(\"load\", resolve);\n link.addEventListener(\"error\", resolve);\n });\n }\n }\n return Promise.resolve();\n }),\n );\n\n // Force layout immediately after stylesheets load\n void timegroup.offsetHeight;\n\n const rect = timegroup.getBoundingClientRect();\n const hasOffset = timegroup.offsetWidth > 0 && timegroup.offsetHeight > 0;\n const hasRect = rect.width > 0 && rect.height > 0;\n const computedWidth = getComputedStyle(timegroup).width;\n const computedHeight = getComputedStyle(timegroup).height;\n const hasComputed = parseFloat(computedWidth) > 0 && parseFloat(computedHeight) > 0;\n\n if (!hasOffset && !hasRect && !hasComputed) {\n throw new Error(\n `Timegroup has no dimensions (${timegroup.offsetWidth}x${timegroup.offsetHeight}). ` +\n `Computed styles: width=${computedWidth}, height=${computedHeight}. ` +\n `Classes: \"${timegroup.className}\". ` +\n `\\n\\nTailwind CSS did not generate styles for these classes. ` +\n `Check that:\\n` +\n `1. Your Tailwind config 'content' array includes the HTML file\\n` +\n `2. Tailwind CSS is properly configured in your project\\n` +\n `3. The dev server successfully compiled CSS (check for Tailwind warnings above)`,\n );\n }\n}\n\nconst api: IEFRenderAPI = {\n async renderStreaming(options: RenderToVideoOptions = {}): Promise<void> {\n const timegroup = findRootTimegroup();\n if (!timegroup) {\n throw new Error(\"No ef-timegroup found. Cannot render.\");\n }\n\n // Check if window.onRenderChunk is available\n if (typeof window === \"undefined\" || !window.onRenderChunk) {\n throw new Error(\n \"window.onRenderChunk is not set. \" +\n \"Call page.exposeFunction('onRenderChunk', callback) from Playwright first.\",\n );\n }\n\n // Hide workbench UI during render\n setWorkbenchRendering(true);\n\n try {\n // Wait for timegroup to have dimensions\n await waitForTimegroupDimensions(timegroup);\n\n // Wait for media to be ready\n await timegroup.waitForMediaDurations();\n\n // Create custom writable stream that calls window.onRenderChunk\n const chunkWriter = new WritableStream<Uint8Array>({\n write(chunk: Uint8Array) {\n if (window.onRenderChunk) {\n window.onRenderChunk(chunk);\n }\n },\n });\n\n // Merge progress callback if window.onRenderProgress is set\n const onProgress = options.onProgress || window.onRenderProgress;\n\n // Render with custom stream\n // Dynamic import to avoid loading render utilities during module initialization\n const { renderTimegroupToVideo } = await import(\"../preview/renderTimegroupToVideo.js\");\n await renderTimegroupToVideo(timegroup, {\n ...options,\n customWritableStream: chunkWriter,\n onProgress,\n returnBuffer: false,\n });\n } finally {\n // Restore workbench UI\n setWorkbenchRendering(false);\n }\n },\n\n async render(options: RenderToVideoOptions = {}): Promise<Uint8Array> {\n const timegroup = findRootTimegroup();\n if (!timegroup) {\n throw new Error(\"No ef-timegroup found. Cannot render.\");\n }\n\n // Hide workbench UI during render\n setWorkbenchRendering(true);\n\n try {\n // Wait for timegroup to have dimensions\n await waitForTimegroupDimensions(timegroup);\n\n // Wait for media to be ready\n await timegroup.waitForMediaDurations();\n\n // Merge progress callback if window.onRenderProgress is set\n const onProgress = options.onProgress || window.onRenderProgress;\n\n // Dynamic import to avoid loading render utilities during module initialization\n const { renderTimegroupToVideo } = await import(\"../preview/renderTimegroupToVideo.js\");\n const buffer = await renderTimegroupToVideo(timegroup, {\n ...options,\n returnBuffer: true,\n onProgress,\n });\n\n if (!buffer) {\n throw new Error(\"Render failed: no buffer returned\");\n }\n\n return buffer;\n } finally {\n // Restore workbench UI\n setWorkbenchRendering(false);\n }\n },\n\n async getRenderInfo(): Promise<RenderInfo> {\n return getRenderInfo();\n },\n\n isReady(): boolean {\n return findRootTimegroup() !== null;\n },\n};\n\n// Export and register on window\nif (typeof window !== \"undefined\") {\n window.EF_RENDER = api;\n}\n\nexport { api as EFRenderAPI };\nexport type { IEFRenderAPI as EFRenderAPIInterface };\n"],"mappings":";;;AA2DA,SAAS,oBAAwC;CAE/C,MAAM,YAAY,SAAS,cAAc,eAAe;AACxD,KAAI,WAAW;EACb,MAAM,YAAY,UAAU,cAAc,eAAe;AACzD,MAAI,UACF,QAAO;;AAMX,QADsB,SAAS,cAAc,eAAe;;AAI9D,SAAS,sBAAsB,WAA0B;CACvD,MAAM,YAAY,SAAS,cAAc,eAAe;AACxD,KAAI,UACF,WAAU,YAAY;;AAI1B,eAAe,2BAA2B,WAAuC;AAC/E,OAAM,QAAQ,IACZ,MAAM,KAAK,SAAS,YAAY,CAAC,KAAK,UAAU;AAC9C,MAAI,MAAM,MAAM;GACd,MAAM,OAAO,MAAM,KAAK,SAAS,iBAAiB,2BAAyB,CAAC,CAAC,MAC1E,MAAO,EAAsB,SAAS,MAAM,KAC9C;AACD,OAAI,QAAQ,CAAE,KAAyB,MACrC,QAAO,IAAI,SAAS,YAAY;AAC9B,SAAK,iBAAiB,QAAQ,QAAQ;AACtC,SAAK,iBAAiB,SAAS,QAAQ;KACvC;;AAGN,SAAO,QAAQ,SAAS;GACxB,CACH;AAGD,CAAK,UAAU;CAEf,MAAM,OAAO,UAAU,uBAAuB;CAC9C,MAAM,YAAY,UAAU,cAAc,KAAK,UAAU,eAAe;CACxE,MAAM,UAAU,KAAK,QAAQ,KAAK,KAAK,SAAS;CAChD,MAAM,gBAAgB,iBAAiB,UAAU,CAAC;CAClD,MAAM,iBAAiB,iBAAiB,UAAU,CAAC;AAGnD,KAAI,CAAC,aAAa,CAAC,WAAW,EAFV,WAAW,cAAc,GAAG,KAAK,WAAW,eAAe,GAAG,GAGhF,OAAM,IAAI,MACR,gCAAgC,UAAU,YAAY,GAAG,UAAU,aAAa,4BACpD,cAAc,WAAW,eAAe,cACrD,UAAU,UAAU,qRAMpC;;AAIL,MAAMA,MAAoB;CACxB,MAAM,gBAAgB,UAAgC,EAAE,EAAiB;EACvE,MAAM,YAAY,mBAAmB;AACrC,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,wCAAwC;AAI1D,MAAI,OAAO,WAAW,eAAe,CAAC,OAAO,cAC3C,OAAM,IAAI,MACR,8GAED;AAIH,wBAAsB,KAAK;AAE3B,MAAI;AAEF,SAAM,2BAA2B,UAAU;AAG3C,SAAM,UAAU,uBAAuB;GAGvC,MAAM,cAAc,IAAI,eAA2B,EACjD,MAAM,OAAmB;AACvB,QAAI,OAAO,cACT,QAAO,cAAc,MAAM;MAGhC,CAAC;GAGF,MAAM,aAAa,QAAQ,cAAc,OAAO;GAIhD,MAAM,EAAE,2BAA2B,MAAM,OAAO;AAChD,SAAM,uBAAuB,WAAW;IACtC,GAAG;IACH,sBAAsB;IACtB;IACA,cAAc;IACf,CAAC;YACM;AAER,yBAAsB,MAAM;;;CAIhC,MAAM,OAAO,UAAgC,EAAE,EAAuB;EACpE,MAAM,YAAY,mBAAmB;AACrC,MAAI,CAAC,UACH,OAAM,IAAI,MAAM,wCAAwC;AAI1D,wBAAsB,KAAK;AAE3B,MAAI;AAEF,SAAM,2BAA2B,UAAU;AAG3C,SAAM,UAAU,uBAAuB;GAGvC,MAAM,aAAa,QAAQ,cAAc,OAAO;GAGhD,MAAM,EAAE,2BAA2B,MAAM,OAAO;GAChD,MAAM,SAAS,MAAM,uBAAuB,WAAW;IACrD,GAAG;IACH,cAAc;IACd;IACD,CAAC;AAEF,OAAI,CAAC,OACH,OAAM,IAAI,MAAM,oCAAoC;AAGtD,UAAO;YACC;AAER,yBAAsB,MAAM;;;CAIhC,MAAM,gBAAqC;AACzC,SAAO,eAAe;;CAGxB,UAAmB;AACjB,SAAO,mBAAmB,KAAK;;CAElC;AAGD,IAAI,OAAO,WAAW,YACpB,QAAO,YAAY"}
@@ -1 +1 @@
1
- {"version":3,"file":"RequestDeduplicator.js","names":[],"sources":["../../../src/transcoding/cache/RequestDeduplicator.ts"],"sourcesContent":["/**\n * Request deduplication utility\n * Manages pending requests to prevent concurrent duplicate requests\n */\n\nexport class RequestDeduplicator {\n private pendingRequests = new Map<string, Promise<any>>();\n\n /**\n * Execute a request with deduplication\n * If a request with the same key is already pending, return the existing promise\n * Otherwise, execute the request factory and track the promise\n */\n async executeRequest<T>(\n key: string,\n requestFactory: () => Promise<T>,\n ): Promise<T> {\n // Check if there's already a pending request for this key\n const existingRequest = this.pendingRequests.get(key);\n if (existingRequest) {\n return existingRequest;\n }\n\n // Create and track the new request\n const requestPromise = requestFactory();\n this.pendingRequests.set(key, requestPromise);\n\n // Prevent unhandled rejection on the raw factory promise. Chrome's V8\n // may detect a rejection as unhandled if no synchronous .catch() is\n // attached before any microtask boundary, even when the promise is\n // currently being await-ed. The error still propagates via the try/catch.\n requestPromise.catch(() => {});\n\n try {\n const result = await requestPromise;\n this.pendingRequests.delete(key);\n return result;\n } catch (error) {\n this.pendingRequests.delete(key);\n throw error;\n }\n }\n\n /**\n * Clear all pending requests (used in cache clearing)\n */\n clear(): void {\n this.pendingRequests.clear();\n }\n\n /**\n * Get number of pending requests\n */\n getPendingCount(): number {\n return this.pendingRequests.size;\n }\n\n /**\n * Check if a request is pending\n */\n isPending(key: string): boolean {\n return this.pendingRequests.has(key);\n }\n\n /**\n * Get all pending request keys\n */\n getPendingKeys(): string[] {\n return Array.from(this.pendingRequests.keys());\n }\n}\n"],"mappings":";;;;;AAKA,IAAa,sBAAb,MAAiC;;yCACL,IAAI,KAA2B;;;;;;;CAOzD,MAAM,eACJ,KACA,gBACY;EAEZ,MAAM,kBAAkB,KAAK,gBAAgB,IAAI,IAAI;AACrD,MAAI,gBACF,QAAO;EAIT,MAAM,iBAAiB,gBAAgB;AACvC,OAAK,gBAAgB,IAAI,KAAK,eAAe;AAM7C,iBAAe,YAAY,GAAG;AAE9B,MAAI;GACF,MAAM,SAAS,MAAM;AACrB,QAAK,gBAAgB,OAAO,IAAI;AAChC,UAAO;WACA,OAAO;AACd,QAAK,gBAAgB,OAAO,IAAI;AAChC,SAAM;;;;;;CAOV,QAAc;AACZ,OAAK,gBAAgB,OAAO;;;;;CAM9B,kBAA0B;AACxB,SAAO,KAAK,gBAAgB;;;;;CAM9B,UAAU,KAAsB;AAC9B,SAAO,KAAK,gBAAgB,IAAI,IAAI;;;;;CAMtC,iBAA2B;AACzB,SAAO,MAAM,KAAK,KAAK,gBAAgB,MAAM,CAAC"}
1
+ {"version":3,"file":"RequestDeduplicator.js","names":[],"sources":["../../../src/transcoding/cache/RequestDeduplicator.ts"],"sourcesContent":["/**\n * Request deduplication utility\n * Manages pending requests to prevent concurrent duplicate requests\n */\n\nexport class RequestDeduplicator {\n private pendingRequests = new Map<string, Promise<any>>();\n\n /**\n * Execute a request with deduplication\n * If a request with the same key is already pending, return the existing promise\n * Otherwise, execute the request factory and track the promise\n */\n async executeRequest<T>(key: string, requestFactory: () => Promise<T>): Promise<T> {\n // Check if there's already a pending request for this key\n const existingRequest = this.pendingRequests.get(key);\n if (existingRequest) {\n return existingRequest;\n }\n\n // Create and track the new request\n const requestPromise = requestFactory();\n this.pendingRequests.set(key, requestPromise);\n\n // Prevent unhandled rejection on the raw factory promise. Chrome's V8\n // may detect a rejection as unhandled if no synchronous .catch() is\n // attached before any microtask boundary, even when the promise is\n // currently being await-ed. The error still propagates via the try/catch.\n requestPromise.catch(() => {});\n\n try {\n const result = await requestPromise;\n this.pendingRequests.delete(key);\n return result;\n } catch (error) {\n this.pendingRequests.delete(key);\n throw error;\n }\n }\n\n /**\n * Clear all pending requests (used in cache clearing)\n */\n clear(): void {\n this.pendingRequests.clear();\n }\n\n /**\n * Get number of pending requests\n */\n getPendingCount(): number {\n return this.pendingRequests.size;\n }\n\n /**\n * Check if a request is pending\n */\n isPending(key: string): boolean {\n return this.pendingRequests.has(key);\n }\n\n /**\n * Get all pending request keys\n */\n getPendingKeys(): string[] {\n return Array.from(this.pendingRequests.keys());\n }\n}\n"],"mappings":";;;;;AAKA,IAAa,sBAAb,MAAiC;;yCACL,IAAI,KAA2B;;;;;;;CAOzD,MAAM,eAAkB,KAAa,gBAA8C;EAEjF,MAAM,kBAAkB,KAAK,gBAAgB,IAAI,IAAI;AACrD,MAAI,gBACF,QAAO;EAIT,MAAM,iBAAiB,gBAAgB;AACvC,OAAK,gBAAgB,IAAI,KAAK,eAAe;AAM7C,iBAAe,YAAY,GAAG;AAE9B,MAAI;GACF,MAAM,SAAS,MAAM;AACrB,QAAK,gBAAgB,OAAO,IAAI;AAChC,UAAO;WACA,OAAO;AACd,QAAK,gBAAgB,OAAO,IAAI;AAChC,SAAM;;;;;;CAOV,QAAc;AACZ,OAAK,gBAAgB,OAAO;;;;;CAM9B,kBAA0B;AACxB,SAAO,KAAK,gBAAgB;;;;;CAM9B,UAAU,KAAsB;AAC9B,SAAO,KAAK,gBAAgB,IAAI,IAAI;;;;;CAMtC,iBAA2B;AACzB,SAAO,MAAM,KAAK,KAAK,gBAAgB,MAAM,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"LRUCache.js","names":[],"sources":["../../src/utils/LRUCache.ts"],"sourcesContent":["/**\n * A simple LRU (Least Recently Used) cache implementation\n */\nexport class LRUCache<K, V> {\n private cache = new Map<K, V>();\n private readonly maxSize: number;\n\n constructor(maxSize: number) {\n this.maxSize = maxSize;\n }\n\n get(key: K): V | undefined {\n const value = this.cache.get(key);\n if (value) {\n // Refresh position by removing and re-adding\n this.cache.delete(key);\n this.cache.set(key, value);\n }\n return value;\n }\n\n set(key: K, value: V): void {\n if (this.cache.has(key)) {\n this.cache.delete(key);\n } else if (this.cache.size >= this.maxSize) {\n // Remove oldest entry (first item in map)\n const firstKey = this.cache.keys().next().value;\n if (firstKey) {\n this.cache.delete(firstKey);\n }\n }\n this.cache.set(key, value);\n }\n\n has(key: K): boolean {\n return this.cache.has(key);\n }\n\n delete(key: K): boolean {\n return this.cache.delete(key);\n }\n\n clear(): void {\n this.cache.clear();\n }\n\n get size(): number {\n return this.cache.size;\n }\n}\n\n/**\n * Size-aware LRU cache that tracks memory usage in bytes\n * Evicts entries when total size exceeds the maximum\n */\nexport class SizeAwareLRUCache<K> {\n private cache = new Map<K, Promise<ArrayBuffer>>();\n private sizes = new Map<K, number>();\n private currentSize = 0;\n private readonly maxSizeBytes: number;\n\n constructor(maxSizeBytes: number) {\n this.maxSizeBytes = maxSizeBytes;\n }\n\n get(key: K): Promise<ArrayBuffer> | undefined {\n const value = this.cache.get(key);\n if (value) {\n // Refresh position by removing and re-adding\n const size = this.sizes.get(key) || 0;\n this.cache.delete(key);\n this.cache.set(key, value);\n this.sizes.delete(key);\n this.sizes.set(key, size);\n }\n return value;\n }\n\n set(key: K, value: Promise<ArrayBuffer>): void {\n // If key already exists, remove it first\n if (this.cache.has(key)) {\n const oldSize = this.sizes.get(key) || 0;\n this.currentSize -= oldSize;\n this.cache.delete(key);\n this.sizes.delete(key);\n }\n\n // Track the size when the promise resolves\n const sizeTrackingPromise = value\n .then((buffer) => {\n const bufferSize = buffer.byteLength;\n this.sizes.set(key, bufferSize);\n this.currentSize += bufferSize;\n\n // Evict oldest entries if we exceed the size limit\n this.evictIfNecessary();\n\n return buffer;\n })\n .catch((error) => {\n // If the promise fails, clean up the entry\n this.cache.delete(key);\n this.sizes.delete(key);\n throw error;\n });\n\n // Suppress unhandled rejection on the derived promise. This promise sits in\n // the cache and may reject before any caller retrieves and awaits it.\n // Zone.js checks for handlers synchronously at rejection time — without this,\n // the re-thrown error triggers an unhandledrejection event. Callers who later\n // await the cached promise still see the rejection (this just adds a no-op branch).\n sizeTrackingPromise.catch(() => {});\n\n this.cache.set(key, sizeTrackingPromise);\n }\n\n private evictIfNecessary(): void {\n while (this.currentSize > this.maxSizeBytes && this.cache.size > 0) {\n // Remove oldest entry (first item in map)\n const firstKey = this.cache.keys().next().value;\n if (firstKey) {\n const size = this.sizes.get(firstKey) || 0;\n this.currentSize -= size;\n this.cache.delete(firstKey);\n this.sizes.delete(firstKey);\n } else {\n break;\n }\n }\n }\n\n has(key: K): boolean {\n return this.cache.has(key);\n }\n\n delete(key: K): boolean {\n const size = this.sizes.get(key) || 0;\n this.currentSize -= size;\n this.sizes.delete(key);\n return this.cache.delete(key);\n }\n\n clear(): void {\n this.cache.clear();\n this.sizes.clear();\n this.currentSize = 0;\n }\n\n get size(): number {\n return this.cache.size;\n }\n\n get currentSizeBytes(): number {\n return this.currentSize;\n }\n\n get maxSize(): number {\n return this.maxSizeBytes;\n }\n}\n\n/**\n * Red-Black Tree node colors\n */\nenum Color {\n RED = \"RED\",\n BLACK = \"BLACK\",\n}\n\n/**\n * Red-Black Tree node for ordered key storage\n */\nclass RBTreeNode<K> {\n constructor(\n public key: K,\n public color: Color = Color.RED,\n public left: RBTreeNode<K> | null = null,\n public right: RBTreeNode<K> | null = null,\n public parent: RBTreeNode<K> | null = null,\n ) {}\n}\n\n/**\n * Red-Black Tree implementation for O(log n) operations\n * Supports insert, delete, search, range queries, and nearest neighbor\n */\nclass RedBlackTree<K> {\n private root: RBTreeNode<K> | null = null;\n private readonly compareFn: (a: K, b: K) => number;\n\n constructor(compareFn: (a: K, b: K) => number) {\n this.compareFn = compareFn;\n }\n\n insert(key: K): void {\n const node = new RBTreeNode(key);\n\n if (!this.root) {\n this.root = node;\n node.color = Color.BLACK;\n return;\n }\n\n this.insertNode(node);\n this.fixInsert(node);\n }\n\n delete(key: K): boolean {\n const node = this.findNode(key);\n if (!node) return false;\n\n this.deleteNode(node);\n return true;\n }\n\n find(key: K): K | null {\n const node = this.findNode(key);\n return node ? node.key : null;\n }\n\n findNearestInRange(center: K, distance: K): K[] {\n // Calculate the range bounds\n const start = this.subtractDistance(center, distance);\n const end = this.addDistance(center, distance);\n\n // Use existing range search (O(log n + k))\n return this.findRange(start, end);\n }\n\n private subtractDistance(center: K, distance: K): K {\n if (typeof center === \"number\" && typeof distance === \"number\") {\n return (center - distance) as K;\n }\n\n // For strings, we can't easily subtract distance, so just return center\n // This means string searches will be exact matches only\n return center;\n }\n\n private addDistance(center: K, distance: K): K {\n if (typeof center === \"number\" && typeof distance === \"number\") {\n return (center + distance) as K;\n }\n\n // For strings, we can't easily add distance, so just return center\n // This means string searches will be exact matches only\n return center;\n }\n\n findRange(start: K, end: K): K[] {\n const result: K[] = [];\n this.inorderRange(this.root, start, end, result);\n return result;\n }\n\n getAllSorted(): K[] {\n const result: K[] = [];\n this.inorder(this.root, result);\n return result;\n }\n\n private findNode(key: K): RBTreeNode<K> | null {\n let current = this.root;\n\n while (current) {\n const cmp = this.compareFn(key, current.key);\n if (cmp === 0) return current;\n current = cmp < 0 ? current.left : current.right;\n }\n\n return null;\n }\n\n private insertNode(node: RBTreeNode<K>): void {\n let parent = null;\n let current = this.root;\n\n while (current) {\n parent = current;\n const cmp = this.compareFn(node.key, current.key);\n current = cmp < 0 ? current.left : current.right;\n }\n\n node.parent = parent;\n if (!parent) {\n this.root = node;\n } else {\n const cmp = this.compareFn(node.key, parent.key);\n if (cmp < 0) {\n parent.left = node;\n } else {\n parent.right = node;\n }\n }\n }\n\n private fixInsert(node: RBTreeNode<K>): void {\n while (node.parent && node.parent.color === Color.RED) {\n if (node.parent === node.parent.parent?.left) {\n const uncle = node.parent.parent.right;\n\n if (uncle?.color === Color.RED) {\n node.parent.color = Color.BLACK;\n uncle.color = Color.BLACK;\n node.parent.parent.color = Color.RED;\n node = node.parent.parent;\n } else {\n if (node === node.parent.right) {\n node = node.parent;\n this.rotateLeft(node);\n }\n\n if (node.parent) {\n node.parent.color = Color.BLACK;\n if (node.parent.parent) {\n node.parent.parent.color = Color.RED;\n this.rotateRight(node.parent.parent);\n }\n }\n }\n } else {\n const uncle = node.parent.parent?.left;\n\n if (uncle?.color === Color.RED) {\n node.parent.color = Color.BLACK;\n uncle.color = Color.BLACK;\n if (node.parent.parent) {\n node.parent.parent.color = Color.RED;\n node = node.parent.parent;\n }\n } else {\n if (node === node.parent.left) {\n node = node.parent;\n this.rotateRight(node);\n }\n\n if (node.parent) {\n node.parent.color = Color.BLACK;\n if (node.parent.parent) {\n node.parent.parent.color = Color.RED;\n this.rotateLeft(node.parent.parent);\n }\n }\n }\n }\n }\n\n if (this.root) {\n this.root.color = Color.BLACK;\n }\n }\n\n private deleteNode(node: RBTreeNode<K>): void {\n let y = node;\n let yOriginalColor = y.color;\n let x: RBTreeNode<K> | null;\n\n if (!node.left) {\n x = node.right;\n this.transplant(node, node.right);\n } else if (!node.right) {\n x = node.left;\n this.transplant(node, node.left);\n } else {\n y = this.minimum(node.right);\n yOriginalColor = y.color;\n x = y.right;\n\n if (y.parent === node) {\n if (x) x.parent = y;\n } else {\n this.transplant(y, y.right);\n y.right = node.right;\n if (y.right) y.right.parent = y;\n }\n\n this.transplant(node, y);\n y.left = node.left;\n if (y.left) y.left.parent = y;\n y.color = node.color;\n }\n\n if (yOriginalColor === Color.BLACK && x) {\n this.fixDelete(x);\n }\n }\n\n private fixDelete(node: RBTreeNode<K>): void {\n while (node !== this.root && node.color === Color.BLACK) {\n if (node === node.parent?.left) {\n let sibling = node.parent.right;\n\n if (sibling?.color === Color.RED) {\n sibling.color = Color.BLACK;\n node.parent.color = Color.RED;\n this.rotateLeft(node.parent);\n sibling = node.parent.right;\n }\n\n if (\n sibling?.left?.color !== Color.RED &&\n sibling?.right?.color !== Color.RED\n ) {\n if (sibling) {\n sibling.color = Color.RED;\n }\n node = node.parent;\n } else {\n if (sibling?.right?.color !== Color.RED) {\n if (sibling.left) sibling.left.color = Color.BLACK;\n sibling.color = Color.RED;\n this.rotateRight(sibling);\n sibling = node.parent.right;\n }\n\n if (sibling) {\n sibling.color = node.parent.color;\n node.parent.color = Color.BLACK;\n if (sibling.right) sibling.right.color = Color.BLACK;\n this.rotateLeft(node.parent);\n }\n if (!this.root) {\n throw new Error(\"Root is null\");\n }\n node = this.root;\n }\n } else {\n let sibling = node.parent?.left;\n\n if (sibling?.color === Color.RED) {\n sibling.color = Color.BLACK;\n if (node.parent) node.parent.color = Color.RED;\n if (node.parent) this.rotateRight(node.parent);\n sibling = node.parent?.left;\n }\n\n if (\n sibling?.right?.color !== Color.RED &&\n sibling?.left?.color !== Color.RED\n ) {\n if (sibling) {\n sibling.color = Color.RED;\n }\n if (node.parent === null) {\n throw new Error(\"Node parent is null\");\n }\n node = node.parent;\n } else {\n if (sibling?.left?.color !== Color.RED) {\n if (sibling.right) sibling.right.color = Color.BLACK;\n sibling.color = Color.RED;\n this.rotateLeft(sibling);\n sibling = node.parent?.left;\n }\n\n if (sibling) {\n sibling.color = node.parent?.color || Color.BLACK;\n if (node.parent) node.parent.color = Color.BLACK;\n if (sibling.left) sibling.left.color = Color.BLACK;\n if (node.parent) this.rotateRight(node.parent);\n }\n if (!this.root) {\n throw new Error(\"Root is null\");\n }\n node = this.root;\n }\n }\n }\n\n node.color = Color.BLACK;\n }\n\n private rotateLeft(node: RBTreeNode<K>): void {\n const rightChild = node.right;\n if (!rightChild) {\n throw new Error(\"Right child is null\");\n }\n node.right = rightChild.left;\n\n if (rightChild.left) {\n rightChild.left.parent = node;\n }\n\n rightChild.parent = node.parent;\n\n if (!node.parent) {\n this.root = rightChild;\n } else if (node === node.parent.left) {\n node.parent.left = rightChild;\n } else {\n node.parent.right = rightChild;\n }\n\n rightChild.left = node;\n node.parent = rightChild;\n }\n\n private rotateRight(node: RBTreeNode<K>): void {\n const leftChild = node.left;\n if (!leftChild) {\n throw new Error(\"Left child is null\");\n }\n node.left = leftChild.right;\n\n if (leftChild.right) {\n leftChild.right.parent = node;\n }\n\n leftChild.parent = node.parent;\n\n if (!node.parent) {\n this.root = leftChild;\n } else if (node === node.parent.right) {\n node.parent.right = leftChild;\n } else {\n node.parent.left = leftChild;\n }\n\n leftChild.right = node;\n node.parent = leftChild;\n }\n\n private transplant(u: RBTreeNode<K>, v: RBTreeNode<K> | null): void {\n if (!u.parent) {\n this.root = v;\n } else if (u === u.parent.left) {\n u.parent.left = v;\n } else {\n u.parent.right = v;\n }\n\n if (v) {\n v.parent = u.parent;\n }\n }\n\n private minimum(node: RBTreeNode<K>): RBTreeNode<K> {\n while (node.left) {\n node = node.left;\n }\n return node;\n }\n\n private inorder(node: RBTreeNode<K> | null, result: K[]): void {\n if (node) {\n this.inorder(node.left, result);\n result.push(node.key);\n this.inorder(node.right, result);\n }\n }\n\n private inorderRange(\n node: RBTreeNode<K> | null,\n start: K,\n end: K,\n result: K[],\n ): void {\n if (!node) return;\n\n const startCmp = this.compareFn(node.key, start);\n const endCmp = this.compareFn(node.key, end);\n\n if (startCmp > 0) {\n this.inorderRange(node.left, start, end, result);\n }\n\n if (startCmp >= 0 && endCmp <= 0) {\n result.push(node.key);\n }\n\n if (endCmp < 0) {\n this.inorderRange(node.right, start, end, result);\n }\n }\n}\n\n/**\n * LRU cache with binary search capabilities using Red-Black tree\n * All operations are O(log n) for ordered queries and O(1) for LRU operations\n */\nexport class OrderedLRUCache<K extends number | string, V> {\n private cache = new Map<K, V>();\n private tree: RedBlackTree<K>;\n private readonly maxSize: number;\n private readonly compareFn: (a: K, b: K) => number;\n\n constructor(maxSize: number, compareFn?: (a: K, b: K) => number) {\n this.maxSize = maxSize;\n this.compareFn = compareFn || ((a, b) => (a < b ? -1 : a > b ? 1 : 0));\n this.tree = new RedBlackTree(this.compareFn);\n }\n\n /**\n * Get value by exact key (O(1))\n */\n get(key: K): V | undefined {\n const value = this.cache.get(key);\n if (value) {\n // Refresh position by removing and re-adding\n this.cache.delete(key);\n this.cache.set(key, value);\n }\n return value;\n }\n\n /**\n * Set key-value pair (O(log n) for tree operations, O(1) for cache)\n */\n set(key: K, value: V): void {\n const isUpdate = this.cache.has(key);\n\n if (isUpdate) {\n this.cache.delete(key);\n } else {\n if (this.cache.size >= this.maxSize) {\n // Remove oldest entry (first item in map)\n const firstKey = this.cache.keys().next().value;\n if (firstKey) {\n this.cache.delete(firstKey);\n this.tree.delete(firstKey);\n }\n }\n // Add to tree index for new keys\n this.tree.insert(key);\n }\n\n this.cache.set(key, value);\n }\n\n /**\n * Find exact key using tree search (O(log n))\n */\n findExact(key: K): V | undefined {\n const foundKey = this.tree.find(key);\n if (foundKey !== null) {\n return this.get(key);\n }\n return undefined;\n }\n\n /**\n * Find keys within distance of center point (O(log n + k) where k is result count)\n * Returns empty array if no keys found in range\n */\n findNearestInRange(center: K, distance: K): Array<{ key: K; value: V }> {\n const nearestKeys = this.tree.findNearestInRange(center, distance);\n const result: Array<{ key: K; value: V }> = [];\n\n for (const key of nearestKeys) {\n const value = this.get(key);\n if (value !== undefined) {\n result.push({ key, value });\n }\n }\n\n return result;\n }\n\n /**\n * Find all key-value pairs in range [start, end] (O(log n + k) where k is result count)\n */\n findRange(start: K, end: K): Array<{ key: K; value: V }> {\n const keys = this.tree.findRange(start, end);\n const result: Array<{ key: K; value: V }> = [];\n\n for (const key of keys) {\n const value = this.get(key);\n if (value !== undefined) {\n result.push({ key, value });\n }\n }\n\n return result;\n }\n\n /**\n * Get all keys in sorted order (O(n))\n */\n getSortedKeys(): ReadonlyArray<K> {\n return this.tree.getAllSorted();\n }\n\n has(key: K): boolean {\n return this.cache.has(key);\n }\n\n delete(key: K): boolean {\n const deleted = this.cache.delete(key);\n if (deleted) {\n this.tree.delete(key);\n }\n return deleted;\n }\n\n clear(): void {\n this.cache.clear();\n this.tree = new RedBlackTree(this.compareFn);\n }\n\n get size(): number {\n return this.cache.size;\n }\n}\n"],"mappings":";;;;AAGA,IAAa,WAAb,MAA4B;CAI1B,YAAY,SAAiB;+BAHb,IAAI,KAAW;AAI7B,OAAK,UAAU;;CAGjB,IAAI,KAAuB;EACzB,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;AACjC,MAAI,OAAO;AAET,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,MAAM,IAAI,KAAK,MAAM;;AAE5B,SAAO;;CAGT,IAAI,KAAQ,OAAgB;AAC1B,MAAI,KAAK,MAAM,IAAI,IAAI,CACrB,MAAK,MAAM,OAAO,IAAI;WACb,KAAK,MAAM,QAAQ,KAAK,SAAS;GAE1C,MAAM,WAAW,KAAK,MAAM,MAAM,CAAC,MAAM,CAAC;AAC1C,OAAI,SACF,MAAK,MAAM,OAAO,SAAS;;AAG/B,OAAK,MAAM,IAAI,KAAK,MAAM;;CAG5B,IAAI,KAAiB;AACnB,SAAO,KAAK,MAAM,IAAI,IAAI;;CAG5B,OAAO,KAAiB;AACtB,SAAO,KAAK,MAAM,OAAO,IAAI;;CAG/B,QAAc;AACZ,OAAK,MAAM,OAAO;;CAGpB,IAAI,OAAe;AACjB,SAAO,KAAK,MAAM;;;;;;;AAQtB,IAAa,oBAAb,MAAkC;CAMhC,YAAY,cAAsB;+BALlB,IAAI,KAA8B;+BAClC,IAAI,KAAgB;qBACd;AAIpB,OAAK,eAAe;;CAGtB,IAAI,KAA0C;EAC5C,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;AACjC,MAAI,OAAO;GAET,MAAM,OAAO,KAAK,MAAM,IAAI,IAAI,IAAI;AACpC,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,MAAM,IAAI,KAAK,MAAM;AAC1B,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,MAAM,IAAI,KAAK,KAAK;;AAE3B,SAAO;;CAGT,IAAI,KAAQ,OAAmC;AAE7C,MAAI,KAAK,MAAM,IAAI,IAAI,EAAE;GACvB,MAAM,UAAU,KAAK,MAAM,IAAI,IAAI,IAAI;AACvC,QAAK,eAAe;AACpB,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,MAAM,OAAO,IAAI;;EAIxB,MAAM,sBAAsB,MACzB,MAAM,WAAW;GAChB,MAAM,aAAa,OAAO;AAC1B,QAAK,MAAM,IAAI,KAAK,WAAW;AAC/B,QAAK,eAAe;AAGpB,QAAK,kBAAkB;AAEvB,UAAO;IACP,CACD,OAAO,UAAU;AAEhB,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,MAAM,OAAO,IAAI;AACtB,SAAM;IACN;AAOJ,sBAAoB,YAAY,GAAG;AAEnC,OAAK,MAAM,IAAI,KAAK,oBAAoB;;CAG1C,AAAQ,mBAAyB;AAC/B,SAAO,KAAK,cAAc,KAAK,gBAAgB,KAAK,MAAM,OAAO,GAAG;GAElE,MAAM,WAAW,KAAK,MAAM,MAAM,CAAC,MAAM,CAAC;AAC1C,OAAI,UAAU;IACZ,MAAM,OAAO,KAAK,MAAM,IAAI,SAAS,IAAI;AACzC,SAAK,eAAe;AACpB,SAAK,MAAM,OAAO,SAAS;AAC3B,SAAK,MAAM,OAAO,SAAS;SAE3B;;;CAKN,IAAI,KAAiB;AACnB,SAAO,KAAK,MAAM,IAAI,IAAI;;CAG5B,OAAO,KAAiB;EACtB,MAAM,OAAO,KAAK,MAAM,IAAI,IAAI,IAAI;AACpC,OAAK,eAAe;AACpB,OAAK,MAAM,OAAO,IAAI;AACtB,SAAO,KAAK,MAAM,OAAO,IAAI;;CAG/B,QAAc;AACZ,OAAK,MAAM,OAAO;AAClB,OAAK,MAAM,OAAO;AAClB,OAAK,cAAc;;CAGrB,IAAI,OAAe;AACjB,SAAO,KAAK,MAAM;;CAGpB,IAAI,mBAA2B;AAC7B,SAAO,KAAK;;CAGd,IAAI,UAAkB;AACpB,SAAO,KAAK"}
1
+ {"version":3,"file":"LRUCache.js","names":[],"sources":["../../src/utils/LRUCache.ts"],"sourcesContent":["/**\n * A simple LRU (Least Recently Used) cache implementation\n */\nexport class LRUCache<K, V> {\n private cache = new Map<K, V>();\n private readonly maxSize: number;\n\n constructor(maxSize: number) {\n this.maxSize = maxSize;\n }\n\n get(key: K): V | undefined {\n const value = this.cache.get(key);\n if (value) {\n // Refresh position by removing and re-adding\n this.cache.delete(key);\n this.cache.set(key, value);\n }\n return value;\n }\n\n set(key: K, value: V): void {\n if (this.cache.has(key)) {\n this.cache.delete(key);\n } else if (this.cache.size >= this.maxSize) {\n // Remove oldest entry (first item in map)\n const firstKey = this.cache.keys().next().value;\n if (firstKey) {\n this.cache.delete(firstKey);\n }\n }\n this.cache.set(key, value);\n }\n\n has(key: K): boolean {\n return this.cache.has(key);\n }\n\n delete(key: K): boolean {\n return this.cache.delete(key);\n }\n\n clear(): void {\n this.cache.clear();\n }\n\n get size(): number {\n return this.cache.size;\n }\n}\n\n/**\n * Size-aware LRU cache that tracks memory usage in bytes\n * Evicts entries when total size exceeds the maximum\n */\nexport class SizeAwareLRUCache<K> {\n private cache = new Map<K, Promise<ArrayBuffer>>();\n private sizes = new Map<K, number>();\n private currentSize = 0;\n private readonly maxSizeBytes: number;\n\n constructor(maxSizeBytes: number) {\n this.maxSizeBytes = maxSizeBytes;\n }\n\n get(key: K): Promise<ArrayBuffer> | undefined {\n const value = this.cache.get(key);\n if (value) {\n // Refresh position by removing and re-adding\n const size = this.sizes.get(key) || 0;\n this.cache.delete(key);\n this.cache.set(key, value);\n this.sizes.delete(key);\n this.sizes.set(key, size);\n }\n return value;\n }\n\n set(key: K, value: Promise<ArrayBuffer>): void {\n // If key already exists, remove it first\n if (this.cache.has(key)) {\n const oldSize = this.sizes.get(key) || 0;\n this.currentSize -= oldSize;\n this.cache.delete(key);\n this.sizes.delete(key);\n }\n\n // Track the size when the promise resolves\n const sizeTrackingPromise = value\n .then((buffer) => {\n const bufferSize = buffer.byteLength;\n this.sizes.set(key, bufferSize);\n this.currentSize += bufferSize;\n\n // Evict oldest entries if we exceed the size limit\n this.evictIfNecessary();\n\n return buffer;\n })\n .catch((error) => {\n // If the promise fails, clean up the entry\n this.cache.delete(key);\n this.sizes.delete(key);\n throw error;\n });\n\n // Suppress unhandled rejection on the derived promise. This promise sits in\n // the cache and may reject before any caller retrieves and awaits it.\n // Zone.js checks for handlers synchronously at rejection time — without this,\n // the re-thrown error triggers an unhandledrejection event. Callers who later\n // await the cached promise still see the rejection (this just adds a no-op branch).\n sizeTrackingPromise.catch(() => {});\n\n this.cache.set(key, sizeTrackingPromise);\n }\n\n private evictIfNecessary(): void {\n while (this.currentSize > this.maxSizeBytes && this.cache.size > 0) {\n // Remove oldest entry (first item in map)\n const firstKey = this.cache.keys().next().value;\n if (firstKey) {\n const size = this.sizes.get(firstKey) || 0;\n this.currentSize -= size;\n this.cache.delete(firstKey);\n this.sizes.delete(firstKey);\n } else {\n break;\n }\n }\n }\n\n has(key: K): boolean {\n return this.cache.has(key);\n }\n\n delete(key: K): boolean {\n const size = this.sizes.get(key) || 0;\n this.currentSize -= size;\n this.sizes.delete(key);\n return this.cache.delete(key);\n }\n\n clear(): void {\n this.cache.clear();\n this.sizes.clear();\n this.currentSize = 0;\n }\n\n get size(): number {\n return this.cache.size;\n }\n\n get currentSizeBytes(): number {\n return this.currentSize;\n }\n\n get maxSize(): number {\n return this.maxSizeBytes;\n }\n}\n\n/**\n * Red-Black Tree node colors\n */\nenum Color {\n RED = \"RED\",\n BLACK = \"BLACK\",\n}\n\n/**\n * Red-Black Tree node for ordered key storage\n */\nclass RBTreeNode<K> {\n constructor(\n public key: K,\n public color: Color = Color.RED,\n public left: RBTreeNode<K> | null = null,\n public right: RBTreeNode<K> | null = null,\n public parent: RBTreeNode<K> | null = null,\n ) {}\n}\n\n/**\n * Red-Black Tree implementation for O(log n) operations\n * Supports insert, delete, search, range queries, and nearest neighbor\n */\nclass RedBlackTree<K> {\n private root: RBTreeNode<K> | null = null;\n private readonly compareFn: (a: K, b: K) => number;\n\n constructor(compareFn: (a: K, b: K) => number) {\n this.compareFn = compareFn;\n }\n\n insert(key: K): void {\n const node = new RBTreeNode(key);\n\n if (!this.root) {\n this.root = node;\n node.color = Color.BLACK;\n return;\n }\n\n this.insertNode(node);\n this.fixInsert(node);\n }\n\n delete(key: K): boolean {\n const node = this.findNode(key);\n if (!node) return false;\n\n this.deleteNode(node);\n return true;\n }\n\n find(key: K): K | null {\n const node = this.findNode(key);\n return node ? node.key : null;\n }\n\n findNearestInRange(center: K, distance: K): K[] {\n // Calculate the range bounds\n const start = this.subtractDistance(center, distance);\n const end = this.addDistance(center, distance);\n\n // Use existing range search (O(log n + k))\n return this.findRange(start, end);\n }\n\n private subtractDistance(center: K, distance: K): K {\n if (typeof center === \"number\" && typeof distance === \"number\") {\n return (center - distance) as K;\n }\n\n // For strings, we can't easily subtract distance, so just return center\n // This means string searches will be exact matches only\n return center;\n }\n\n private addDistance(center: K, distance: K): K {\n if (typeof center === \"number\" && typeof distance === \"number\") {\n return (center + distance) as K;\n }\n\n // For strings, we can't easily add distance, so just return center\n // This means string searches will be exact matches only\n return center;\n }\n\n findRange(start: K, end: K): K[] {\n const result: K[] = [];\n this.inorderRange(this.root, start, end, result);\n return result;\n }\n\n getAllSorted(): K[] {\n const result: K[] = [];\n this.inorder(this.root, result);\n return result;\n }\n\n private findNode(key: K): RBTreeNode<K> | null {\n let current = this.root;\n\n while (current) {\n const cmp = this.compareFn(key, current.key);\n if (cmp === 0) return current;\n current = cmp < 0 ? current.left : current.right;\n }\n\n return null;\n }\n\n private insertNode(node: RBTreeNode<K>): void {\n let parent = null;\n let current = this.root;\n\n while (current) {\n parent = current;\n const cmp = this.compareFn(node.key, current.key);\n current = cmp < 0 ? current.left : current.right;\n }\n\n node.parent = parent;\n if (!parent) {\n this.root = node;\n } else {\n const cmp = this.compareFn(node.key, parent.key);\n if (cmp < 0) {\n parent.left = node;\n } else {\n parent.right = node;\n }\n }\n }\n\n private fixInsert(node: RBTreeNode<K>): void {\n while (node.parent && node.parent.color === Color.RED) {\n if (node.parent === node.parent.parent?.left) {\n const uncle = node.parent.parent.right;\n\n if (uncle?.color === Color.RED) {\n node.parent.color = Color.BLACK;\n uncle.color = Color.BLACK;\n node.parent.parent.color = Color.RED;\n node = node.parent.parent;\n } else {\n if (node === node.parent.right) {\n node = node.parent;\n this.rotateLeft(node);\n }\n\n if (node.parent) {\n node.parent.color = Color.BLACK;\n if (node.parent.parent) {\n node.parent.parent.color = Color.RED;\n this.rotateRight(node.parent.parent);\n }\n }\n }\n } else {\n const uncle = node.parent.parent?.left;\n\n if (uncle?.color === Color.RED) {\n node.parent.color = Color.BLACK;\n uncle.color = Color.BLACK;\n if (node.parent.parent) {\n node.parent.parent.color = Color.RED;\n node = node.parent.parent;\n }\n } else {\n if (node === node.parent.left) {\n node = node.parent;\n this.rotateRight(node);\n }\n\n if (node.parent) {\n node.parent.color = Color.BLACK;\n if (node.parent.parent) {\n node.parent.parent.color = Color.RED;\n this.rotateLeft(node.parent.parent);\n }\n }\n }\n }\n }\n\n if (this.root) {\n this.root.color = Color.BLACK;\n }\n }\n\n private deleteNode(node: RBTreeNode<K>): void {\n let y = node;\n let yOriginalColor = y.color;\n let x: RBTreeNode<K> | null;\n\n if (!node.left) {\n x = node.right;\n this.transplant(node, node.right);\n } else if (!node.right) {\n x = node.left;\n this.transplant(node, node.left);\n } else {\n y = this.minimum(node.right);\n yOriginalColor = y.color;\n x = y.right;\n\n if (y.parent === node) {\n if (x) x.parent = y;\n } else {\n this.transplant(y, y.right);\n y.right = node.right;\n if (y.right) y.right.parent = y;\n }\n\n this.transplant(node, y);\n y.left = node.left;\n if (y.left) y.left.parent = y;\n y.color = node.color;\n }\n\n if (yOriginalColor === Color.BLACK && x) {\n this.fixDelete(x);\n }\n }\n\n private fixDelete(node: RBTreeNode<K>): void {\n while (node !== this.root && node.color === Color.BLACK) {\n if (node === node.parent?.left) {\n let sibling = node.parent.right;\n\n if (sibling?.color === Color.RED) {\n sibling.color = Color.BLACK;\n node.parent.color = Color.RED;\n this.rotateLeft(node.parent);\n sibling = node.parent.right;\n }\n\n if (sibling?.left?.color !== Color.RED && sibling?.right?.color !== Color.RED) {\n if (sibling) {\n sibling.color = Color.RED;\n }\n node = node.parent;\n } else {\n if (sibling?.right?.color !== Color.RED) {\n if (sibling.left) sibling.left.color = Color.BLACK;\n sibling.color = Color.RED;\n this.rotateRight(sibling);\n sibling = node.parent.right;\n }\n\n if (sibling) {\n sibling.color = node.parent.color;\n node.parent.color = Color.BLACK;\n if (sibling.right) sibling.right.color = Color.BLACK;\n this.rotateLeft(node.parent);\n }\n if (!this.root) {\n throw new Error(\"Root is null\");\n }\n node = this.root;\n }\n } else {\n let sibling = node.parent?.left;\n\n if (sibling?.color === Color.RED) {\n sibling.color = Color.BLACK;\n if (node.parent) node.parent.color = Color.RED;\n if (node.parent) this.rotateRight(node.parent);\n sibling = node.parent?.left;\n }\n\n if (sibling?.right?.color !== Color.RED && sibling?.left?.color !== Color.RED) {\n if (sibling) {\n sibling.color = Color.RED;\n }\n if (node.parent === null) {\n throw new Error(\"Node parent is null\");\n }\n node = node.parent;\n } else {\n if (sibling?.left?.color !== Color.RED) {\n if (sibling.right) sibling.right.color = Color.BLACK;\n sibling.color = Color.RED;\n this.rotateLeft(sibling);\n sibling = node.parent?.left;\n }\n\n if (sibling) {\n sibling.color = node.parent?.color || Color.BLACK;\n if (node.parent) node.parent.color = Color.BLACK;\n if (sibling.left) sibling.left.color = Color.BLACK;\n if (node.parent) this.rotateRight(node.parent);\n }\n if (!this.root) {\n throw new Error(\"Root is null\");\n }\n node = this.root;\n }\n }\n }\n\n node.color = Color.BLACK;\n }\n\n private rotateLeft(node: RBTreeNode<K>): void {\n const rightChild = node.right;\n if (!rightChild) {\n throw new Error(\"Right child is null\");\n }\n node.right = rightChild.left;\n\n if (rightChild.left) {\n rightChild.left.parent = node;\n }\n\n rightChild.parent = node.parent;\n\n if (!node.parent) {\n this.root = rightChild;\n } else if (node === node.parent.left) {\n node.parent.left = rightChild;\n } else {\n node.parent.right = rightChild;\n }\n\n rightChild.left = node;\n node.parent = rightChild;\n }\n\n private rotateRight(node: RBTreeNode<K>): void {\n const leftChild = node.left;\n if (!leftChild) {\n throw new Error(\"Left child is null\");\n }\n node.left = leftChild.right;\n\n if (leftChild.right) {\n leftChild.right.parent = node;\n }\n\n leftChild.parent = node.parent;\n\n if (!node.parent) {\n this.root = leftChild;\n } else if (node === node.parent.right) {\n node.parent.right = leftChild;\n } else {\n node.parent.left = leftChild;\n }\n\n leftChild.right = node;\n node.parent = leftChild;\n }\n\n private transplant(u: RBTreeNode<K>, v: RBTreeNode<K> | null): void {\n if (!u.parent) {\n this.root = v;\n } else if (u === u.parent.left) {\n u.parent.left = v;\n } else {\n u.parent.right = v;\n }\n\n if (v) {\n v.parent = u.parent;\n }\n }\n\n private minimum(node: RBTreeNode<K>): RBTreeNode<K> {\n while (node.left) {\n node = node.left;\n }\n return node;\n }\n\n private inorder(node: RBTreeNode<K> | null, result: K[]): void {\n if (node) {\n this.inorder(node.left, result);\n result.push(node.key);\n this.inorder(node.right, result);\n }\n }\n\n private inorderRange(node: RBTreeNode<K> | null, start: K, end: K, result: K[]): void {\n if (!node) return;\n\n const startCmp = this.compareFn(node.key, start);\n const endCmp = this.compareFn(node.key, end);\n\n if (startCmp > 0) {\n this.inorderRange(node.left, start, end, result);\n }\n\n if (startCmp >= 0 && endCmp <= 0) {\n result.push(node.key);\n }\n\n if (endCmp < 0) {\n this.inorderRange(node.right, start, end, result);\n }\n }\n}\n\n/**\n * LRU cache with binary search capabilities using Red-Black tree\n * All operations are O(log n) for ordered queries and O(1) for LRU operations\n */\nexport class OrderedLRUCache<K extends number | string, V> {\n private cache = new Map<K, V>();\n private tree: RedBlackTree<K>;\n private readonly maxSize: number;\n private readonly compareFn: (a: K, b: K) => number;\n\n constructor(maxSize: number, compareFn?: (a: K, b: K) => number) {\n this.maxSize = maxSize;\n this.compareFn = compareFn || ((a, b) => (a < b ? -1 : a > b ? 1 : 0));\n this.tree = new RedBlackTree(this.compareFn);\n }\n\n /**\n * Get value by exact key (O(1))\n */\n get(key: K): V | undefined {\n const value = this.cache.get(key);\n if (value) {\n // Refresh position by removing and re-adding\n this.cache.delete(key);\n this.cache.set(key, value);\n }\n return value;\n }\n\n /**\n * Set key-value pair (O(log n) for tree operations, O(1) for cache)\n */\n set(key: K, value: V): void {\n const isUpdate = this.cache.has(key);\n\n if (isUpdate) {\n this.cache.delete(key);\n } else {\n if (this.cache.size >= this.maxSize) {\n // Remove oldest entry (first item in map)\n const firstKey = this.cache.keys().next().value;\n if (firstKey) {\n this.cache.delete(firstKey);\n this.tree.delete(firstKey);\n }\n }\n // Add to tree index for new keys\n this.tree.insert(key);\n }\n\n this.cache.set(key, value);\n }\n\n /**\n * Find exact key using tree search (O(log n))\n */\n findExact(key: K): V | undefined {\n const foundKey = this.tree.find(key);\n if (foundKey !== null) {\n return this.get(key);\n }\n return undefined;\n }\n\n /**\n * Find keys within distance of center point (O(log n + k) where k is result count)\n * Returns empty array if no keys found in range\n */\n findNearestInRange(center: K, distance: K): Array<{ key: K; value: V }> {\n const nearestKeys = this.tree.findNearestInRange(center, distance);\n const result: Array<{ key: K; value: V }> = [];\n\n for (const key of nearestKeys) {\n const value = this.get(key);\n if (value !== undefined) {\n result.push({ key, value });\n }\n }\n\n return result;\n }\n\n /**\n * Find all key-value pairs in range [start, end] (O(log n + k) where k is result count)\n */\n findRange(start: K, end: K): Array<{ key: K; value: V }> {\n const keys = this.tree.findRange(start, end);\n const result: Array<{ key: K; value: V }> = [];\n\n for (const key of keys) {\n const value = this.get(key);\n if (value !== undefined) {\n result.push({ key, value });\n }\n }\n\n return result;\n }\n\n /**\n * Get all keys in sorted order (O(n))\n */\n getSortedKeys(): ReadonlyArray<K> {\n return this.tree.getAllSorted();\n }\n\n has(key: K): boolean {\n return this.cache.has(key);\n }\n\n delete(key: K): boolean {\n const deleted = this.cache.delete(key);\n if (deleted) {\n this.tree.delete(key);\n }\n return deleted;\n }\n\n clear(): void {\n this.cache.clear();\n this.tree = new RedBlackTree(this.compareFn);\n }\n\n get size(): number {\n return this.cache.size;\n }\n}\n"],"mappings":";;;;AAGA,IAAa,WAAb,MAA4B;CAI1B,YAAY,SAAiB;+BAHb,IAAI,KAAW;AAI7B,OAAK,UAAU;;CAGjB,IAAI,KAAuB;EACzB,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;AACjC,MAAI,OAAO;AAET,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,MAAM,IAAI,KAAK,MAAM;;AAE5B,SAAO;;CAGT,IAAI,KAAQ,OAAgB;AAC1B,MAAI,KAAK,MAAM,IAAI,IAAI,CACrB,MAAK,MAAM,OAAO,IAAI;WACb,KAAK,MAAM,QAAQ,KAAK,SAAS;GAE1C,MAAM,WAAW,KAAK,MAAM,MAAM,CAAC,MAAM,CAAC;AAC1C,OAAI,SACF,MAAK,MAAM,OAAO,SAAS;;AAG/B,OAAK,MAAM,IAAI,KAAK,MAAM;;CAG5B,IAAI,KAAiB;AACnB,SAAO,KAAK,MAAM,IAAI,IAAI;;CAG5B,OAAO,KAAiB;AACtB,SAAO,KAAK,MAAM,OAAO,IAAI;;CAG/B,QAAc;AACZ,OAAK,MAAM,OAAO;;CAGpB,IAAI,OAAe;AACjB,SAAO,KAAK,MAAM;;;;;;;AAQtB,IAAa,oBAAb,MAAkC;CAMhC,YAAY,cAAsB;+BALlB,IAAI,KAA8B;+BAClC,IAAI,KAAgB;qBACd;AAIpB,OAAK,eAAe;;CAGtB,IAAI,KAA0C;EAC5C,MAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;AACjC,MAAI,OAAO;GAET,MAAM,OAAO,KAAK,MAAM,IAAI,IAAI,IAAI;AACpC,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,MAAM,IAAI,KAAK,MAAM;AAC1B,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,MAAM,IAAI,KAAK,KAAK;;AAE3B,SAAO;;CAGT,IAAI,KAAQ,OAAmC;AAE7C,MAAI,KAAK,MAAM,IAAI,IAAI,EAAE;GACvB,MAAM,UAAU,KAAK,MAAM,IAAI,IAAI,IAAI;AACvC,QAAK,eAAe;AACpB,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,MAAM,OAAO,IAAI;;EAIxB,MAAM,sBAAsB,MACzB,MAAM,WAAW;GAChB,MAAM,aAAa,OAAO;AAC1B,QAAK,MAAM,IAAI,KAAK,WAAW;AAC/B,QAAK,eAAe;AAGpB,QAAK,kBAAkB;AAEvB,UAAO;IACP,CACD,OAAO,UAAU;AAEhB,QAAK,MAAM,OAAO,IAAI;AACtB,QAAK,MAAM,OAAO,IAAI;AACtB,SAAM;IACN;AAOJ,sBAAoB,YAAY,GAAG;AAEnC,OAAK,MAAM,IAAI,KAAK,oBAAoB;;CAG1C,AAAQ,mBAAyB;AAC/B,SAAO,KAAK,cAAc,KAAK,gBAAgB,KAAK,MAAM,OAAO,GAAG;GAElE,MAAM,WAAW,KAAK,MAAM,MAAM,CAAC,MAAM,CAAC;AAC1C,OAAI,UAAU;IACZ,MAAM,OAAO,KAAK,MAAM,IAAI,SAAS,IAAI;AACzC,SAAK,eAAe;AACpB,SAAK,MAAM,OAAO,SAAS;AAC3B,SAAK,MAAM,OAAO,SAAS;SAE3B;;;CAKN,IAAI,KAAiB;AACnB,SAAO,KAAK,MAAM,IAAI,IAAI;;CAG5B,OAAO,KAAiB;EACtB,MAAM,OAAO,KAAK,MAAM,IAAI,IAAI,IAAI;AACpC,OAAK,eAAe;AACpB,OAAK,MAAM,OAAO,IAAI;AACtB,SAAO,KAAK,MAAM,OAAO,IAAI;;CAG/B,QAAc;AACZ,OAAK,MAAM,OAAO;AAClB,OAAK,MAAM,OAAO;AAClB,OAAK,cAAc;;CAGrB,IAAI,OAAe;AACjB,SAAO,KAAK,MAAM;;CAGpB,IAAI,mBAA2B;AAC7B,SAAO,KAAK;;CAGd,IAAI,UAAkB;AACpB,SAAO,KAAK"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.45.1",
3
+ "version": "0.45.3",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",