@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.
- package/dist/DelayedLoadingState.js.map +1 -1
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_RENDERING.js.map +1 -1
- package/dist/canvas/EFCanvas.js +3 -3
- package/dist/canvas/EFCanvas.js.map +1 -1
- package/dist/canvas/EFCanvasItem.js.map +1 -1
- package/dist/canvas/api/CanvasAPI.js.map +1 -1
- package/dist/canvas/getElementBounds.js.map +1 -1
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
- package/dist/canvas/overlays/overlayState.js.map +1 -1
- package/dist/canvas/selection/SelectionController.js +25 -23
- package/dist/canvas/selection/SelectionController.js.map +1 -1
- package/dist/canvas/selection/SelectionModel.js.map +1 -1
- package/dist/canvas/selection/selectionContext.js.map +1 -1
- package/dist/elements/ContainerInfo.js.map +1 -1
- package/dist/elements/CrossUpdateController.js.map +1 -1
- package/dist/elements/EFAudio.d.ts +2 -2
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +2 -2
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +4 -4
- package/dist/elements/EFImage.js +1 -1
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/CachedFetcher.js.map +1 -1
- package/dist/elements/EFMedia/MediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/SegmentIndex.js.map +1 -1
- package/dist/elements/EFMedia/SegmentTransport.js.map +1 -1
- package/dist/elements/EFMedia/TimingModel.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/GlobalInputCache.js.map +1 -1
- package/dist/elements/EFMedia/shared/PrecisionUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.js +9 -8
- package/dist/elements/EFPanZoom.js.map +1 -1
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +4 -4
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +4 -4
- package/dist/elements/EFTimegroup.d.ts +4 -4
- package/dist/elements/EFTimegroup.js +7 -8
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +4 -4
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +4 -4
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.js.map +1 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/SampleBuffer.js.map +1 -1
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/cloneFactoryRegistry.js.map +1 -1
- package/dist/elements/durationConverter.js.map +1 -1
- package/dist/elements/easingUtils.js.map +1 -1
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/setupTemporalHierarchy.js.map +1 -1
- package/dist/elements/updateAnimations.js +1 -1
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.js.map +1 -1
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.js.map +1 -1
- package/dist/gui/EFConfiguration.d.ts +4 -4
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFOverlayItem.js.map +1 -1
- package/dist/gui/EFOverlayLayer.js.map +1 -1
- package/dist/gui/EFPreview.js.map +1 -1
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.js.map +1 -1
- package/dist/gui/EFTogglePlay.js.map +1 -1
- package/dist/gui/EFTransformHandles.js.map +1 -1
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.js.map +1 -1
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin2.js.map +1 -1
- package/dist/gui/TargetOrContextMixin.js.map +1 -1
- package/dist/gui/currentTimeContext.js.map +1 -1
- package/dist/gui/efContext.js.map +1 -1
- package/dist/gui/fetchContext.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
- package/dist/gui/hierarchy/hierarchyContext.js.map +1 -1
- package/dist/gui/panZoomTransformContext.js.map +1 -1
- package/dist/gui/previewSettingsContext.js.map +1 -1
- package/dist/gui/theme.js.map +1 -1
- package/dist/gui/timeline/EFTimeline.js +0 -1
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
- package/dist/gui/timeline/TrimHandles.js.map +1 -1
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
- package/dist/gui/timeline/timelineStateContext.js.map +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
- package/dist/gui/transformCalculations.js.map +1 -1
- package/dist/gui/transformUtils.js.map +1 -1
- package/dist/gui/tree/EFTree.js.map +1 -1
- package/dist/gui/tree/EFTreeItem.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/otel/BridgeSpanExporter.js.map +1 -1
- package/dist/otel/setupBrowserTracing.js.map +1 -1
- package/dist/otel/tracingHelpers.js.map +1 -1
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
- package/dist/preview/RenderContext.js.map +1 -1
- package/dist/preview/RenderProfiler.js.map +1 -1
- package/dist/preview/RenderStats.js.map +1 -1
- package/dist/preview/encoding/canvasEncoder.js.map +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
- package/dist/preview/previewSettings.js.map +1 -1
- package/dist/preview/previewTypes.js.map +1 -1
- package/dist/preview/renderElementToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.js +2 -44
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.js +2 -2
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderVideoToVideo.js +2 -2
- package/dist/preview/renderVideoToVideo.js.map +1 -1
- package/dist/preview/renderers.js.map +1 -1
- package/dist/preview/rendering/ScaleConfig.js.map +1 -1
- package/dist/preview/rendering/loadImage.js.map +1 -1
- package/dist/preview/rendering/renderToImageNative.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/dist/preview/statsTrackingStrategy.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
- package/dist/utils/LRUCache.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"waveformUtils.js","names":["audioBuffer: AudioBuffer","waveformData: WaveformData"],"sources":["../../../../src/gui/timeline/tracks/waveformUtils.ts"],"sourcesContent":["/**\n * Waveform extraction utilities for DAW-style audio visualization.\n *\n * Extracts min/max peak pairs from audio data at a given resolution.\n * Designed for timeline visualization where we need to see amplitude\n * overview across the entire audio duration.\n */\n\nimport type { EFMedia } from \"../../../elements/EFMedia.js\";\n\n/** Samples per second for waveform data - balances resolution vs. data size */\nexport const WAVEFORM_SAMPLES_PER_SECOND = 100;\n\n/** Waveform peak data: alternating min/max values normalized to [-1, 1] */\nexport interface WaveformData {\n /** Peak data: [min0, max0, min1, max1, ...] normalized to [-1, 1] */\n peaks: Float32Array;\n /** Duration of the audio in milliseconds */\n durationMs: number;\n /** Samples per second (for interpreting peaks array) */\n samplesPerSecond: number;\n}\n\n/** Simple cache for waveform data keyed by audio URL */\nconst waveformCache = new Map<string, WaveformData>();\n\n/**\n * Extract waveform peak data from a media element.\n * Fetches audio through the media engine's transcoding pipeline,\n * then decodes with Web Audio API.\n * Results are cached by src URL.\n */\nexport async function extractWaveformData(\n element: EFMedia,\n signal?: AbortSignal,\n): Promise<WaveformData | null> {\n const src = element.src;\n if (!src) return null;\n\n const cached = waveformCache.get(src);\n if (cached) {\n return cached;\n }\n\n try {\n const mediaEngine = await element.getMediaEngine(signal);\n signal?.throwIfAborted();\n\n if (!mediaEngine?.tracks.audio) {\n return null;\n }\n\n const durationMs = mediaEngine.durationMs;\n if (!durationMs || durationMs <= 0) {\n return null;\n }\n\n const abortSignal = signal ?? new AbortController().signal;\n const audioSpan = await element.fetchAudioSpanningTime(
|
|
1
|
+
{"version":3,"file":"waveformUtils.js","names":["audioBuffer: AudioBuffer","waveformData: WaveformData"],"sources":["../../../../src/gui/timeline/tracks/waveformUtils.ts"],"sourcesContent":["/**\n * Waveform extraction utilities for DAW-style audio visualization.\n *\n * Extracts min/max peak pairs from audio data at a given resolution.\n * Designed for timeline visualization where we need to see amplitude\n * overview across the entire audio duration.\n */\n\nimport type { EFMedia } from \"../../../elements/EFMedia.js\";\n\n/** Samples per second for waveform data - balances resolution vs. data size */\nexport const WAVEFORM_SAMPLES_PER_SECOND = 100;\n\n/** Waveform peak data: alternating min/max values normalized to [-1, 1] */\nexport interface WaveformData {\n /** Peak data: [min0, max0, min1, max1, ...] normalized to [-1, 1] */\n peaks: Float32Array;\n /** Duration of the audio in milliseconds */\n durationMs: number;\n /** Samples per second (for interpreting peaks array) */\n samplesPerSecond: number;\n}\n\n/** Simple cache for waveform data keyed by audio URL */\nconst waveformCache = new Map<string, WaveformData>();\n\n/**\n * Extract waveform peak data from a media element.\n * Fetches audio through the media engine's transcoding pipeline,\n * then decodes with Web Audio API.\n * Results are cached by src URL.\n */\nexport async function extractWaveformData(\n element: EFMedia,\n signal?: AbortSignal,\n): Promise<WaveformData | null> {\n const src = element.src;\n if (!src) return null;\n\n const cached = waveformCache.get(src);\n if (cached) {\n return cached;\n }\n\n try {\n const mediaEngine = await element.getMediaEngine(signal);\n signal?.throwIfAborted();\n\n if (!mediaEngine?.tracks.audio) {\n return null;\n }\n\n const durationMs = mediaEngine.durationMs;\n if (!durationMs || durationMs <= 0) {\n return null;\n }\n\n const abortSignal = signal ?? new AbortController().signal;\n const audioSpan = await element.fetchAudioSpanningTime(0, durationMs, abortSignal);\n signal?.throwIfAborted();\n\n if (!audioSpan) {\n return null;\n }\n\n const arrayBuffer = await audioSpan.blob.arrayBuffer();\n signal?.throwIfAborted();\n\n // Decode audio data\n const audioContext = new OfflineAudioContext(1, 1, 44100);\n let audioBuffer: AudioBuffer;\n\n try {\n audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n } catch (decodeError) {\n console.warn(\"Failed to decode audio for waveform:\", decodeError);\n return null;\n }\n\n signal?.throwIfAborted();\n\n // Extract peaks from the decoded audio\n const peaks = extractPeaksFromBuffer(audioBuffer, WAVEFORM_SAMPLES_PER_SECOND);\n const decodedDurationMs = audioBuffer.duration * 1000;\n\n const waveformData: WaveformData = {\n peaks,\n durationMs: decodedDurationMs,\n samplesPerSecond: WAVEFORM_SAMPLES_PER_SECOND,\n };\n\n waveformCache.set(src, waveformData);\n\n return waveformData;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.warn(\"Error extracting waveform data:\", error);\n return null;\n }\n}\n\n/**\n * Extract min/max peaks from an AudioBuffer.\n * Returns Float32Array with alternating [min, max, min, max, ...] values.\n */\nfunction extractPeaksFromBuffer(buffer: AudioBuffer, samplesPerSecond: number): Float32Array {\n const channelData = buffer.getChannelData(0); // Use first channel\n const sampleRate = buffer.sampleRate;\n const duration = buffer.duration;\n\n // Calculate how many samples to output\n const outputSamples = Math.ceil(duration * samplesPerSecond);\n\n // Each output sample has min and max\n const peaks = new Float32Array(outputSamples * 2);\n\n // Samples per output window\n const samplesPerWindow = Math.floor(sampleRate / samplesPerSecond);\n\n for (let i = 0; i < outputSamples; i++) {\n const startSample = i * samplesPerWindow;\n const endSample = Math.min(startSample + samplesPerWindow, channelData.length);\n\n let min = 0;\n let max = 0;\n\n for (let j = startSample; j < endSample; j++) {\n const sample = channelData[j] ?? 0;\n if (sample < min) min = sample;\n if (sample > max) max = sample;\n }\n\n // Store as alternating min/max pairs\n peaks[i * 2] = min;\n peaks[i * 2 + 1] = max;\n }\n\n return peaks;\n}\n\n/**\n * Render waveform data to a canvas context.\n * Draws a filled waveform path centered vertically.\n */\nexport function renderWaveformToCanvas(\n ctx: CanvasRenderingContext2D,\n waveformData: WaveformData,\n x: number,\n y: number,\n width: number,\n height: number,\n color: string,\n startMs: number = 0,\n endMs?: number,\n): void {\n const { peaks, durationMs, samplesPerSecond } = waveformData;\n const actualEndMs = endMs ?? durationMs;\n\n // Calculate which samples to render\n const startSample = Math.floor((startMs / 1000) * samplesPerSecond);\n const endSample = Math.ceil((actualEndMs / 1000) * samplesPerSecond);\n const sampleCount = endSample - startSample;\n\n if (sampleCount <= 0) return;\n\n const centerY = y + height / 2;\n const halfHeight = height / 2;\n const pixelsPerSample = width / sampleCount;\n\n ctx.fillStyle = color;\n ctx.beginPath();\n\n // Draw top half (max values) left to right\n for (let i = 0; i < sampleCount; i++) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n const maxValue = peaks[peakIndex + 1] ?? 0;\n\n const px = x + i * pixelsPerSample;\n const py = centerY - maxValue * halfHeight;\n\n if (i === 0) {\n ctx.moveTo(px, py);\n } else {\n ctx.lineTo(px, py);\n }\n }\n\n // Draw bottom half (min values) right to left\n for (let i = sampleCount - 1; i >= 0; i--) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n const minValue = peaks[peakIndex] ?? 0;\n\n const px = x + i * pixelsPerSample;\n const py = centerY - minValue * halfHeight;\n\n ctx.lineTo(px, py);\n }\n\n ctx.closePath();\n ctx.fill();\n}\n\n/**\n * Clear waveform cache (useful for testing or memory management)\n */\nexport function clearWaveformCache(): void {\n waveformCache.clear();\n}\n"],"mappings":";;AAWA,MAAa,8BAA8B;;AAa3C,MAAM,gCAAgB,IAAI,KAA2B;;;;;;;AAQrD,eAAsB,oBACpB,SACA,QAC8B;CAC9B,MAAM,MAAM,QAAQ;AACpB,KAAI,CAAC,IAAK,QAAO;CAEjB,MAAM,SAAS,cAAc,IAAI,IAAI;AACrC,KAAI,OACF,QAAO;AAGT,KAAI;EACF,MAAM,cAAc,MAAM,QAAQ,eAAe,OAAO;AACxD,UAAQ,gBAAgB;AAExB,MAAI,CAAC,aAAa,OAAO,MACvB,QAAO;EAGT,MAAM,aAAa,YAAY;AAC/B,MAAI,CAAC,cAAc,cAAc,EAC/B,QAAO;EAGT,MAAM,cAAc,UAAU,IAAI,iBAAiB,CAAC;EACpD,MAAM,YAAY,MAAM,QAAQ,uBAAuB,GAAG,YAAY,YAAY;AAClF,UAAQ,gBAAgB;AAExB,MAAI,CAAC,UACH,QAAO;EAGT,MAAM,cAAc,MAAM,UAAU,KAAK,aAAa;AACtD,UAAQ,gBAAgB;EAGxB,MAAM,eAAe,IAAI,oBAAoB,GAAG,GAAG,MAAM;EACzD,IAAIA;AAEJ,MAAI;AACF,iBAAc,MAAM,aAAa,gBAAgB,YAAY;WACtD,aAAa;AACpB,WAAQ,KAAK,wCAAwC,YAAY;AACjE,UAAO;;AAGT,UAAQ,gBAAgB;EAMxB,MAAMC,eAA6B;GACjC,OAJY,uBAAuB,aAAa,4BAA4B;GAK5E,YAJwB,YAAY,WAAW;GAK/C,kBAAkB;GACnB;AAED,gBAAc,IAAI,KAAK,aAAa;AAEpC,SAAO;UACA,OAAO;AACd,MAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAER,UAAQ,KAAK,mCAAmC,MAAM;AACtD,SAAO;;;;;;;AAQX,SAAS,uBAAuB,QAAqB,kBAAwC;CAC3F,MAAM,cAAc,OAAO,eAAe,EAAE;CAC5C,MAAM,aAAa,OAAO;CAC1B,MAAM,WAAW,OAAO;CAGxB,MAAM,gBAAgB,KAAK,KAAK,WAAW,iBAAiB;CAG5D,MAAM,QAAQ,IAAI,aAAa,gBAAgB,EAAE;CAGjD,MAAM,mBAAmB,KAAK,MAAM,aAAa,iBAAiB;AAElE,MAAK,IAAI,IAAI,GAAG,IAAI,eAAe,KAAK;EACtC,MAAM,cAAc,IAAI;EACxB,MAAM,YAAY,KAAK,IAAI,cAAc,kBAAkB,YAAY,OAAO;EAE9E,IAAI,MAAM;EACV,IAAI,MAAM;AAEV,OAAK,IAAI,IAAI,aAAa,IAAI,WAAW,KAAK;GAC5C,MAAM,SAAS,YAAY,MAAM;AACjC,OAAI,SAAS,IAAK,OAAM;AACxB,OAAI,SAAS,IAAK,OAAM;;AAI1B,QAAM,IAAI,KAAK;AACf,QAAM,IAAI,IAAI,KAAK;;AAGrB,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transformCalculations.js","names":["newX: number","newY: number"],"sources":["../../src/gui/transformCalculations.ts"],"sourcesContent":["import { getCornerPoint, getOppositeCorner } from \"./transformUtils.js\";\nimport type { TransformBounds } from \"./EFTransformHandles.js\";\n\nexport type ResizeHandle = \"nw\" | \"n\" | \"ne\" | \"e\" | \"se\" | \"s\" | \"sw\" | \"w\";\n\n/**\n * Calculate the axis-aligned bounding box of a rotated rectangle.\n * Given the element's position (top-left), size, and rotation,\n * returns the min/max x/y that fully contains the rotated rectangle.\n */\nexport function getRotatedBoundingBox(\n x: number,\n y: number,\n width: number,\n height: number,\n rotationDegrees: number,\n): { minX: number; minY: number; maxX: number; maxY: number } {\n // If no rotation, simple case\n if (rotationDegrees === 0) {\n return { minX: x, minY: y, maxX: x + width, maxY: y + height };\n }\n\n const rotationRadians = (rotationDegrees * Math.PI) / 180;\n const cos = Math.cos(rotationRadians);\n const sin = Math.sin(rotationRadians);\n\n // Center of the rectangle\n const centerX = x + width / 2;\n const centerY = y + height / 2;\n\n // Half dimensions\n const halfW = width / 2;\n const halfH = height / 2;\n\n // Four corners relative to center (before rotation)\n const corners = [\n { x: -halfW, y: -halfH }, // top-left\n { x: halfW, y: -halfH }, // top-right\n { x: halfW, y: halfH }, // bottom-right\n { x: -halfW, y: halfH }, // bottom-left\n ];\n\n // Rotate each corner and find bounds\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n\n for (const corner of corners) {\n // Rotate corner around center\n const rotatedX = corner.x * cos - corner.y * sin + centerX;\n const rotatedY = corner.x * sin + corner.y * cos + centerY;\n\n minX = Math.min(minX, rotatedX);\n minY = Math.min(minY, rotatedY);\n maxX = Math.max(maxX, rotatedX);\n maxY = Math.max(maxY, rotatedY);\n }\n\n return { minX, minY, maxX, maxY };\n}\n\ntype CursorType =\n | \"n-resize\"\n | \"e-resize\"\n | \"s-resize\"\n | \"w-resize\"\n | \"ne-resize\"\n | \"nw-resize\"\n | \"se-resize\"\n | \"sw-resize\";\n\n/**\n * Get the cursor type for a resize handle based on rotation.\n * The cursor should reflect the actual direction the handle will resize in screen space.\n *\n * @param handle - The resize handle identifier\n * @param rotationDegrees - Current rotation in degrees (0-360)\n * @returns CSS cursor value\n */\nexport function getResizeHandleCursor(\n handle: ResizeHandle,\n rotationDegrees: number,\n): CursorType {\n // Map handles to their base angles (in degrees, where 0° is north, clockwise)\n const handleAngles: Record<ResizeHandle, number> = {\n n: 0,\n ne: 45,\n e: 90,\n se: 135,\n s: 180,\n sw: 225,\n w: 270,\n nw: 315,\n };\n\n // Calculate the effective angle after rotation\n const baseAngle = handleAngles[handle];\n const effectiveAngle = (baseAngle + rotationDegrees) % 360;\n const normalizedAngle =\n effectiveAngle < 0 ? effectiveAngle + 360 : effectiveAngle;\n\n // Map angle back to cursor\n // Edge handles (n, e, s, w) map to cardinal directions\n // Corner handles (ne, nw, se, sw) map to diagonal directions\n if (normalizedAngle >= 337.5 || normalizedAngle < 22.5) {\n return \"n-resize\";\n } else if (normalizedAngle >= 22.5 && normalizedAngle < 67.5) {\n return \"ne-resize\";\n } else if (normalizedAngle >= 67.5 && normalizedAngle < 112.5) {\n return \"e-resize\";\n } else if (normalizedAngle >= 112.5 && normalizedAngle < 157.5) {\n return \"se-resize\";\n } else if (normalizedAngle >= 157.5 && normalizedAngle < 202.5) {\n return \"s-resize\";\n } else if (normalizedAngle >= 202.5 && normalizedAngle < 247.5) {\n return \"sw-resize\";\n } else if (normalizedAngle >= 247.5 && normalizedAngle < 292.5) {\n return \"w-resize\";\n } else {\n // 292.5 to 337.5\n return \"nw-resize\";\n }\n}\n\n/**\n * Convert screen coordinate delta to canvas coordinate delta.\n * @param screenDeltaX - Screen pixel delta X\n * @param screenDeltaY - Screen pixel delta Y\n * @param canvasScale - Canvas zoom scale (must be > 0)\n * @returns Canvas coordinate delta\n */\nexport function screenToCanvasDelta(\n screenDeltaX: number,\n screenDeltaY: number,\n canvasScale: number,\n): { x: number; y: number } {\n if (canvasScale <= 0) {\n throw new Error(\"Canvas scale must be greater than 0\");\n }\n return {\n x: screenDeltaX / canvasScale,\n y: screenDeltaY / canvasScale,\n };\n}\n\n/**\n * Convert canvas coordinate delta to screen coordinate delta.\n * @param canvasDeltaX - Canvas coordinate delta X\n * @param canvasDeltaY - Canvas coordinate delta Y\n * @param canvasScale - Canvas zoom scale (must be > 0)\n * @returns Screen pixel delta\n */\nexport function canvasToScreenDelta(\n canvasDeltaX: number,\n canvasDeltaY: number,\n canvasScale: number,\n): { x: number; y: number } {\n if (canvasScale <= 0) {\n throw new Error(\"Canvas scale must be greater than 0\");\n }\n return {\n x: canvasDeltaX * canvasScale,\n y: canvasDeltaY * canvasScale,\n };\n}\n\n/**\n * Calculate new bounds for drag operation in canvas coordinates.\n * Pure function - no side effects.\n *\n * Works in canvas coordinate space with zoom as a parameter.\n * Converts screen deltas to canvas deltas.\n *\n * @param startPosition - Starting position in canvas coordinates\n * @param screenDeltaX - Mouse movement delta in screen pixels\n * @param screenDeltaY - Mouse movement delta in screen pixels\n * @param zoomScale - Canvas zoom scale (1.0 = no zoom, 2.0 = 2x zoom, etc.)\n * @returns New bounds with updated position (in canvas coordinates)\n */\nexport function calculateDragBounds(\n startPosition: { x: number; y: number },\n screenDeltaX: number,\n screenDeltaY: number,\n zoomScale: number = 1,\n): { x: number; y: number } {\n if (zoomScale <= 0) {\n throw new Error(\"Zoom scale must be greater than 0\");\n }\n\n // Convert screen deltas to canvas deltas\n const canvasDeltaX = screenDeltaX / zoomScale;\n const canvasDeltaY = screenDeltaY / zoomScale;\n\n return {\n x: startPosition.x + canvasDeltaX,\n y: startPosition.y + canvasDeltaY,\n };\n}\n\n/**\n * Options for resize calculation.\n * Modifier keys and constraints.\n */\nexport interface ResizeOptions {\n /** Lock aspect ratio (Shift key or multi-selection) */\n lockAspectRatio?: boolean;\n /** Resize from center instead of opposite corner (Ctrl key) */\n resizeFromCenter?: boolean;\n}\n\n/**\n * Calculate new bounds for resize operation in canvas coordinates.\n * Pure function - no side effects.\n *\n * Works in canvas coordinate space with zoom as a parameter.\n * Converts screen deltas to canvas deltas, calculates new bounds in canvas coordinates.\n *\n * @param startSize - Starting size in canvas coordinates\n * @param startPosition - Starting position in canvas coordinates\n * @param startCorner - Starting corner position in canvas coordinates\n * @param handle - Resize handle being dragged\n * @param screenDeltaX - Mouse movement delta in screen pixels\n * @param screenDeltaY - Mouse movement delta in screen pixels\n * @param rotationDegrees - Current rotation in degrees\n * @param minSize - Minimum size constraint in canvas coordinates\n * @param zoomScale - Canvas zoom scale (1.0 = no zoom, 2.0 = 2x zoom, etc.)\n * @param options - Optional resize modifiers (lockAspectRatio, resizeFromCenter)\n * @returns New bounds with updated size and position (in canvas coordinates)\n */\nexport function calculateResizeBounds(\n startSize: { width: number; height: number },\n startPosition: { x: number; y: number },\n startCorner: { x: number; y: number },\n handle: ResizeHandle,\n screenDeltaX: number,\n screenDeltaY: number,\n rotationDegrees: number,\n minSize: number,\n zoomScale: number = 1,\n options: ResizeOptions = {},\n): TransformBounds {\n if (zoomScale <= 0) {\n throw new Error(\"Zoom scale must be greater than 0\");\n }\n\n const { lockAspectRatio = false, resizeFromCenter = false } = options;\n const initialAspectRatio = startSize.width / startSize.height;\n\n // Convert screen deltas to canvas deltas\n const canvasDeltaX = screenDeltaX / zoomScale;\n const canvasDeltaY = screenDeltaY / zoomScale;\n\n const rotationRadians = (rotationDegrees * Math.PI) / 180;\n const oppositeCorner = getOppositeCorner(handle);\n\n // Rotate canvas deltas to align with element's local coordinate system\n const cos = Math.cos(-rotationRadians);\n const sin = Math.sin(-rotationRadians);\n const rotatedDeltaX = cos * canvasDeltaX - sin * canvasDeltaY;\n const rotatedDeltaY = sin * canvasDeltaX + cos * canvasDeltaY;\n\n // For center resize, delta applies to both sides (double effect)\n const deltaMultiplier = resizeFromCenter ? 2 : 1;\n\n // Calculate new size in canvas coordinates\n let newWidth = startSize.width;\n let newHeight = startSize.height;\n\n if (handle.includes(\"e\")) {\n newWidth = startSize.width + rotatedDeltaX * deltaMultiplier;\n } else if (handle.includes(\"w\")) {\n newWidth = startSize.width - rotatedDeltaX * deltaMultiplier;\n }\n\n if (handle.includes(\"s\")) {\n newHeight = startSize.height + rotatedDeltaY * deltaMultiplier;\n } else if (handle.includes(\"n\")) {\n newHeight = startSize.height - rotatedDeltaY * deltaMultiplier;\n }\n\n // Apply aspect ratio constraint if enabled\n if (lockAspectRatio) {\n const isCornerHandle = handle.length === 2; // \"ne\", \"nw\", \"se\", \"sw\"\n const isHorizontalOnly = handle === \"e\" || handle === \"w\";\n const isVerticalOnly = handle === \"n\" || handle === \"s\";\n\n if (isCornerHandle) {\n // For corners: use the dimension with larger change\n const widthScale = newWidth / startSize.width;\n const heightScale = newHeight / startSize.height;\n const uniformScale =\n Math.abs(widthScale - 1) > Math.abs(heightScale - 1)\n ? widthScale\n : heightScale;\n newWidth = startSize.width * uniformScale;\n newHeight = startSize.height * uniformScale;\n } else if (isHorizontalOnly) {\n // Horizontal handle: adjust height to match aspect ratio\n newHeight = newWidth / initialAspectRatio;\n } else if (isVerticalOnly) {\n // Vertical handle: adjust width to match aspect ratio\n newWidth = newHeight * initialAspectRatio;\n }\n }\n\n // Apply min size constraint (in canvas coordinates)\n newWidth = Math.max(minSize, newWidth);\n newHeight = Math.max(minSize, newHeight);\n\n // Re-apply aspect ratio after min size if needed\n if (lockAspectRatio && (newWidth === minSize || newHeight === minSize)) {\n if (newWidth === minSize) {\n newHeight = Math.max(minSize, minSize / initialAspectRatio);\n } else {\n newWidth = Math.max(minSize, minSize * initialAspectRatio);\n }\n }\n\n // Calculate new position based on resize mode\n let newX: number;\n let newY: number;\n\n if (resizeFromCenter) {\n // Keep center fixed\n const centerX = startPosition.x + startSize.width / 2;\n const centerY = startPosition.y + startSize.height / 2;\n newX = centerX - newWidth / 2;\n newY = centerY - newHeight / 2;\n } else {\n // Keep opposite corner fixed\n const newOppositeCorner = getCornerPoint(\n startPosition.x,\n startPosition.y,\n newWidth,\n newHeight,\n rotationRadians,\n oppositeCorner.x,\n oppositeCorner.y,\n );\n\n const offsetX = startCorner.x - newOppositeCorner.x;\n const offsetY = startCorner.y - newOppositeCorner.y;\n newX = startPosition.x + offsetX;\n newY = startPosition.y + offsetY;\n }\n\n return {\n x: newX,\n y: newY,\n width: newWidth,\n height: newHeight,\n };\n}\n\n/**\n * Calculate new rotation angle.\n * Pure function - no side effects.\n *\n * @param startAngle - Starting angle in degrees (0-360)\n * @param startRotation - Starting rotation value in degrees\n * @param currentMouseX - Current mouse X in screen pixels\n * @param currentMouseY - Current mouse Y in screen pixels\n * @param centerX - Element center X in screen pixels\n * @param centerY - Element center Y in screen pixels\n * @param rotationStep - Optional rotation step for snapping (in degrees)\n * @returns New rotation angle in degrees\n */\nexport function calculateRotation(\n startAngle: number,\n startRotation: number,\n currentMouseX: number,\n currentMouseY: number,\n centerX: number,\n centerY: number,\n rotationStep?: number,\n): number {\n const dx = currentMouseX - centerX;\n const dy = currentMouseY - centerY;\n const radians = Math.atan2(dy, dx);\n const currentAngle = radians * (180 / Math.PI) + 90;\n\n // Normalize angle difference to [-180, 180] to avoid wrapping issues\n let deltaAngle = currentAngle - startAngle;\n while (deltaAngle > 180) deltaAngle -= 360;\n while (deltaAngle < -180) deltaAngle += 360;\n\n let newRotation = startRotation + deltaAngle;\n\n if (rotationStep !== undefined && rotationStep > 0) {\n newRotation = Math.round(newRotation / rotationStep) * rotationStep;\n }\n\n return newRotation;\n}\n\n/**\n * Parse rotation angle from CSS transform.\n * Handles both rotate() syntax and matrix() transforms.\n * Pure function - no side effects.\n *\n * @param transform - CSS transform string (e.g., \"rotate(45deg)\" or \"matrix(a, b, c, d, e, f)\")\n * @returns Rotation angle in degrees\n */\nexport function parseRotationFromTransform(transform: string): number {\n if (!transform || transform === \"none\") return 0;\n\n // Try rotate() syntax first (e.g., \"rotate(45deg)\", \"rotate(0.5rad)\")\n const rotateMatch = transform.match(/rotate\\(([^)]+)\\)/);\n if (rotateMatch?.[1]) {\n const value = rotateMatch[1].trim();\n const numValue = parseFloat(value);\n const unit = value.replace(String(numValue), \"\").trim();\n if (unit === \"rad\" || unit === \"radians\") {\n return (numValue * 180) / Math.PI;\n }\n return numValue; // degrees (default)\n }\n\n // Fall back to matrix transform: matrix(a, b, c, d, tx, ty)\n // For rotation: a = cos(θ), b = sin(θ)\n const matrixMatch = transform.match(/matrix\\(([^)]+)\\)/);\n if (!matrixMatch?.[1]) return 0;\n\n const values = matrixMatch[1].split(\",\").map((v) => parseFloat(v.trim()));\n if (values.length < 2) return 0;\n\n const a = values[0];\n const b = values[1];\n if (a === undefined || b === undefined || isNaN(a) || isNaN(b)) {\n return 0;\n }\n\n return Math.atan2(b, a) * (180 / Math.PI);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"transformCalculations.js","names":["newX: number","newY: number"],"sources":["../../src/gui/transformCalculations.ts"],"sourcesContent":["import { getCornerPoint, getOppositeCorner } from \"./transformUtils.js\";\nimport type { TransformBounds } from \"./EFTransformHandles.js\";\n\nexport type ResizeHandle = \"nw\" | \"n\" | \"ne\" | \"e\" | \"se\" | \"s\" | \"sw\" | \"w\";\n\n/**\n * Calculate the axis-aligned bounding box of a rotated rectangle.\n * Given the element's position (top-left), size, and rotation,\n * returns the min/max x/y that fully contains the rotated rectangle.\n */\nexport function getRotatedBoundingBox(\n x: number,\n y: number,\n width: number,\n height: number,\n rotationDegrees: number,\n): { minX: number; minY: number; maxX: number; maxY: number } {\n // If no rotation, simple case\n if (rotationDegrees === 0) {\n return { minX: x, minY: y, maxX: x + width, maxY: y + height };\n }\n\n const rotationRadians = (rotationDegrees * Math.PI) / 180;\n const cos = Math.cos(rotationRadians);\n const sin = Math.sin(rotationRadians);\n\n // Center of the rectangle\n const centerX = x + width / 2;\n const centerY = y + height / 2;\n\n // Half dimensions\n const halfW = width / 2;\n const halfH = height / 2;\n\n // Four corners relative to center (before rotation)\n const corners = [\n { x: -halfW, y: -halfH }, // top-left\n { x: halfW, y: -halfH }, // top-right\n { x: halfW, y: halfH }, // bottom-right\n { x: -halfW, y: halfH }, // bottom-left\n ];\n\n // Rotate each corner and find bounds\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n\n for (const corner of corners) {\n // Rotate corner around center\n const rotatedX = corner.x * cos - corner.y * sin + centerX;\n const rotatedY = corner.x * sin + corner.y * cos + centerY;\n\n minX = Math.min(minX, rotatedX);\n minY = Math.min(minY, rotatedY);\n maxX = Math.max(maxX, rotatedX);\n maxY = Math.max(maxY, rotatedY);\n }\n\n return { minX, minY, maxX, maxY };\n}\n\ntype CursorType =\n | \"n-resize\"\n | \"e-resize\"\n | \"s-resize\"\n | \"w-resize\"\n | \"ne-resize\"\n | \"nw-resize\"\n | \"se-resize\"\n | \"sw-resize\";\n\n/**\n * Get the cursor type for a resize handle based on rotation.\n * The cursor should reflect the actual direction the handle will resize in screen space.\n *\n * @param handle - The resize handle identifier\n * @param rotationDegrees - Current rotation in degrees (0-360)\n * @returns CSS cursor value\n */\nexport function getResizeHandleCursor(handle: ResizeHandle, rotationDegrees: number): CursorType {\n // Map handles to their base angles (in degrees, where 0° is north, clockwise)\n const handleAngles: Record<ResizeHandle, number> = {\n n: 0,\n ne: 45,\n e: 90,\n se: 135,\n s: 180,\n sw: 225,\n w: 270,\n nw: 315,\n };\n\n // Calculate the effective angle after rotation\n const baseAngle = handleAngles[handle];\n const effectiveAngle = (baseAngle + rotationDegrees) % 360;\n const normalizedAngle = effectiveAngle < 0 ? effectiveAngle + 360 : effectiveAngle;\n\n // Map angle back to cursor\n // Edge handles (n, e, s, w) map to cardinal directions\n // Corner handles (ne, nw, se, sw) map to diagonal directions\n if (normalizedAngle >= 337.5 || normalizedAngle < 22.5) {\n return \"n-resize\";\n } else if (normalizedAngle >= 22.5 && normalizedAngle < 67.5) {\n return \"ne-resize\";\n } else if (normalizedAngle >= 67.5 && normalizedAngle < 112.5) {\n return \"e-resize\";\n } else if (normalizedAngle >= 112.5 && normalizedAngle < 157.5) {\n return \"se-resize\";\n } else if (normalizedAngle >= 157.5 && normalizedAngle < 202.5) {\n return \"s-resize\";\n } else if (normalizedAngle >= 202.5 && normalizedAngle < 247.5) {\n return \"sw-resize\";\n } else if (normalizedAngle >= 247.5 && normalizedAngle < 292.5) {\n return \"w-resize\";\n } else {\n // 292.5 to 337.5\n return \"nw-resize\";\n }\n}\n\n/**\n * Convert screen coordinate delta to canvas coordinate delta.\n * @param screenDeltaX - Screen pixel delta X\n * @param screenDeltaY - Screen pixel delta Y\n * @param canvasScale - Canvas zoom scale (must be > 0)\n * @returns Canvas coordinate delta\n */\nexport function screenToCanvasDelta(\n screenDeltaX: number,\n screenDeltaY: number,\n canvasScale: number,\n): { x: number; y: number } {\n if (canvasScale <= 0) {\n throw new Error(\"Canvas scale must be greater than 0\");\n }\n return {\n x: screenDeltaX / canvasScale,\n y: screenDeltaY / canvasScale,\n };\n}\n\n/**\n * Convert canvas coordinate delta to screen coordinate delta.\n * @param canvasDeltaX - Canvas coordinate delta X\n * @param canvasDeltaY - Canvas coordinate delta Y\n * @param canvasScale - Canvas zoom scale (must be > 0)\n * @returns Screen pixel delta\n */\nexport function canvasToScreenDelta(\n canvasDeltaX: number,\n canvasDeltaY: number,\n canvasScale: number,\n): { x: number; y: number } {\n if (canvasScale <= 0) {\n throw new Error(\"Canvas scale must be greater than 0\");\n }\n return {\n x: canvasDeltaX * canvasScale,\n y: canvasDeltaY * canvasScale,\n };\n}\n\n/**\n * Calculate new bounds for drag operation in canvas coordinates.\n * Pure function - no side effects.\n *\n * Works in canvas coordinate space with zoom as a parameter.\n * Converts screen deltas to canvas deltas.\n *\n * @param startPosition - Starting position in canvas coordinates\n * @param screenDeltaX - Mouse movement delta in screen pixels\n * @param screenDeltaY - Mouse movement delta in screen pixels\n * @param zoomScale - Canvas zoom scale (1.0 = no zoom, 2.0 = 2x zoom, etc.)\n * @returns New bounds with updated position (in canvas coordinates)\n */\nexport function calculateDragBounds(\n startPosition: { x: number; y: number },\n screenDeltaX: number,\n screenDeltaY: number,\n zoomScale: number = 1,\n): { x: number; y: number } {\n if (zoomScale <= 0) {\n throw new Error(\"Zoom scale must be greater than 0\");\n }\n\n // Convert screen deltas to canvas deltas\n const canvasDeltaX = screenDeltaX / zoomScale;\n const canvasDeltaY = screenDeltaY / zoomScale;\n\n return {\n x: startPosition.x + canvasDeltaX,\n y: startPosition.y + canvasDeltaY,\n };\n}\n\n/**\n * Options for resize calculation.\n * Modifier keys and constraints.\n */\nexport interface ResizeOptions {\n /** Lock aspect ratio (Shift key or multi-selection) */\n lockAspectRatio?: boolean;\n /** Resize from center instead of opposite corner (Ctrl key) */\n resizeFromCenter?: boolean;\n}\n\n/**\n * Calculate new bounds for resize operation in canvas coordinates.\n * Pure function - no side effects.\n *\n * Works in canvas coordinate space with zoom as a parameter.\n * Converts screen deltas to canvas deltas, calculates new bounds in canvas coordinates.\n *\n * @param startSize - Starting size in canvas coordinates\n * @param startPosition - Starting position in canvas coordinates\n * @param startCorner - Starting corner position in canvas coordinates\n * @param handle - Resize handle being dragged\n * @param screenDeltaX - Mouse movement delta in screen pixels\n * @param screenDeltaY - Mouse movement delta in screen pixels\n * @param rotationDegrees - Current rotation in degrees\n * @param minSize - Minimum size constraint in canvas coordinates\n * @param zoomScale - Canvas zoom scale (1.0 = no zoom, 2.0 = 2x zoom, etc.)\n * @param options - Optional resize modifiers (lockAspectRatio, resizeFromCenter)\n * @returns New bounds with updated size and position (in canvas coordinates)\n */\nexport function calculateResizeBounds(\n startSize: { width: number; height: number },\n startPosition: { x: number; y: number },\n startCorner: { x: number; y: number },\n handle: ResizeHandle,\n screenDeltaX: number,\n screenDeltaY: number,\n rotationDegrees: number,\n minSize: number,\n zoomScale: number = 1,\n options: ResizeOptions = {},\n): TransformBounds {\n if (zoomScale <= 0) {\n throw new Error(\"Zoom scale must be greater than 0\");\n }\n\n const { lockAspectRatio = false, resizeFromCenter = false } = options;\n const initialAspectRatio = startSize.width / startSize.height;\n\n // Convert screen deltas to canvas deltas\n const canvasDeltaX = screenDeltaX / zoomScale;\n const canvasDeltaY = screenDeltaY / zoomScale;\n\n const rotationRadians = (rotationDegrees * Math.PI) / 180;\n const oppositeCorner = getOppositeCorner(handle);\n\n // Rotate canvas deltas to align with element's local coordinate system\n const cos = Math.cos(-rotationRadians);\n const sin = Math.sin(-rotationRadians);\n const rotatedDeltaX = cos * canvasDeltaX - sin * canvasDeltaY;\n const rotatedDeltaY = sin * canvasDeltaX + cos * canvasDeltaY;\n\n // For center resize, delta applies to both sides (double effect)\n const deltaMultiplier = resizeFromCenter ? 2 : 1;\n\n // Calculate new size in canvas coordinates\n let newWidth = startSize.width;\n let newHeight = startSize.height;\n\n if (handle.includes(\"e\")) {\n newWidth = startSize.width + rotatedDeltaX * deltaMultiplier;\n } else if (handle.includes(\"w\")) {\n newWidth = startSize.width - rotatedDeltaX * deltaMultiplier;\n }\n\n if (handle.includes(\"s\")) {\n newHeight = startSize.height + rotatedDeltaY * deltaMultiplier;\n } else if (handle.includes(\"n\")) {\n newHeight = startSize.height - rotatedDeltaY * deltaMultiplier;\n }\n\n // Apply aspect ratio constraint if enabled\n if (lockAspectRatio) {\n const isCornerHandle = handle.length === 2; // \"ne\", \"nw\", \"se\", \"sw\"\n const isHorizontalOnly = handle === \"e\" || handle === \"w\";\n const isVerticalOnly = handle === \"n\" || handle === \"s\";\n\n if (isCornerHandle) {\n // For corners: use the dimension with larger change\n const widthScale = newWidth / startSize.width;\n const heightScale = newHeight / startSize.height;\n const uniformScale =\n Math.abs(widthScale - 1) > Math.abs(heightScale - 1) ? widthScale : heightScale;\n newWidth = startSize.width * uniformScale;\n newHeight = startSize.height * uniformScale;\n } else if (isHorizontalOnly) {\n // Horizontal handle: adjust height to match aspect ratio\n newHeight = newWidth / initialAspectRatio;\n } else if (isVerticalOnly) {\n // Vertical handle: adjust width to match aspect ratio\n newWidth = newHeight * initialAspectRatio;\n }\n }\n\n // Apply min size constraint (in canvas coordinates)\n newWidth = Math.max(minSize, newWidth);\n newHeight = Math.max(minSize, newHeight);\n\n // Re-apply aspect ratio after min size if needed\n if (lockAspectRatio && (newWidth === minSize || newHeight === minSize)) {\n if (newWidth === minSize) {\n newHeight = Math.max(minSize, minSize / initialAspectRatio);\n } else {\n newWidth = Math.max(minSize, minSize * initialAspectRatio);\n }\n }\n\n // Calculate new position based on resize mode\n let newX: number;\n let newY: number;\n\n if (resizeFromCenter) {\n // Keep center fixed\n const centerX = startPosition.x + startSize.width / 2;\n const centerY = startPosition.y + startSize.height / 2;\n newX = centerX - newWidth / 2;\n newY = centerY - newHeight / 2;\n } else {\n // Keep opposite corner fixed\n const newOppositeCorner = getCornerPoint(\n startPosition.x,\n startPosition.y,\n newWidth,\n newHeight,\n rotationRadians,\n oppositeCorner.x,\n oppositeCorner.y,\n );\n\n const offsetX = startCorner.x - newOppositeCorner.x;\n const offsetY = startCorner.y - newOppositeCorner.y;\n newX = startPosition.x + offsetX;\n newY = startPosition.y + offsetY;\n }\n\n return {\n x: newX,\n y: newY,\n width: newWidth,\n height: newHeight,\n };\n}\n\n/**\n * Calculate new rotation angle.\n * Pure function - no side effects.\n *\n * @param startAngle - Starting angle in degrees (0-360)\n * @param startRotation - Starting rotation value in degrees\n * @param currentMouseX - Current mouse X in screen pixels\n * @param currentMouseY - Current mouse Y in screen pixels\n * @param centerX - Element center X in screen pixels\n * @param centerY - Element center Y in screen pixels\n * @param rotationStep - Optional rotation step for snapping (in degrees)\n * @returns New rotation angle in degrees\n */\nexport function calculateRotation(\n startAngle: number,\n startRotation: number,\n currentMouseX: number,\n currentMouseY: number,\n centerX: number,\n centerY: number,\n rotationStep?: number,\n): number {\n const dx = currentMouseX - centerX;\n const dy = currentMouseY - centerY;\n const radians = Math.atan2(dy, dx);\n const currentAngle = radians * (180 / Math.PI) + 90;\n\n // Normalize angle difference to [-180, 180] to avoid wrapping issues\n let deltaAngle = currentAngle - startAngle;\n while (deltaAngle > 180) deltaAngle -= 360;\n while (deltaAngle < -180) deltaAngle += 360;\n\n let newRotation = startRotation + deltaAngle;\n\n if (rotationStep !== undefined && rotationStep > 0) {\n newRotation = Math.round(newRotation / rotationStep) * rotationStep;\n }\n\n return newRotation;\n}\n\n/**\n * Parse rotation angle from CSS transform.\n * Handles both rotate() syntax and matrix() transforms.\n * Pure function - no side effects.\n *\n * @param transform - CSS transform string (e.g., \"rotate(45deg)\" or \"matrix(a, b, c, d, e, f)\")\n * @returns Rotation angle in degrees\n */\nexport function parseRotationFromTransform(transform: string): number {\n if (!transform || transform === \"none\") return 0;\n\n // Try rotate() syntax first (e.g., \"rotate(45deg)\", \"rotate(0.5rad)\")\n const rotateMatch = transform.match(/rotate\\(([^)]+)\\)/);\n if (rotateMatch?.[1]) {\n const value = rotateMatch[1].trim();\n const numValue = parseFloat(value);\n const unit = value.replace(String(numValue), \"\").trim();\n if (unit === \"rad\" || unit === \"radians\") {\n return (numValue * 180) / Math.PI;\n }\n return numValue; // degrees (default)\n }\n\n // Fall back to matrix transform: matrix(a, b, c, d, tx, ty)\n // For rotation: a = cos(θ), b = sin(θ)\n const matrixMatch = transform.match(/matrix\\(([^)]+)\\)/);\n if (!matrixMatch?.[1]) return 0;\n\n const values = matrixMatch[1].split(\",\").map((v) => parseFloat(v.trim()));\n if (values.length < 2) return 0;\n\n const a = values[0];\n const b = values[1];\n if (a === undefined || b === undefined || isNaN(a) || isNaN(b)) {\n return 0;\n }\n\n return Math.atan2(b, a) * (180 / Math.PI);\n}\n"],"mappings":";;;;;;;;AAUA,SAAgB,sBACd,GACA,GACA,OACA,QACA,iBAC4D;AAE5D,KAAI,oBAAoB,EACtB,QAAO;EAAE,MAAM;EAAG,MAAM;EAAG,MAAM,IAAI;EAAO,MAAM,IAAI;EAAQ;CAGhE,MAAM,kBAAmB,kBAAkB,KAAK,KAAM;CACtD,MAAM,MAAM,KAAK,IAAI,gBAAgB;CACrC,MAAM,MAAM,KAAK,IAAI,gBAAgB;CAGrC,MAAM,UAAU,IAAI,QAAQ;CAC5B,MAAM,UAAU,IAAI,SAAS;CAG7B,MAAM,QAAQ,QAAQ;CACtB,MAAM,QAAQ,SAAS;CAGvB,MAAM,UAAU;EACd;GAAE,GAAG,CAAC;GAAO,GAAG,CAAC;GAAO;EACxB;GAAE,GAAG;GAAO,GAAG,CAAC;GAAO;EACvB;GAAE,GAAG;GAAO,GAAG;GAAO;EACtB;GAAE,GAAG,CAAC;GAAO,GAAG;GAAO;EACxB;CAGD,IAAI,OAAO;CACX,IAAI,OAAO;CACX,IAAI,OAAO;CACX,IAAI,OAAO;AAEX,MAAK,MAAM,UAAU,SAAS;EAE5B,MAAM,WAAW,OAAO,IAAI,MAAM,OAAO,IAAI,MAAM;EACnD,MAAM,WAAW,OAAO,IAAI,MAAM,OAAO,IAAI,MAAM;AAEnD,SAAO,KAAK,IAAI,MAAM,SAAS;AAC/B,SAAO,KAAK,IAAI,MAAM,SAAS;AAC/B,SAAO,KAAK,IAAI,MAAM,SAAS;AAC/B,SAAO,KAAK,IAAI,MAAM,SAAS;;AAGjC,QAAO;EAAE;EAAM;EAAM;EAAM;EAAM;;;;;;;;;;AAqBnC,SAAgB,sBAAsB,QAAsB,iBAAqC;CAe/F,MAAM,kBAb6C;EACjD,GAAG;EACH,IAAI;EACJ,GAAG;EACH,IAAI;EACJ,GAAG;EACH,IAAI;EACJ,GAAG;EACH,IAAI;EACL,CAG8B,UACK,mBAAmB;CACvD,MAAM,kBAAkB,iBAAiB,IAAI,iBAAiB,MAAM;AAKpE,KAAI,mBAAmB,SAAS,kBAAkB,KAChD,QAAO;UACE,mBAAmB,QAAQ,kBAAkB,KACtD,QAAO;UACE,mBAAmB,QAAQ,kBAAkB,MACtD,QAAO;UACE,mBAAmB,SAAS,kBAAkB,MACvD,QAAO;UACE,mBAAmB,SAAS,kBAAkB,MACvD,QAAO;UACE,mBAAmB,SAAS,kBAAkB,MACvD,QAAO;UACE,mBAAmB,SAAS,kBAAkB,MACvD,QAAO;KAGP,QAAO;;;;;;;;;;;;;;;AA2DX,SAAgB,oBACd,eACA,cACA,cACA,YAAoB,GACM;AAC1B,KAAI,aAAa,EACf,OAAM,IAAI,MAAM,oCAAoC;CAItD,MAAM,eAAe,eAAe;CACpC,MAAM,eAAe,eAAe;AAEpC,QAAO;EACL,GAAG,cAAc,IAAI;EACrB,GAAG,cAAc,IAAI;EACtB;;;;;;;;;;;;;;;;;;;;;AAiCH,SAAgB,sBACd,WACA,eACA,aACA,QACA,cACA,cACA,iBACA,SACA,YAAoB,GACpB,UAAyB,EAAE,EACV;AACjB,KAAI,aAAa,EACf,OAAM,IAAI,MAAM,oCAAoC;CAGtD,MAAM,EAAE,kBAAkB,OAAO,mBAAmB,UAAU;CAC9D,MAAM,qBAAqB,UAAU,QAAQ,UAAU;CAGvD,MAAM,eAAe,eAAe;CACpC,MAAM,eAAe,eAAe;CAEpC,MAAM,kBAAmB,kBAAkB,KAAK,KAAM;CACtD,MAAM,iBAAiB,kBAAkB,OAAO;CAGhD,MAAM,MAAM,KAAK,IAAI,CAAC,gBAAgB;CACtC,MAAM,MAAM,KAAK,IAAI,CAAC,gBAAgB;CACtC,MAAM,gBAAgB,MAAM,eAAe,MAAM;CACjD,MAAM,gBAAgB,MAAM,eAAe,MAAM;CAGjD,MAAM,kBAAkB,mBAAmB,IAAI;CAG/C,IAAI,WAAW,UAAU;CACzB,IAAI,YAAY,UAAU;AAE1B,KAAI,OAAO,SAAS,IAAI,CACtB,YAAW,UAAU,QAAQ,gBAAgB;UACpC,OAAO,SAAS,IAAI,CAC7B,YAAW,UAAU,QAAQ,gBAAgB;AAG/C,KAAI,OAAO,SAAS,IAAI,CACtB,aAAY,UAAU,SAAS,gBAAgB;UACtC,OAAO,SAAS,IAAI,CAC7B,aAAY,UAAU,SAAS,gBAAgB;AAIjD,KAAI,iBAAiB;EACnB,MAAM,iBAAiB,OAAO,WAAW;EACzC,MAAM,mBAAmB,WAAW,OAAO,WAAW;EACtD,MAAM,iBAAiB,WAAW,OAAO,WAAW;AAEpD,MAAI,gBAAgB;GAElB,MAAM,aAAa,WAAW,UAAU;GACxC,MAAM,cAAc,YAAY,UAAU;GAC1C,MAAM,eACJ,KAAK,IAAI,aAAa,EAAE,GAAG,KAAK,IAAI,cAAc,EAAE,GAAG,aAAa;AACtE,cAAW,UAAU,QAAQ;AAC7B,eAAY,UAAU,SAAS;aACtB,iBAET,aAAY,WAAW;WACd,eAET,YAAW,YAAY;;AAK3B,YAAW,KAAK,IAAI,SAAS,SAAS;AACtC,aAAY,KAAK,IAAI,SAAS,UAAU;AAGxC,KAAI,oBAAoB,aAAa,WAAW,cAAc,SAC5D,KAAI,aAAa,QACf,aAAY,KAAK,IAAI,SAAS,UAAU,mBAAmB;KAE3D,YAAW,KAAK,IAAI,SAAS,UAAU,mBAAmB;CAK9D,IAAIA;CACJ,IAAIC;AAEJ,KAAI,kBAAkB;EAEpB,MAAM,UAAU,cAAc,IAAI,UAAU,QAAQ;EACpD,MAAM,UAAU,cAAc,IAAI,UAAU,SAAS;AACrD,SAAO,UAAU,WAAW;AAC5B,SAAO,UAAU,YAAY;QACxB;EAEL,MAAM,oBAAoB,eACxB,cAAc,GACd,cAAc,GACd,UACA,WACA,iBACA,eAAe,GACf,eAAe,EAChB;EAED,MAAM,UAAU,YAAY,IAAI,kBAAkB;EAClD,MAAM,UAAU,YAAY,IAAI,kBAAkB;AAClD,SAAO,cAAc,IAAI;AACzB,SAAO,cAAc,IAAI;;AAG3B,QAAO;EACL,GAAG;EACH,GAAG;EACH,OAAO;EACP,QAAQ;EACT;;;;;;;;;;AAoDH,SAAgB,2BAA2B,WAA2B;AACpE,KAAI,CAAC,aAAa,cAAc,OAAQ,QAAO;CAG/C,MAAM,cAAc,UAAU,MAAM,oBAAoB;AACxD,KAAI,cAAc,IAAI;EACpB,MAAM,QAAQ,YAAY,GAAG,MAAM;EACnC,MAAM,WAAW,WAAW,MAAM;EAClC,MAAM,OAAO,MAAM,QAAQ,OAAO,SAAS,EAAE,GAAG,CAAC,MAAM;AACvD,MAAI,SAAS,SAAS,SAAS,UAC7B,QAAQ,WAAW,MAAO,KAAK;AAEjC,SAAO;;CAKT,MAAM,cAAc,UAAU,MAAM,oBAAoB;AACxD,KAAI,CAAC,cAAc,GAAI,QAAO;CAE9B,MAAM,SAAS,YAAY,GAAG,MAAM,IAAI,CAAC,KAAK,MAAM,WAAW,EAAE,MAAM,CAAC,CAAC;AACzE,KAAI,OAAO,SAAS,EAAG,QAAO;CAE9B,MAAM,IAAI,OAAO;CACjB,MAAM,IAAI,OAAO;AACjB,KAAI,MAAM,UAAa,MAAM,UAAa,MAAM,EAAE,IAAI,MAAM,EAAE,CAC5D,QAAO;AAGT,QAAO,KAAK,MAAM,GAAG,EAAE,IAAI,MAAM,KAAK"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transformUtils.js","names":[],"sources":["../../src/gui/transformUtils.ts"],"sourcesContent":["/**\n * Pure utility functions for transform calculations.\n * Extracted from motion designer TransformHandles component.\n */\n\n/**\n * Rotate a point around a center point by given radians.\n */\nexport function rotatePoint(\n cx: number,\n cy: number,\n x: number,\n y: number,\n radians: number,\n): { x: number; y: number } {\n const cos = Math.cos(radians);\n const sin = Math.sin(radians);\n const nx = cos * (x - cx) - sin * (y - cy) + cx;\n const ny = sin * (x - cx) + cos * (y - cy) + cy;\n return { x: nx, y: ny };\n}\n\n/**\n * Calculate corner point in canvas coordinates for a rotated element.\n * @param x - Element x position\n * @param y - Element y position\n * @param width - Element width\n * @param height - Element height\n * @param rotationRadians - Rotation in radians\n * @param xMagnitude - 0 = left, 0.5 = center, 1 = right\n * @param yMagnitude - 0 = top, 0.5 = center, 1 = bottom\n */\nexport function getCornerPoint(\n x: number,\n y: number,\n width: number,\n height: number,\n rotationRadians: number,\n xMagnitude: number,\n yMagnitude: number,\n): { x: number; y: number } {\n const centerX = x + width / 2;\n const centerY = y + height / 2;\n const localCornerX = x + xMagnitude * width;\n const localCornerY = y + yMagnitude * height;\n return rotatePoint(
|
|
1
|
+
{"version":3,"file":"transformUtils.js","names":[],"sources":["../../src/gui/transformUtils.ts"],"sourcesContent":["/**\n * Pure utility functions for transform calculations.\n * Extracted from motion designer TransformHandles component.\n */\n\n/**\n * Rotate a point around a center point by given radians.\n */\nexport function rotatePoint(\n cx: number,\n cy: number,\n x: number,\n y: number,\n radians: number,\n): { x: number; y: number } {\n const cos = Math.cos(radians);\n const sin = Math.sin(radians);\n const nx = cos * (x - cx) - sin * (y - cy) + cx;\n const ny = sin * (x - cx) + cos * (y - cy) + cy;\n return { x: nx, y: ny };\n}\n\n/**\n * Calculate corner point in canvas coordinates for a rotated element.\n * @param x - Element x position\n * @param y - Element y position\n * @param width - Element width\n * @param height - Element height\n * @param rotationRadians - Rotation in radians\n * @param xMagnitude - 0 = left, 0.5 = center, 1 = right\n * @param yMagnitude - 0 = top, 0.5 = center, 1 = bottom\n */\nexport function getCornerPoint(\n x: number,\n y: number,\n width: number,\n height: number,\n rotationRadians: number,\n xMagnitude: number,\n yMagnitude: number,\n): { x: number; y: number } {\n const centerX = x + width / 2;\n const centerY = y + height / 2;\n const localCornerX = x + xMagnitude * width;\n const localCornerY = y + yMagnitude * height;\n return rotatePoint(centerX, centerY, localCornerX, localCornerY, rotationRadians);\n}\n\n/**\n * Get opposite corner magnitudes for a handle.\n * Used to determine which corner stays fixed during resize.\n */\nexport function getOppositeCorner(handle: string): { x: number; y: number } {\n switch (handle) {\n case \"nw\":\n return { x: 1, y: 1 }; // se corner\n case \"n\":\n return { x: 0.5, y: 1 }; // s corner\n case \"ne\":\n return { x: 0, y: 1 }; // sw corner\n case \"e\":\n return { x: 0, y: 0.5 }; // w corner\n case \"se\":\n return { x: 0, y: 0 }; // nw corner\n case \"s\":\n return { x: 0.5, y: 0 }; // n corner\n case \"sw\":\n return { x: 1, y: 0 }; // ne corner\n case \"w\":\n return { x: 1, y: 0.5 }; // e corner\n default:\n return { x: 0.5, y: 0.5 };\n }\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,YACd,IACA,IACA,GACA,GACA,SAC0B;CAC1B,MAAM,MAAM,KAAK,IAAI,QAAQ;CAC7B,MAAM,MAAM,KAAK,IAAI,QAAQ;AAG7B,QAAO;EAAE,GAFE,OAAO,IAAI,MAAM,OAAO,IAAI,MAAM;EAE7B,GADL,OAAO,IAAI,MAAM,OAAO,IAAI,MAAM;EACtB;;;;;;;;;;;;AAazB,SAAgB,eACd,GACA,GACA,OACA,QACA,iBACA,YACA,YAC0B;AAK1B,QAAO,YAJS,IAAI,QAAQ,GACZ,IAAI,SAAS,GACR,IAAI,aAAa,OACjB,IAAI,aAAa,QAC2B,gBAAgB;;;;;;AAOnF,SAAgB,kBAAkB,QAA0C;AAC1E,SAAQ,QAAR;EACE,KAAK,KACH,QAAO;GAAE,GAAG;GAAG,GAAG;GAAG;EACvB,KAAK,IACH,QAAO;GAAE,GAAG;GAAK,GAAG;GAAG;EACzB,KAAK,KACH,QAAO;GAAE,GAAG;GAAG,GAAG;GAAG;EACvB,KAAK,IACH,QAAO;GAAE,GAAG;GAAG,GAAG;GAAK;EACzB,KAAK,KACH,QAAO;GAAE,GAAG;GAAG,GAAG;GAAG;EACvB,KAAK,IACH,QAAO;GAAE,GAAG;GAAK,GAAG;GAAG;EACzB,KAAK,KACH,QAAO;GAAE,GAAG;GAAG,GAAG;GAAG;EACvB,KAAK,IACH,QAAO;GAAE,GAAG;GAAG,GAAG;GAAK;EACzB,QACE,QAAO;GAAE,GAAG;GAAK,GAAG;GAAK"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFTree.js","names":["EFTree"],"sources":["../../../src/gui/tree/EFTree.ts"],"sourcesContent":["import { provide } from \"@lit/context\";\nimport { css, html, LitElement, nothing, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\n\nimport type {
|
|
1
|
+
{"version":3,"file":"EFTree.js","names":["EFTree"],"sources":["../../../src/gui/tree/EFTree.ts"],"sourcesContent":["import { provide } from \"@lit/context\";\nimport { css, html, LitElement, nothing, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\n\nimport type { TreeItem, TreeContext, TreeState, TreeActions } from \"./treeContext.js\";\nimport { treeContext, collectAllIds } from \"./treeContext.js\";\nimport \"./EFTreeItem.js\";\n\n/**\n * Generic tree component for displaying hierarchical data.\n *\n * Takes an array of TreeItem objects and renders them as an expandable tree.\n * Provides context for selection and expand/collapse state.\n *\n * @fires tree-select - When an item is selected. Detail: { id: string, item: TreeItem }\n *\n * @example\n * ```html\n * <ef-tree\n * .items=${[\n * { id: \"folder1\", label: \"Folder 1\", children: [\n * { id: \"file1\", label: \"File 1\" },\n * { id: \"file2\", label: \"File 2\" },\n * ]},\n * { id: \"file3\", label: \"File 3\" },\n * ]}\n * @tree-select=${(e) => console.log('Selected:', e.detail.id)}\n * ></ef-tree>\n * ```\n */\n@customElement(\"ef-tree\")\nexport class EFTree extends LitElement {\n static styles = css`\n :host {\n display: block;\n overflow: auto;\n font-size: 12px;\n\n --tree-bg: var(--ef-color-bg);\n --tree-text: var(--ef-color-text);\n --tree-hover-bg: var(--ef-color-hover);\n --tree-selected-bg: var(--ef-color-selected);\n --tree-border: var(--ef-color-border);\n }\n\n .tree-container {\n background: var(--tree-bg);\n color: var(--tree-text);\n min-height: 100%;\n padding: 4px 0;\n }\n\n .header {\n padding: 8px 12px;\n font-weight: 600;\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--ef-color-text-muted);\n border-bottom: 1px solid var(--tree-border);\n margin-bottom: 4px;\n }\n\n .empty {\n padding: 16px;\n text-align: center;\n color: var(--ef-color-text-subtle);\n font-style: italic;\n }\n `;\n\n /** Tree items to display */\n @property({ type: Array, attribute: false })\n items: TreeItem[] = [];\n\n /** Optional header text */\n @property({ type: String })\n header = \"\";\n\n /** Whether to show the header */\n @property({ type: Boolean, attribute: \"show-header\" })\n showHeader = false;\n\n /** Currently selected item ID (can be set externally) */\n @property({ type: String, attribute: \"selected-id\" })\n selectedId: string | null = null;\n\n /** Whether to expand all items by default */\n @property({ type: Boolean, attribute: \"expand-all\" })\n expandAll = true;\n\n @state()\n private treeState: TreeState = {\n selectedId: null,\n expandedIds: new Set(),\n };\n\n private treeActions: TreeActions = {\n select: (id: string | null) => {\n this.treeState = {\n ...this.treeState,\n selectedId: id,\n };\n\n // Find the item for the event detail\n const item = id ? this.findItem(id, this.items) : null;\n\n this.dispatchEvent(\n new CustomEvent(\"tree-select\", {\n detail: { id, item },\n bubbles: true,\n composed: true,\n }),\n );\n },\n\n toggleExpanded: (id: string) => {\n const newExpanded = new Set(this.treeState.expandedIds);\n if (newExpanded.has(id)) {\n newExpanded.delete(id);\n } else {\n newExpanded.add(id);\n }\n this.treeState = {\n ...this.treeState,\n expandedIds: newExpanded,\n };\n },\n\n setExpanded: (id: string, expanded: boolean) => {\n const newExpanded = new Set(this.treeState.expandedIds);\n if (expanded) {\n newExpanded.add(id);\n } else {\n newExpanded.delete(id);\n }\n this.treeState = {\n ...this.treeState,\n expandedIds: newExpanded,\n };\n },\n };\n\n @provide({ context: treeContext })\n @state()\n // @ts-ignore\n private providedContext: TreeContext = {\n state: this.treeState,\n actions: this.treeActions,\n };\n\n private findItem(id: string, items: TreeItem[]): TreeItem | null {\n for (const item of items) {\n if (item.id === id) return item;\n if (item.children) {\n const found = this.findItem(id, item.children);\n if (found) return found;\n }\n }\n return null;\n }\n\n private initializeExpandedState(): void {\n if (this.expandAll && this.items.length > 0) {\n this.treeState = {\n ...this.treeState,\n expandedIds: collectAllIds(this.items),\n };\n }\n }\n\n protected willUpdate(changedProperties: PropertyValues): void {\n // Sync external selectedId with internal state\n if (changedProperties.has(\"selectedId\") && this.selectedId !== this.treeState.selectedId) {\n this.treeState = {\n ...this.treeState,\n selectedId: this.selectedId,\n };\n }\n\n // Always update provided context\n this.providedContext = {\n state: this.treeState,\n actions: this.treeActions,\n };\n\n super.willUpdate(changedProperties);\n }\n\n protected updated(changedProperties: PropertyValues): void {\n super.updated(changedProperties);\n\n // Re-initialize expanded state when items change\n if (changedProperties.has(\"items\")) {\n this.initializeExpandedState();\n }\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.initializeExpandedState();\n }\n\n render() {\n return html`\n <div class=\"tree-container\">\n ${this.showHeader ? html`<div class=\"header\">${this.header}</div>` : nothing}\n ${\n this.items.length > 0\n ? this.items.map((item) => html`<ef-tree-item .item=${item}></ef-tree-item>`)\n : html`<div class=\"empty\">No items</div>`\n }\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-tree\": EFTree;\n }\n}\n"],"mappings":";;;;;;;;AA+BO,mBAAMA,iBAAe,WAAW;;;eA0CjB,EAAE;gBAIb;oBAII;oBAIe;mBAIhB;mBAGmB;GAC7B,YAAY;GACZ,6BAAa,IAAI,KAAK;GACvB;qBAEkC;GACjC,SAAS,OAAsB;AAC7B,SAAK,YAAY;KACf,GAAG,KAAK;KACR,YAAY;KACb;IAGD,MAAM,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,MAAM,GAAG;AAElD,SAAK,cACH,IAAI,YAAY,eAAe;KAC7B,QAAQ;MAAE;MAAI;MAAM;KACpB,SAAS;KACT,UAAU;KACX,CAAC,CACH;;GAGH,iBAAiB,OAAe;IAC9B,MAAM,cAAc,IAAI,IAAI,KAAK,UAAU,YAAY;AACvD,QAAI,YAAY,IAAI,GAAG,CACrB,aAAY,OAAO,GAAG;QAEtB,aAAY,IAAI,GAAG;AAErB,SAAK,YAAY;KACf,GAAG,KAAK;KACR,aAAa;KACd;;GAGH,cAAc,IAAY,aAAsB;IAC9C,MAAM,cAAc,IAAI,IAAI,KAAK,UAAU,YAAY;AACvD,QAAI,SACF,aAAY,IAAI,GAAG;QAEnB,aAAY,OAAO,GAAG;AAExB,SAAK,YAAY;KACf,GAAG,KAAK;KACR,aAAa;KACd;;GAEJ;yBAKsC;GACrC,OAAO,KAAK;GACZ,SAAS,KAAK;GACf;;;gBArHe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuHnB,AAAQ,SAAS,IAAY,OAAoC;AAC/D,OAAK,MAAM,QAAQ,OAAO;AACxB,OAAI,KAAK,OAAO,GAAI,QAAO;AAC3B,OAAI,KAAK,UAAU;IACjB,MAAM,QAAQ,KAAK,SAAS,IAAI,KAAK,SAAS;AAC9C,QAAI,MAAO,QAAO;;;AAGtB,SAAO;;CAGT,AAAQ,0BAAgC;AACtC,MAAI,KAAK,aAAa,KAAK,MAAM,SAAS,EACxC,MAAK,YAAY;GACf,GAAG,KAAK;GACR,aAAa,cAAc,KAAK,MAAM;GACvC;;CAIL,AAAU,WAAW,mBAAyC;AAE5D,MAAI,kBAAkB,IAAI,aAAa,IAAI,KAAK,eAAe,KAAK,UAAU,WAC5E,MAAK,YAAY;GACf,GAAG,KAAK;GACR,YAAY,KAAK;GAClB;AAIH,OAAK,kBAAkB;GACrB,OAAO,KAAK;GACZ,SAAS,KAAK;GACf;AAED,QAAM,WAAW,kBAAkB;;CAGrC,AAAU,QAAQ,mBAAyC;AACzD,QAAM,QAAQ,kBAAkB;AAGhC,MAAI,kBAAkB,IAAI,QAAQ,CAChC,MAAK,yBAAyB;;CAIlC,oBAA0B;AACxB,QAAM,mBAAmB;AACzB,OAAK,yBAAyB;;CAGhC,SAAS;AACP,SAAO,IAAI;;UAEL,KAAK,aAAa,IAAI,uBAAuB,KAAK,OAAO,UAAU,QAAQ;UAE3E,KAAK,MAAM,SAAS,IAChB,KAAK,MAAM,KAAK,SAAS,IAAI,uBAAuB,KAAK,kBAAkB,GAC3E,IAAI,oCACT;;;;;YA3IN,SAAS;CAAE,MAAM;CAAO,WAAW;CAAO,CAAC;YAI3C,SAAS,EAAE,MAAM,QAAQ,CAAC;YAI1B,SAAS;CAAE,MAAM;CAAS,WAAW;CAAe,CAAC;YAIrD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAe,CAAC;YAIpD,SAAS;CAAE,MAAM;CAAS,WAAW;CAAc,CAAC;YAGpD,OAAO;YAoDP,QAAQ,EAAE,SAAS,aAAa,CAAC,EACjC,OAAO;qBAlHT,cAAc,UAAU"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFTreeItem.js","names":["EFTreeItem"],"sources":["../../../src/gui/tree/EFTreeItem.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement, nothing } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\n\nimport type { TreeItem, TreeContext } from \"./treeContext.js\";\nimport { treeContext } from \"./treeContext.js\";\n\n/**\n * Generic tree item component.\n *\n * Renders a single item in a tree with:\n * - Expand/collapse toggle for items with children\n * - Optional icon\n * - Label\n * - Recursive children rendering\n *\n * @fires tree-item-click - When item is clicked (for selection)\n */\n@customElement(\"ef-tree-item\")\nexport class EFTreeItem extends LitElement {\n static styles = css`\n :host {\n display: block;\n }\n\n .item-row {\n display: flex;\n align-items: center;\n height: var(--tree-item-height, 1.5rem);\n padding-left: var(--tree-item-padding-left, 0.5rem);\n padding-right: var(--tree-item-padding-right, 0.5rem);\n font-size: var(--tree-item-font-size, 0.75rem);\n cursor: pointer;\n user-select: none;\n color: var(--tree-text);\n }\n\n .item-row:hover {\n background: var(--tree-hover-bg);\n }\n\n .item-row[data-selected] {\n background: var(--tree-selected-bg);\n }\n\n .expand-icon {\n width: var(--tree-expand-icon-size, 1rem);\n height: var(--tree-expand-icon-size, 1rem);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n flex-shrink: 0;\n }\n\n .expand-icon svg {\n width: 0.75rem;\n height: 0.75rem;\n transition: transform 0.15s ease;\n }\n\n .expand-icon[data-expanded] svg {\n transform: rotate(90deg);\n }\n\n .icon {\n margin-right: var(--tree-icon-gap, 0.25rem);\n flex-shrink: 0;\n display: flex;\n align-items: center;\n }\n\n .label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n }\n\n .children {\n padding-left: var(--tree-indent, 1rem);\n }\n\n .children[data-collapsed] {\n display: none;\n }\n `;\n\n @consume({ context: treeContext, subscribe: true })\n treeContext?: TreeContext;\n\n @property({ type: Object, attribute: false })\n item!: TreeItem;\n\n @state()\n private localExpanded = true;\n\n get isSelected(): boolean {\n if (!this.treeContext || !this.item) return false;\n return this.treeContext.state.selectedId === this.item.id;\n }\n\n get isExpanded(): boolean {\n if (!this.treeContext || !this.item) return this.localExpanded;\n return this.treeContext.state.expandedIds.has(this.item.id);\n }\n\n get hasChildren(): boolean {\n return Boolean(this.item?.children && this.item.children.length > 0);\n }\n\n private handleClick(e: Event): void {\n e.stopPropagation();\n if (this.treeContext && this.item) {\n this.treeContext.actions.select(this.item.id);\n }\n }\n\n private handleExpandClick(e: Event): void {\n e.stopPropagation();\n if (this.treeContext && this.item) {\n this.treeContext.actions.toggleExpanded(this.item.id);\n } else {\n this.localExpanded = !this.localExpanded;\n }\n }\n\n render() {\n if (!this.item) return nothing;\n\n const expanded = this.isExpanded;\n\n return html`\n <div\n class=\"item-row\"\n ?data-selected=${this.isSelected}\n @click=${this.handleClick}\n >\n ${\n this.hasChildren\n ? html`\n <span\n class=\"expand-icon\"\n ?data-expanded=${expanded}\n @click=${this.handleExpandClick}\n >\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M8 5v14l11-7z\" />\n </svg>\n </span>\n `\n : html`<span class=\"expand-icon\"></span>`\n }\n ${
|
|
1
|
+
{"version":3,"file":"EFTreeItem.js","names":["EFTreeItem"],"sources":["../../../src/gui/tree/EFTreeItem.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement, nothing } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\n\nimport type { TreeItem, TreeContext } from \"./treeContext.js\";\nimport { treeContext } from \"./treeContext.js\";\n\n/**\n * Generic tree item component.\n *\n * Renders a single item in a tree with:\n * - Expand/collapse toggle for items with children\n * - Optional icon\n * - Label\n * - Recursive children rendering\n *\n * @fires tree-item-click - When item is clicked (for selection)\n */\n@customElement(\"ef-tree-item\")\nexport class EFTreeItem extends LitElement {\n static styles = css`\n :host {\n display: block;\n }\n\n .item-row {\n display: flex;\n align-items: center;\n height: var(--tree-item-height, 1.5rem);\n padding-left: var(--tree-item-padding-left, 0.5rem);\n padding-right: var(--tree-item-padding-right, 0.5rem);\n font-size: var(--tree-item-font-size, 0.75rem);\n cursor: pointer;\n user-select: none;\n color: var(--tree-text);\n }\n\n .item-row:hover {\n background: var(--tree-hover-bg);\n }\n\n .item-row[data-selected] {\n background: var(--tree-selected-bg);\n }\n\n .expand-icon {\n width: var(--tree-expand-icon-size, 1rem);\n height: var(--tree-expand-icon-size, 1rem);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n flex-shrink: 0;\n }\n\n .expand-icon svg {\n width: 0.75rem;\n height: 0.75rem;\n transition: transform 0.15s ease;\n }\n\n .expand-icon[data-expanded] svg {\n transform: rotate(90deg);\n }\n\n .icon {\n margin-right: var(--tree-icon-gap, 0.25rem);\n flex-shrink: 0;\n display: flex;\n align-items: center;\n }\n\n .label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n }\n\n .children {\n padding-left: var(--tree-indent, 1rem);\n }\n\n .children[data-collapsed] {\n display: none;\n }\n `;\n\n @consume({ context: treeContext, subscribe: true })\n treeContext?: TreeContext;\n\n @property({ type: Object, attribute: false })\n item!: TreeItem;\n\n @state()\n private localExpanded = true;\n\n get isSelected(): boolean {\n if (!this.treeContext || !this.item) return false;\n return this.treeContext.state.selectedId === this.item.id;\n }\n\n get isExpanded(): boolean {\n if (!this.treeContext || !this.item) return this.localExpanded;\n return this.treeContext.state.expandedIds.has(this.item.id);\n }\n\n get hasChildren(): boolean {\n return Boolean(this.item?.children && this.item.children.length > 0);\n }\n\n private handleClick(e: Event): void {\n e.stopPropagation();\n if (this.treeContext && this.item) {\n this.treeContext.actions.select(this.item.id);\n }\n }\n\n private handleExpandClick(e: Event): void {\n e.stopPropagation();\n if (this.treeContext && this.item) {\n this.treeContext.actions.toggleExpanded(this.item.id);\n } else {\n this.localExpanded = !this.localExpanded;\n }\n }\n\n render() {\n if (!this.item) return nothing;\n\n const expanded = this.isExpanded;\n\n return html`\n <div\n class=\"item-row\"\n ?data-selected=${this.isSelected}\n @click=${this.handleClick}\n >\n ${\n this.hasChildren\n ? html`\n <span\n class=\"expand-icon\"\n ?data-expanded=${expanded}\n @click=${this.handleExpandClick}\n >\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M8 5v14l11-7z\" />\n </svg>\n </span>\n `\n : html`<span class=\"expand-icon\"></span>`\n }\n ${this.item.icon ? html`<span class=\"icon\">${this.item.icon}</span>` : nothing}\n <span class=\"label\">${this.item.label}</span>\n </div>\n ${\n this.hasChildren\n ? html`\n <div class=\"children\" ?data-collapsed=${!expanded}>\n ${this.item.children!.map(\n (child) => html`<ef-tree-item .item=${child}></ef-tree-item>`,\n )}\n </div>\n `\n : nothing\n }\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-tree-item\": EFTreeItem;\n }\n}\n"],"mappings":";;;;;;;AAmBO,uBAAMA,qBAAmB,WAAW;;;uBA4EjB;;;gBA3ER,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6EnB,IAAI,aAAsB;AACxB,MAAI,CAAC,KAAK,eAAe,CAAC,KAAK,KAAM,QAAO;AAC5C,SAAO,KAAK,YAAY,MAAM,eAAe,KAAK,KAAK;;CAGzD,IAAI,aAAsB;AACxB,MAAI,CAAC,KAAK,eAAe,CAAC,KAAK,KAAM,QAAO,KAAK;AACjD,SAAO,KAAK,YAAY,MAAM,YAAY,IAAI,KAAK,KAAK,GAAG;;CAG7D,IAAI,cAAuB;AACzB,SAAO,QAAQ,KAAK,MAAM,YAAY,KAAK,KAAK,SAAS,SAAS,EAAE;;CAGtE,AAAQ,YAAY,GAAgB;AAClC,IAAE,iBAAiB;AACnB,MAAI,KAAK,eAAe,KAAK,KAC3B,MAAK,YAAY,QAAQ,OAAO,KAAK,KAAK,GAAG;;CAIjD,AAAQ,kBAAkB,GAAgB;AACxC,IAAE,iBAAiB;AACnB,MAAI,KAAK,eAAe,KAAK,KAC3B,MAAK,YAAY,QAAQ,eAAe,KAAK,KAAK,GAAG;MAErD,MAAK,gBAAgB,CAAC,KAAK;;CAI/B,SAAS;AACP,MAAI,CAAC,KAAK,KAAM,QAAO;EAEvB,MAAM,WAAW,KAAK;AAEtB,SAAO,IAAI;;;yBAGU,KAAK,WAAW;iBACxB,KAAK,YAAY;;UAGxB,KAAK,cACD,IAAI;;;iCAGe,SAAS;yBACjB,KAAK,kBAAkB;;;;;;gBAOlC,IAAI,oCACT;UACC,KAAK,KAAK,OAAO,IAAI,sBAAsB,KAAK,KAAK,KAAK,WAAW,QAAQ;8BACzD,KAAK,KAAK,MAAM;;QAGtC,KAAK,cACD,IAAI;oDACoC,CAAC,SAAS;gBAC9C,KAAK,KAAK,SAAU,KACnB,UAAU,IAAI,uBAAuB,MAAM,kBAC7C,CAAC;;cAGJ,QACL;;;;YA9EJ,QAAQ;CAAE,SAAS;CAAa,WAAW;CAAM,CAAC;YAGlD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,OAAO;yBA5ET,cAAc,eAAe"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import \"./elements/EFTimegroup.js\";\n\nexport { EFTimegroup } from \"./elements/EFTimegroup.js\";\nexport {\n registerCloneFactory,\n unregisterCloneFactory,\n getCloneFactory,\n type CloneFactory,\n type CloneFactoryResult,\n} from \"./elements/cloneFactoryRegistry.js\";\nexport type { ContainerInfo } from \"./elements/ContainerInfo.js\";\nexport { getContainerInfoFromElement } from \"./elements/ContainerInfo.js\";\nexport type { ElementPositionInfo } from \"./elements/ElementPositionInfo.js\";\nexport {
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import \"./elements/EFTimegroup.js\";\n\nexport { EFTimegroup } from \"./elements/EFTimegroup.js\";\nexport {\n registerCloneFactory,\n unregisterCloneFactory,\n getCloneFactory,\n type CloneFactory,\n type CloneFactoryResult,\n} from \"./elements/cloneFactoryRegistry.js\";\nexport type { ContainerInfo } from \"./elements/ContainerInfo.js\";\nexport { getContainerInfoFromElement } from \"./elements/ContainerInfo.js\";\nexport type { ElementPositionInfo } from \"./elements/ElementPositionInfo.js\";\nexport { getPositionInfoFromElement, PositionInfoMixin } from \"./elements/ElementPositionInfo.js\";\nexport { needsFitScale, elementNeedsFitScale } from \"./gui/FitScaleHelpers.js\";\n\nimport \"./elements/EFImage.js\";\n\nexport { EFImage } from \"./elements/EFImage.js\";\n\nimport \"./elements/EFMedia.js\";\n\nexport type { EFMedia } from \"./elements/EFMedia.js\";\n\nimport \"./elements/EFAudio.js\";\n\nexport { EFAudio } from \"./elements/EFAudio.js\";\n\nimport \"./elements/EFVideo.js\";\n\nexport { EFVideo } from \"./elements/EFVideo.js\";\n\nimport \"./elements/EFCaptions.js\";\n\nexport {\n EFCaptions,\n EFCaptionsActiveWord,\n EFCaptionsAfterActiveWord,\n EFCaptionsBeforeActiveWord,\n EFCaptionsSegment,\n} from \"./elements/EFCaptions.js\";\n\nimport \"./elements/EFText.js\";\nimport \"./elements/EFTextSegment.js\";\n\nexport { EFText } from \"./elements/EFText.js\";\nexport { EFTextSegment } from \"./elements/EFTextSegment.js\";\n\nimport \"./elements/EFWaveform.js\";\n\nexport { EFWaveform } from \"./elements/EFWaveform.js\";\n\nimport \"./elements/EFTemporal.js\";\n\nexport { isEFTemporal } from \"./elements/EFTemporal.js\";\nexport type { TemporalMixinInterface } from \"./elements/EFTemporal.js\";\n\nimport \"./gui/EFConfiguration.ts\";\n\nexport { EFConfiguration } from \"./gui/EFConfiguration.ts\";\n\nimport \"./gui/EFWorkbench.js\";\n\nexport { EFWorkbench } from \"./gui/EFWorkbench.js\";\n\nimport \"./gui/EFPreview.js\";\n\nexport { EFPreview } from \"./gui/EFPreview.js\";\n\nimport \"./gui/EFFilmstrip.js\";\n\nexport { EFFilmstrip } from \"./gui/EFFilmstrip.js\";\n\nimport \"./gui/hierarchy/EFHierarchy.js\";\nimport \"./gui/hierarchy/EFHierarchyItem.js\";\n\nexport { EFHierarchy } from \"./gui/hierarchy/EFHierarchy.js\";\nexport {\n EFHierarchyItem,\n EFTimegroupHierarchyItem,\n EFAudioHierarchyItem,\n EFVideoHierarchyItem,\n EFCaptionsHierarchyItem,\n EFCaptionsActiveWordHierarchyItem,\n EFTextHierarchyItem,\n EFTextSegmentHierarchyItem,\n EFWaveformHierarchyItem,\n EFImageHierarchyItem,\n EFHTMLHierarchyItem,\n} from \"./gui/hierarchy/EFHierarchyItem.js\";\nexport type {\n HierarchyState,\n HierarchyActions,\n HierarchyContext,\n} from \"./gui/hierarchy/hierarchyContext.js\";\nexport { hierarchyContext } from \"./gui/hierarchy/hierarchyContext.js\";\n\n// Generic tree component\nimport \"./gui/tree/EFTree.js\";\nimport \"./gui/tree/EFTreeItem.js\";\n\nexport { EFTree } from \"./gui/tree/EFTree.js\";\nexport { EFTreeItem } from \"./gui/tree/EFTreeItem.js\";\nexport type { TreeItem, TreeState, TreeActions, TreeContext } from \"./gui/tree/treeContext.js\";\nexport { treeContext, collectAllIds } from \"./gui/tree/treeContext.js\";\n\nimport \"./gui/EFTogglePlay.js\";\n\nexport { EFTogglePlay } from \"./gui/EFTogglePlay.js\";\n\nimport \"./gui/EFPlay.js\";\n\nexport { EFPlay } from \"./gui/EFPlay.js\";\n\nimport \"./gui/EFPause.js\";\n\nexport { EFPause } from \"./gui/EFPause.js\";\n\nimport \"./gui/EFToggleLoop.js\";\n\nexport { EFToggleLoop } from \"./gui/EFToggleLoop.js\";\n\nimport \"./gui/EFScrubber.js\";\n\nexport { EFScrubber } from \"./gui/EFScrubber.js\";\n\nimport \"./gui/EFTimeDisplay.js\";\n\nexport { EFTimeDisplay } from \"./gui/EFTimeDisplay.js\";\n\nimport \"./gui/EFActiveRootTemporal.js\";\n\nexport { EFActiveRootTemporal } from \"./gui/EFActiveRootTemporal.js\";\n\nimport \"./gui/EFDial.js\";\n\nexport { type DialChangeDetail, EFDial } from \"./gui/EFDial.js\";\n\nimport \"./gui/EFControls.js\";\n\nexport { EFControls } from \"./gui/EFControls.js\";\n\nimport \"./gui/EFFocusOverlay.js\";\n\nexport { EFFocusOverlay } from \"./gui/EFFocusOverlay.js\";\n\nimport \"./gui/transformUtils.js\";\n\nexport { getCornerPoint, getOppositeCorner, rotatePoint } from \"./gui/transformUtils.js\";\n\nimport \"./gui/EFTransformHandles.ts\";\n\nexport { type TransformBounds, EFTransformHandles } from \"./gui/EFTransformHandles.ts\";\n\nimport \"./gui/EFResizableBox.ts\";\n\nexport { type BoxBounds, EFResizableBox } from \"./gui/EFResizableBox.ts\";\n\nimport \"./gui/EFFitScale.js\";\n\nexport {\n EFFitScale,\n computeFitScale,\n type ScaleInput,\n type ScaleOutput,\n} from \"./gui/EFFitScale.js\";\n\nimport \"./elements/EFSurface.ts\";\n\nexport { EFSurface } from \"./elements/EFSurface.ts\";\n\nimport \"./elements/EFPanZoom.js\";\n\nexport { EFPanZoom } from \"./elements/EFPanZoom.js\";\nexport type { PanZoomTransform } from \"./elements/EFPanZoom.js\";\n\nimport \"./canvas/EFCanvas.js\";\nimport \"./canvas/EFCanvasItem.js\";\n\nexport { EFCanvas } from \"./canvas/EFCanvas.js\";\nexport { EFCanvasItem } from \"./canvas/EFCanvasItem.js\";\nexport { CanvasAPI } from \"./canvas/api/CanvasAPI.js\";\nexport type { CanvasElementData, SelectionState, CanvasElementBounds } from \"./canvas/api/types.js\";\nexport { SelectionModel } from \"./canvas/selection/SelectionModel.js\";\n\nimport \"./gui/EFOverlayLayer.ts\";\n\nexport { EFOverlayLayer } from \"./gui/EFOverlayLayer.ts\";\n\nimport \"./gui/EFOverlayItem.ts\";\n\nexport { EFOverlayItem } from \"./gui/EFOverlayItem.ts\";\nexport type { OverlayItemPosition } from \"./gui/EFOverlayItem.ts\";\n\nimport \"./gui/EFTimelineRuler.ts\";\n\nexport {\n EFTimelineRuler,\n quantizeToFrameTimeMs,\n calculateFrameIntervalMs,\n calculatePixelsPerFrame,\n shouldShowFrameMarkers,\n} from \"./gui/EFTimelineRuler.ts\";\n\nimport \"./gui/timeline/EFTimeline.js\";\nimport \"./gui/timeline/TrimHandles.js\";\nimport \"./gui/timeline/tracks/EFThumbnailStrip.js\";\n\nexport { EFTimeline } from \"./gui/timeline/EFTimeline.js\";\nexport {\n EFTrimHandles,\n type TrimValue,\n type TrimChangeDetail,\n} from \"./gui/timeline/TrimHandles.js\";\nexport { EFThumbnailStrip } from \"./gui/timeline/tracks/EFThumbnailStrip.js\";\n\nif (typeof window !== \"undefined\") {\n // @ts-expect-error\n window.EF_REGISTERED = true;\n}\n\nimport \"./EF_FRAMEGEN.js\";\n\n// Initialize render API\nimport \"./render/EFRenderAPI.js\";\n\nexport { getRenderInfo, RenderInfoSchema, type RenderInfo } from \"./getRenderInfo.js\";\nexport { getRenderData } from \"./render/getRenderData.js\";\n// Export types only - actual render functions are loaded dynamically by EFTimegroup\nexport type {\n RenderToVideoOptions,\n RenderProgress,\n} from \"./preview/renderTimegroupToVideo.types.js\";\nexport type {\n ContentReadyMode,\n CaptureOptions,\n CanvasPreviewOptions,\n CanvasPreviewResult,\n} from \"./preview/renderTimegroupToCanvas.types.js\";\nexport type { TraceContext } from \"./otel/tracingHelpers.js\";\n\n// Element-to-canvas rendering\nexport {\n renderElementToCanvas,\n type RenderElementOptions,\n} from \"./preview/renderElementToCanvas.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwNA,IAAI,OAAO,WAAW,YAEpB,QAAO,gBAAgB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BridgeSpanExporter.js","names":[],"sources":["../../src/otel/BridgeSpanExporter.ts"],"sourcesContent":["import { type ExportResult, ExportResultCode } from \"@opentelemetry/core\";\nimport type { ReadableSpan, SpanExporter } from \"@opentelemetry/sdk-trace-base\";\n\nfunction toHex(value: unknown): string {\n if (typeof value === \"string\") return value;\n if (Array.isArray(value)) {\n return value\n .map((b) => {\n const byte = typeof b === \"number\" ? b : 0;\n return byte.toString(16).padStart(2, \"0\");\n })\n .join(\"\");\n }\n if (ArrayBuffer.isView(value)) {\n return Array.from(value as Uint8Array)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n }\n return String(value);\n}\n\ninterface OtlpAttributeValue {\n stringValue?: string;\n intValue?: number;\n doubleValue?: number;\n boolValue?: boolean;\n arrayValue?: { values: OtlpAttributeValue[] };\n}\n\nfunction convertAttribute(value: unknown): OtlpAttributeValue {\n if (typeof value === \"string\") return { stringValue: value };\n if (typeof value === \"number\")\n return Number.isInteger(value)
|
|
1
|
+
{"version":3,"file":"BridgeSpanExporter.js","names":[],"sources":["../../src/otel/BridgeSpanExporter.ts"],"sourcesContent":["import { type ExportResult, ExportResultCode } from \"@opentelemetry/core\";\nimport type { ReadableSpan, SpanExporter } from \"@opentelemetry/sdk-trace-base\";\n\nfunction toHex(value: unknown): string {\n if (typeof value === \"string\") return value;\n if (Array.isArray(value)) {\n return value\n .map((b) => {\n const byte = typeof b === \"number\" ? b : 0;\n return byte.toString(16).padStart(2, \"0\");\n })\n .join(\"\");\n }\n if (ArrayBuffer.isView(value)) {\n return Array.from(value as Uint8Array)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n }\n return String(value);\n}\n\ninterface OtlpAttributeValue {\n stringValue?: string;\n intValue?: number;\n doubleValue?: number;\n boolValue?: boolean;\n arrayValue?: { values: OtlpAttributeValue[] };\n}\n\nfunction convertAttribute(value: unknown): OtlpAttributeValue {\n if (typeof value === \"string\") return { stringValue: value };\n if (typeof value === \"number\")\n return Number.isInteger(value) ? { intValue: value } : { doubleValue: value };\n if (typeof value === \"boolean\") return { boolValue: value };\n if (Array.isArray(value)) return { arrayValue: { values: value.map(convertAttribute) } };\n return { stringValue: String(value) };\n}\n\ninterface BridgeWithSpanExport {\n exportSpans?: (endpoint: string, payload: string) => void;\n}\n\nexport class BridgeSpanExporter implements SpanExporter {\n private bridge: BridgeWithSpanExport;\n private endpoint: string;\n\n constructor(bridge: BridgeWithSpanExport, endpoint: string) {\n this.bridge = bridge;\n this.endpoint = endpoint;\n }\n\n export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {\n if (!this.bridge?.exportSpans) {\n resultCallback({ code: ExportResultCode.FAILED });\n return;\n }\n\n try {\n const otlpPayload = {\n resourceSpans: [\n {\n resource: {\n attributes: Object.entries(spans[0]?.resource?.attributes || {}).map(\n ([key, value]) => ({\n key,\n value: convertAttribute(value),\n }),\n ),\n },\n scopeSpans: [\n {\n scope: {\n name: \"telecine-browser\",\n version: \"1.0.0\",\n },\n spans: spans.map((span) => {\n const ctx = span.spanContext();\n return {\n traceId: toHex(ctx.traceId),\n spanId: toHex(ctx.spanId),\n parentSpanId: span.parentSpanId ? toHex(span.parentSpanId) : undefined,\n name: span.name,\n kind: span.kind,\n startTimeUnixNano: String(\n span.startTime[0] * 1_000_000_000 + span.startTime[1],\n ),\n endTimeUnixNano: String(span.endTime[0] * 1_000_000_000 + span.endTime[1]),\n attributes: Object.entries(span.attributes).map(([key, value]) => ({\n key,\n value: convertAttribute(value),\n })),\n status: span.status,\n events: span.events.map((event) => ({\n timeUnixNano: String(event.time[0] * 1_000_000_000 + event.time[1]),\n name: event.name,\n attributes: Object.entries(event.attributes || {}).map(([key, value]) => ({\n key,\n value: convertAttribute(value),\n })),\n })),\n links: span.links.map((link) => ({\n traceId: toHex(link.context.traceId),\n spanId: toHex(link.context.spanId),\n attributes: Object.entries(link.attributes || {}).map(([key, value]) => ({\n key,\n value: convertAttribute(value),\n })),\n })),\n };\n }),\n },\n ],\n },\n ],\n };\n\n const serializedPayload = JSON.stringify(otlpPayload);\n\n this.bridge.exportSpans(this.endpoint, serializedPayload);\n resultCallback({ code: ExportResultCode.SUCCESS });\n } catch (error) {\n resultCallback({\n code: ExportResultCode.FAILED,\n error: error instanceof Error ? error : new Error(String(error)),\n });\n }\n }\n\n shutdown(): Promise<void> {\n return Promise.resolve();\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupBrowserTracing.js","names":["provider: WebTracerProvider | null","spanProcessor: BatchSpanProcessor | SimpleSpanProcessor"],"sources":["../../src/otel/setupBrowserTracing.ts"],"sourcesContent":["import { Resource } from \"@opentelemetry/resources\";\nimport {
|
|
1
|
+
{"version":3,"file":"setupBrowserTracing.js","names":["provider: WebTracerProvider | null","spanProcessor: BatchSpanProcessor | SimpleSpanProcessor"],"sources":["../../src/otel/setupBrowserTracing.ts"],"sourcesContent":["import { Resource } from \"@opentelemetry/resources\";\nimport { BatchSpanProcessor, SimpleSpanProcessor } from \"@opentelemetry/sdk-trace-base\";\nimport { WebTracerProvider } from \"@opentelemetry/sdk-trace-web\";\nimport { ATTR_SERVICE_NAME } from \"@opentelemetry/semantic-conventions\";\nimport { BridgeSpanExporter } from \"./BridgeSpanExporter.js\";\n\nlet isInitialized = false;\nlet provider: WebTracerProvider | null = null;\n\ninterface BridgeWithSpanExport {\n exportSpans?: (endpoint: string, payload: string) => void;\n}\n\nexport interface BrowserTracingConfig {\n otelEndpoint: string;\n serviceName?: string;\n bridge?: BridgeWithSpanExport;\n useBatching?: boolean;\n}\n\nexport async function setupBrowserTracing(config: BrowserTracingConfig): Promise<void> {\n if (isInitialized) {\n return;\n }\n\n try {\n if (!config.bridge) {\n throw new Error(\"Bridge is required for browser tracing\");\n }\n\n const exporter = new BridgeSpanExporter(config.bridge, config.otelEndpoint);\n\n let spanProcessor: BatchSpanProcessor | SimpleSpanProcessor;\n if (config.useBatching) {\n spanProcessor = new BatchSpanProcessor(exporter, {\n maxQueueSize: 100,\n maxExportBatchSize: 10,\n scheduledDelayMillis: 500,\n });\n } else {\n spanProcessor = new SimpleSpanProcessor(exporter);\n }\n\n provider = new WebTracerProvider({\n resource: new Resource({\n [ATTR_SERVICE_NAME]: config.serviceName || \"telecine-browser\",\n }),\n spanProcessors: [spanProcessor],\n });\n\n // Dynamically import ZoneContextManager only when tracing is enabled\n // This prevents zone.js from being loaded for users who don't need tracing\n const { ZoneContextManager } = await import(\"@opentelemetry/context-zone\");\n\n provider.register({\n contextManager: new ZoneContextManager(),\n });\n\n isInitialized = true;\n } catch (error) {\n console.error(\"Failed to initialize browser tracing:\", error);\n throw error;\n }\n}\n\nexport function isBrowserTracingInitialized(): boolean {\n return isInitialized;\n}\n"],"mappings":";;;;;;;AAMA,IAAI,gBAAgB;AACpB,IAAIA,WAAqC;AAazC,eAAsB,oBAAoB,QAA6C;AACrF,KAAI,cACF;AAGF,KAAI;AACF,MAAI,CAAC,OAAO,OACV,OAAM,IAAI,MAAM,yCAAyC;EAG3D,MAAM,WAAW,IAAI,mBAAmB,OAAO,QAAQ,OAAO,aAAa;EAE3E,IAAIC;AACJ,MAAI,OAAO,YACT,iBAAgB,IAAI,mBAAmB,UAAU;GAC/C,cAAc;GACd,oBAAoB;GACpB,sBAAsB;GACvB,CAAC;MAEF,iBAAgB,IAAI,oBAAoB,SAAS;AAGnD,aAAW,IAAI,kBAAkB;GAC/B,UAAU,IAAI,SAAS,GACpB,oBAAoB,OAAO,eAAe,oBAC5C,CAAC;GACF,gBAAgB,CAAC,cAAc;GAChC,CAAC;EAIF,MAAM,EAAE,uBAAuB,MAAM,OAAO;AAE5C,WAAS,SAAS,EAChB,gBAAgB,IAAI,oBAAoB,EACzC,CAAC;AAEF,kBAAgB;UACT,OAAO;AACd,UAAQ,MAAM,yCAAyC,MAAM;AAC7D,QAAM"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracingHelpers.js","names":["currentFrameSpan: Span | undefined","ctx: Context"],"sources":["../../src/otel/tracingHelpers.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"tracingHelpers.js","names":["currentFrameSpan: Span | undefined","ctx: Context"],"sources":["../../src/otel/tracingHelpers.ts"],"sourcesContent":["import { type Context, context, propagation, type Span, trace } from \"@opentelemetry/api\";\n\nexport type TraceContext = Record<string, string>;\n\n/**\n * Global flag to enable/disable tracing.\n * When false, all tracing functions become no-ops for zero overhead.\n */\nlet tracingEnabled = false;\n\n/**\n * Enable tracing globally. Call this during initialization if tracing is requested.\n */\nexport function enableTracing(): void {\n tracingEnabled = true;\n}\n\n/**\n * Check if tracing is currently enabled.\n */\nexport function isTracingEnabled(): boolean {\n return tracingEnabled;\n}\n\n/**\n * Frame-local span storage for rendering.\n * Since rendering is single-threaded and sequential (one frame at a time),\n * we store the active frame span directly and use it as parent for orphaned spans.\n */\nlet currentFrameSpan: Span | undefined;\n\n/**\n * Set the current frame's span. Call this when starting a frame render.\n * All spans created during this frame will use this as parent if\n * Zone.js doesn't provide one via context.active()\n */\nexport function setCurrentFrameSpan(span: Span): void {\n currentFrameSpan = span;\n}\n\n/**\n * Clear the current frame span. Call this when a frame completes.\n */\nexport function clearCurrentFrameSpan(): void {\n currentFrameSpan = undefined;\n}\n\nexport function extractParentContext(traceContext?: TraceContext): Context {\n if (!traceContext) {\n return context.active();\n }\n\n try {\n return propagation.extract(context.active(), traceContext);\n } catch (_error) {\n return context.active();\n }\n}\n\n/**\n * Get the active span's context to pass to child operations\n * Use this when calling functions that create child spans\n */\nexport function getActiveContext(): Context {\n return context.active();\n}\n\n/**\n * Wrapper that passes span context explicitly to the function\n * Use this for operations that need to store or propagate context across boundaries\n */\nexport async function withSpanAndContext<T>(\n name: string,\n attributes: Record<string, string | number | boolean> | undefined,\n parentContext: Context | undefined,\n fn: (span: Span, activeContext: Context) => Promise<T>,\n): Promise<T> {\n // No-op if tracing is disabled\n if (!tracingEnabled) {\n // Create a minimal no-op span for compatibility\n const noopSpan = trace.getTracer(\"telecine-browser\").startSpan(name);\n const ctx = parentContext || context.active();\n const result = await fn(noopSpan, ctx);\n noopSpan.end();\n return result;\n }\n\n const tracer = trace.getTracer(\"telecine-browser\");\n\n // Same context resolution as withSpan\n let ctx: Context;\n\n if (parentContext) {\n ctx = parentContext;\n } else {\n const activeContext = context.active();\n const activeSpan = trace.getSpan(activeContext);\n\n if (activeSpan?.isRecording?.()) {\n ctx = activeContext;\n } else if (currentFrameSpan) {\n ctx = trace.setSpan(context.active(), currentFrameSpan);\n } else {\n ctx = activeContext;\n }\n }\n\n const span = tracer.startSpan(\n name,\n {\n attributes,\n },\n ctx,\n );\n\n // Create context with this span as active\n const spanContext = trace.setSpan(ctx, span);\n\n try {\n // Pass the spanContext explicitly to the function\n const result = await context.with(spanContext, async () => {\n return fn(span, spanContext);\n });\n span.end();\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.end();\n throw error;\n }\n}\n\nexport function createSpan(\n name: string,\n attributes?: Record<string, string | number | boolean>,\n parentContext?: Context,\n): Span {\n const tracer = trace.getTracer(\"telecine-browser\");\n const ctx = parentContext || context.active();\n\n return context.with(ctx, () => {\n const span = tracer.startSpan(name);\n\n if (attributes) {\n span.setAttributes(attributes);\n }\n\n return span;\n });\n}\n\nexport async function withSpan<T>(\n name: string,\n attributes: Record<string, string | number | boolean> | undefined,\n parentContext: Context | undefined,\n fn: (span: Span) => Promise<T>,\n): Promise<T> {\n // No-op if tracing is disabled\n if (!tracingEnabled) {\n // Create a minimal no-op span for compatibility\n const noopSpan = trace.getTracer(\"telecine-browser\").startSpan(name);\n const result = await fn(noopSpan);\n noopSpan.end();\n return result;\n }\n\n const tracer = trace.getTracer(\"telecine-browser\");\n\n // Context resolution priority:\n // 1. Explicit parentContext (if provided)\n // 2. context.active() from Zone.js (if it has a valid span)\n // 3. Create context from currentFrameSpan (fallback for Lit Task boundaries)\n let ctx: Context;\n\n if (parentContext) {\n ctx = parentContext;\n } else {\n const activeContext = context.active();\n const activeSpan = trace.getSpan(activeContext);\n\n // Try to use context.active() if it has a real span\n if (activeSpan?.isRecording?.()) {\n ctx = activeContext;\n } else if (currentFrameSpan) {\n // Create context from the stored frame span\n ctx = trace.setSpan(context.active(), currentFrameSpan);\n } else {\n ctx = activeContext;\n }\n }\n\n // Start span with explicit parent\n const span = tracer.startSpan(\n name,\n {\n attributes,\n },\n ctx,\n );\n\n // Create context with this span as active\n const spanContext = trace.setSpan(ctx, span);\n\n try {\n // Execute function with span context\n const result = await context.with(spanContext, async () => {\n return fn(span);\n });\n span.end();\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.end();\n throw error;\n }\n}\n\nexport function withSpanSync<T>(\n name: string,\n attributes: Record<string, string | number | boolean> | undefined,\n parentContext: Context | undefined,\n fn: (span: Span) => T,\n): T {\n // No-op if tracing is disabled\n if (!tracingEnabled) {\n // Create a minimal no-op span for compatibility\n const noopSpan = trace.getTracer(\"telecine-browser\").startSpan(name);\n const result = fn(noopSpan);\n noopSpan.end();\n return result;\n }\n\n const span = createSpan(name, attributes, parentContext);\n const ctx = parentContext || context.active();\n\n try {\n const result = context.with(trace.setSpan(ctx, span), () => fn(span));\n span.end();\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.end();\n throw error;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdaptiveResolutionTracker.js","names":[],"sources":["../../src/preview/AdaptiveResolutionTracker.ts"],"sourcesContent":["/**\n * Adaptive Resolution Tracker\n *\n * Monitors actual render time to dynamically adjust preview resolution\n * for smooth playback without dropped frames.\n *\n * Key insight: We measure how long each render() takes, not rAF timing.\n * If renders consistently take longer than our frame budget, scale down.\n * If renders consistently have headroom, scale up.\n */\n\nimport { logger } from \"./logger.js\";\n\n/**\n * Available resolution scale steps for adaptive scaling.\n * Finer-grained steps (5% increments) for smoother adaptation.\n * Ordered from highest to lowest quality.\n */\nconst SCALE_STEPS = [\n 1.0, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7, 0.65, 0.6, 0.55, 0.5, 0.45, 0.4, 0.35,\n 0.3, 0.25, 0.2, 0.15, 0.1,\n] as const;\ntype ScaleStep = (typeof SCALE_STEPS)[number];\n\n/**\n * Compute Pressure API types (not yet in TypeScript lib)\n */\ntype PressureState = \"nominal\" | \"fair\" | \"serious\" | \"critical\";\n\ninterface PressureRecord {\n state: PressureState;\n time: number;\n}\n\ninterface PressureObserverCallback {\n (records: PressureRecord[]): void;\n}\n\ninterface PressureObserverOptions {\n sampleInterval?: number;\n}\n\ndeclare class PressureObserver {\n constructor(callback: PressureObserverCallback);\n observe(source: \"cpu\", options?: PressureObserverOptions): Promise<void>;\n unobserve(source: \"cpu\"): void;\n disconnect(): void;\n}\n\n/**\n * Timing thresholds\n *\n * Target: 30fps = 33.33ms per frame\n * Tolerate down to 15fps (half target) before scaling down.\n * Scale up when we have plenty of headroom.\n */\nconst TARGET_FRAME_TIME_MS = 33.33; // 30fps target\nconst SCALE_DOWN_THRESHOLD_MS = 66.67; // 15fps (half target) - only scale down if really struggling\nconst SCALE_UP_THRESHOLD_MS = 25; // If avg render time is well below target, consider scaling up\nconst ROLLING_WINDOW_SIZE = 30; // ~1 second of samples at 30fps\nconst MIN_SCALE_CHANGE_INTERVAL_MS = 2000; // Wait 2s between any scale changes\nconst SCALE_UP_STABILITY_SAMPLES = 60; // Need 60 samples (~2s) of good performance to scale up\n\n/** Size of the pressure history for histogram display */\nconst PRESSURE_HISTORY_SIZE = 60;\n\n/**\n * Tracks render time to recommend optimal preview resolution.\n */\nexport class AdaptiveResolutionTracker {\n private renderTimes: number[] = []; // Rolling window of render times (ms)\n private currentScaleIndex = 0; // Index into SCALE_STEPS (0 = highest quality)\n private lastScaleChangeTime = 0;\n private samplesAtCurrentScale = 0; // How many samples we've collected at current scale\n\n // Compute Pressure API\n private pressureObserver: PressureObserver | null = null;\n private pressureState: PressureState = \"nominal\";\n private pressureHistory: PressureState[] = [];\n\n // For display - track frame intervals separately from render times\n private lastFrameTime = 0;\n private frameIntervals: number[] = [];\n\n // Callbacks\n private onScaleChange?: (scale: ScaleStep) => void;\n\n constructor(options?: { onScaleChange?: (scale: ScaleStep) => void }) {\n this.onScaleChange = options?.onScaleChange;\n this.initPressureObserver();\n }\n\n /**\n * Initialize Compute Pressure API observer if available.\n */\n private initPressureObserver(): void {\n if (!(\"PressureObserver\" in globalThis)) {\n return;\n }\n\n try {\n this.pressureObserver = new PressureObserver((records) => {\n if (records.length > 0) {\n const latest = records[records.length - 1]!;\n this.pressureState = latest.state;\n\n this.pressureHistory.push(latest.state);\n if (this.pressureHistory.length > PRESSURE_HISTORY_SIZE) {\n this.pressureHistory.shift();\n }\n }\n });\n\n this.pressureObserver\n .observe(\"cpu\", { sampleInterval: 500 })\n .catch(() => {\n // Ignore errors from observe (e.g., AbortError if disconnect called before observe resolves)\n });\n } catch (e) {\n logger.warn(\n \"[AdaptiveResolutionTracker] Failed to initialize PressureObserver:\",\n e,\n );\n this.pressureObserver = null;\n }\n }\n\n /**\n * Record a frame's render time.\n * Call this AFTER each render completes with how long the render took.\n *\n * @param renderTimeMs - How long the render() call took in milliseconds\n * @param timestamp - Optional rAF timestamp for frame interval tracking (display only)\n */\n recordFrame(renderTimeMs: number, timestamp?: number): void {\n // Track render times for adaptive decisions\n this.renderTimes.push(renderTimeMs);\n if (this.renderTimes.length > ROLLING_WINDOW_SIZE) {\n this.renderTimes.shift();\n }\n this.samplesAtCurrentScale++;\n\n // Track frame intervals for FPS display (separate from render time)\n if (timestamp !== undefined && this.lastFrameTime > 0) {\n const interval = timestamp - this.lastFrameTime;\n this.frameIntervals.push(interval);\n if (this.frameIntervals.length > ROLLING_WINDOW_SIZE) {\n this.frameIntervals.shift();\n }\n }\n if (timestamp !== undefined) {\n this.lastFrameTime = timestamp;\n }\n\n // Check if we should adjust scale\n this.checkForScaleAdjustment();\n }\n\n /**\n * Check if we should scale up or down based on render time trends.\n */\n private checkForScaleAdjustment(): void {\n if (this.renderTimes.length < 10) return; // Need some samples\n\n const now = performance.now();\n if (now - this.lastScaleChangeTime < MIN_SCALE_CHANGE_INTERVAL_MS) {\n return; // Rate limit changes\n }\n\n const avgRenderTime =\n this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length;\n\n // Scale DOWN if we're consistently slow\n if (avgRenderTime > SCALE_DOWN_THRESHOLD_MS) {\n this.scaleDown(\"slow\");\n return;\n }\n\n // Scale DOWN if CPU pressure is high (proactive)\n if (this.pressureState === \"critical\" || this.pressureState === \"serious\") {\n this.scaleDown(\"pressure\");\n return;\n }\n\n // Scale UP if we have sustained headroom and CPU isn't under pressure\n // (we already returned above if pressure is serious/critical, but check again for clarity)\n const pressureOk =\n this.pressureState === \"nominal\" || this.pressureState === \"fair\";\n if (\n avgRenderTime < SCALE_UP_THRESHOLD_MS &&\n this.samplesAtCurrentScale >= SCALE_UP_STABILITY_SAMPLES &&\n pressureOk\n ) {\n this.scaleUp();\n }\n }\n\n /**\n * Decrease resolution (increase scale index).\n */\n private scaleDown(reason: \"slow\" | \"pressure\"): void {\n if (this.currentScaleIndex < SCALE_STEPS.length - 1) {\n this.currentScaleIndex++;\n this.lastScaleChangeTime = performance.now();\n this.samplesAtCurrentScale = 0;\n this.renderTimes = []; // Clear history at new scale\n\n const newScale = SCALE_STEPS[this.currentScaleIndex]!;\n logger.debug(\n `[AdaptiveResolutionTracker] Scaling DOWN to ${(newScale * 100).toFixed(0)}% (reason: ${reason})`,\n );\n this.onScaleChange?.(newScale);\n }\n }\n\n /**\n * Increase resolution (decrease scale index).\n */\n private scaleUp(): void {\n if (this.currentScaleIndex > 0) {\n this.currentScaleIndex--;\n this.lastScaleChangeTime = performance.now();\n this.samplesAtCurrentScale = 0;\n this.renderTimes = []; // Clear history at new scale\n\n const newScale = SCALE_STEPS[this.currentScaleIndex]!;\n logger.debug(\n `[AdaptiveResolutionTracker] Scaling UP to ${(newScale * 100).toFixed(0)}% (reason: stable performance)`,\n );\n this.onScaleChange?.(newScale);\n }\n }\n\n /**\n * Get the current recommended scale factor.\n */\n getRecommendedScale(): ScaleStep {\n return SCALE_STEPS[this.currentScaleIndex]!;\n }\n\n /**\n * Get current statistics for display.\n */\n getStats(): {\n currentScale: ScaleStep;\n avgRenderTime: number;\n fps: number;\n pressureState: PressureState;\n pressureHistory: PressureState[];\n samplesAtCurrentScale: number;\n canScaleUp: boolean;\n canScaleDown: boolean;\n headroom: number; // How much faster than target we're rendering (negative = behind)\n } {\n const avgRenderTime =\n this.renderTimes.length > 0\n ? this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length\n : 0;\n\n // FPS based on frame intervals (how often we're called), not render time\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 const now = performance.now();\n const timeSinceLastChange = now - this.lastScaleChangeTime;\n const canChange = timeSinceLastChange >= MIN_SCALE_CHANGE_INTERVAL_MS;\n\n const pressureOk =\n this.pressureState === \"nominal\" || this.pressureState === \"fair\";\n const canScaleUp =\n canChange &&\n this.currentScaleIndex > 0 &&\n avgRenderTime < SCALE_UP_THRESHOLD_MS &&\n this.samplesAtCurrentScale >= SCALE_UP_STABILITY_SAMPLES &&\n pressureOk;\n\n const canScaleDown =\n canChange && this.currentScaleIndex < SCALE_STEPS.length - 1;\n\n // Headroom: positive = we're faster than needed, negative = we're behind\n const headroom = TARGET_FRAME_TIME_MS - avgRenderTime;\n\n return {\n currentScale: this.getRecommendedScale(),\n avgRenderTime,\n fps,\n pressureState: this.pressureState,\n pressureHistory: [...this.pressureHistory],\n samplesAtCurrentScale: this.samplesAtCurrentScale,\n canScaleUp,\n canScaleDown,\n headroom,\n };\n }\n\n /**\n * Reset the tracker state.\n */\n reset(): void {\n this.lastFrameTime = 0;\n this.frameIntervals = [];\n this.renderTimes = [];\n this.currentScaleIndex = 0;\n this.lastScaleChangeTime = 0;\n this.samplesAtCurrentScale = 0;\n }\n\n /**\n * Initialize the tracker to start at a specific scale.\n */\n initializeAtScale(targetScale: number): void {\n let bestIndex = 0;\n for (let i = 0; i < SCALE_STEPS.length; i++) {\n if (SCALE_STEPS[i]! <= targetScale) {\n bestIndex = i;\n break;\n }\n }\n\n this.currentScaleIndex = bestIndex;\n this.lastFrameTime = 0;\n this.frameIntervals = [];\n this.renderTimes = [];\n this.lastScaleChangeTime = 0;\n this.samplesAtCurrentScale = 0;\n\n logger.debug(\n `[AdaptiveResolutionTracker] Initialized at scale ${(SCALE_STEPS[bestIndex]! * 100).toFixed(0)}%`,\n );\n }\n\n /**\n * Clean up resources.\n */\n dispose(): void {\n if (this.pressureObserver) {\n try {\n this.pressureObserver.disconnect();\n } catch {\n // Ignore cleanup errors\n }\n this.pressureObserver = null;\n }\n }\n}\n"],"mappings":";;;;;;;;AAkBA,MAAM,cAAc;CAClB;CAAK;CAAM;CAAK;CAAM;CAAK;CAAM;CAAK;CAAM;CAAK;CAAM;CAAK;CAAM;CAAK;CACvE;CAAK;CAAM;CAAK;CAAM;CACvB;;;;;;;;AAmCD,MAAM,uBAAuB;AAC7B,MAAM,0BAA0B;AAChC,MAAM,wBAAwB;AAC9B,MAAM,sBAAsB;AAC5B,MAAM,+BAA+B;AACrC,MAAM,6BAA6B;;AAGnC,MAAM,wBAAwB;;;;AAK9B,IAAa,4BAAb,MAAuC;CAkBrC,YAAY,SAA0D;qBAjBtC,EAAE;2BACN;6BACE;+BACE;0BAGoB;uBACb;yBACI,EAAE;uBAGrB;wBACW,EAAE;AAMnC,OAAK,gBAAgB,SAAS;AAC9B,OAAK,sBAAsB;;;;;CAM7B,AAAQ,uBAA6B;AACnC,MAAI,EAAE,sBAAsB,YAC1B;AAGF,MAAI;AACF,QAAK,mBAAmB,IAAI,kBAAkB,YAAY;AACxD,QAAI,QAAQ,SAAS,GAAG;KACtB,MAAM,SAAS,QAAQ,QAAQ,SAAS;AACxC,UAAK,gBAAgB,OAAO;AAE5B,UAAK,gBAAgB,KAAK,OAAO,MAAM;AACvC,SAAI,KAAK,gBAAgB,SAAS,sBAChC,MAAK,gBAAgB,OAAO;;KAGhC;AAEF,QAAK,iBACF,QAAQ,OAAO,EAAE,gBAAgB,KAAK,CAAC,CACvC,YAAY,GAEX;WACG,GAAG;AACV,UAAO,KACL,sEACA,EACD;AACD,QAAK,mBAAmB;;;;;;;;;;CAW5B,YAAY,cAAsB,WAA0B;AAE1D,OAAK,YAAY,KAAK,aAAa;AACnC,MAAI,KAAK,YAAY,SAAS,oBAC5B,MAAK,YAAY,OAAO;AAE1B,OAAK;AAGL,MAAI,cAAc,UAAa,KAAK,gBAAgB,GAAG;GACrD,MAAM,WAAW,YAAY,KAAK;AAClC,QAAK,eAAe,KAAK,SAAS;AAClC,OAAI,KAAK,eAAe,SAAS,oBAC/B,MAAK,eAAe,OAAO;;AAG/B,MAAI,cAAc,OAChB,MAAK,gBAAgB;AAIvB,OAAK,yBAAyB;;;;;CAMhC,AAAQ,0BAAgC;AACtC,MAAI,KAAK,YAAY,SAAS,GAAI;AAGlC,MADY,YAAY,KAAK,GACnB,KAAK,sBAAsB,6BACnC;EAGF,MAAM,gBACJ,KAAK,YAAY,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,YAAY;AAGjE,MAAI,gBAAgB,yBAAyB;AAC3C,QAAK,UAAU,OAAO;AACtB;;AAIF,MAAI,KAAK,kBAAkB,cAAc,KAAK,kBAAkB,WAAW;AACzE,QAAK,UAAU,WAAW;AAC1B;;EAKF,MAAM,aACJ,KAAK,kBAAkB,aAAa,KAAK,kBAAkB;AAC7D,MACE,gBAAgB,yBAChB,KAAK,yBAAyB,8BAC9B,WAEA,MAAK,SAAS;;;;;CAOlB,AAAQ,UAAU,QAAmC;AACnD,MAAI,KAAK,oBAAoB,YAAY,SAAS,GAAG;AACnD,QAAK;AACL,QAAK,sBAAsB,YAAY,KAAK;AAC5C,QAAK,wBAAwB;AAC7B,QAAK,cAAc,EAAE;GAErB,MAAM,WAAW,YAAY,KAAK;AAClC,UAAO,MACL,gDAAgD,WAAW,KAAK,QAAQ,EAAE,CAAC,aAAa,OAAO,GAChG;AACD,QAAK,gBAAgB,SAAS;;;;;;CAOlC,AAAQ,UAAgB;AACtB,MAAI,KAAK,oBAAoB,GAAG;AAC9B,QAAK;AACL,QAAK,sBAAsB,YAAY,KAAK;AAC5C,QAAK,wBAAwB;AAC7B,QAAK,cAAc,EAAE;GAErB,MAAM,WAAW,YAAY,KAAK;AAClC,UAAO,MACL,8CAA8C,WAAW,KAAK,QAAQ,EAAE,CAAC,gCAC1E;AACD,QAAK,gBAAgB,SAAS;;;;;;CAOlC,sBAAiC;AAC/B,SAAO,YAAY,KAAK;;;;;CAM1B,WAUE;EACA,MAAM,gBACJ,KAAK,YAAY,SAAS,IACtB,KAAK,YAAY,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,YAAY,SAC/D;EAGN,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;EAI7D,MAAM,YAFM,YAAY,KAAK,GACK,KAAK,uBACE;EAEzC,MAAM,aACJ,KAAK,kBAAkB,aAAa,KAAK,kBAAkB;EAC7D,MAAM,aACJ,aACA,KAAK,oBAAoB,KACzB,gBAAgB,yBAChB,KAAK,yBAAyB,8BAC9B;EAEF,MAAM,eACJ,aAAa,KAAK,oBAAoB,YAAY,SAAS;EAG7D,MAAM,WAAW,uBAAuB;AAExC,SAAO;GACL,cAAc,KAAK,qBAAqB;GACxC;GACA;GACA,eAAe,KAAK;GACpB,iBAAiB,CAAC,GAAG,KAAK,gBAAgB;GAC1C,uBAAuB,KAAK;GAC5B;GACA;GACA;GACD;;;;;CAMH,QAAc;AACZ,OAAK,gBAAgB;AACrB,OAAK,iBAAiB,EAAE;AACxB,OAAK,cAAc,EAAE;AACrB,OAAK,oBAAoB;AACzB,OAAK,sBAAsB;AAC3B,OAAK,wBAAwB;;;;;CAM/B,kBAAkB,aAA2B;EAC3C,IAAI,YAAY;AAChB,OAAK,IAAI,IAAI,GAAG,IAAI,YAAY,QAAQ,IACtC,KAAI,YAAY,MAAO,aAAa;AAClC,eAAY;AACZ;;AAIJ,OAAK,oBAAoB;AACzB,OAAK,gBAAgB;AACrB,OAAK,iBAAiB,EAAE;AACxB,OAAK,cAAc,EAAE;AACrB,OAAK,sBAAsB;AAC3B,OAAK,wBAAwB;AAE7B,SAAO,MACL,qDAAqD,YAAY,aAAc,KAAK,QAAQ,EAAE,CAAC,GAChG;;;;;CAMH,UAAgB;AACd,MAAI,KAAK,kBAAkB;AACzB,OAAI;AACF,SAAK,iBAAiB,YAAY;WAC5B;AAGR,QAAK,mBAAmB"}
|
|
1
|
+
{"version":3,"file":"AdaptiveResolutionTracker.js","names":[],"sources":["../../src/preview/AdaptiveResolutionTracker.ts"],"sourcesContent":["/**\n * Adaptive Resolution Tracker\n *\n * Monitors actual render time to dynamically adjust preview resolution\n * for smooth playback without dropped frames.\n *\n * Key insight: We measure how long each render() takes, not rAF timing.\n * If renders consistently take longer than our frame budget, scale down.\n * If renders consistently have headroom, scale up.\n */\n\nimport { logger } from \"./logger.js\";\n\n/**\n * Available resolution scale steps for adaptive scaling.\n * Finer-grained steps (5% increments) for smoother adaptation.\n * Ordered from highest to lowest quality.\n */\nconst SCALE_STEPS = [\n 1.0, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7, 0.65, 0.6, 0.55, 0.5, 0.45, 0.4, 0.35, 0.3, 0.25, 0.2, 0.15,\n 0.1,\n] as const;\ntype ScaleStep = (typeof SCALE_STEPS)[number];\n\n/**\n * Compute Pressure API types (not yet in TypeScript lib)\n */\ntype PressureState = \"nominal\" | \"fair\" | \"serious\" | \"critical\";\n\ninterface PressureRecord {\n state: PressureState;\n time: number;\n}\n\ninterface PressureObserverCallback {\n (records: PressureRecord[]): void;\n}\n\ninterface PressureObserverOptions {\n sampleInterval?: number;\n}\n\ndeclare class PressureObserver {\n constructor(callback: PressureObserverCallback);\n observe(source: \"cpu\", options?: PressureObserverOptions): Promise<void>;\n unobserve(source: \"cpu\"): void;\n disconnect(): void;\n}\n\n/**\n * Timing thresholds\n *\n * Target: 30fps = 33.33ms per frame\n * Tolerate down to 15fps (half target) before scaling down.\n * Scale up when we have plenty of headroom.\n */\nconst TARGET_FRAME_TIME_MS = 33.33; // 30fps target\nconst SCALE_DOWN_THRESHOLD_MS = 66.67; // 15fps (half target) - only scale down if really struggling\nconst SCALE_UP_THRESHOLD_MS = 25; // If avg render time is well below target, consider scaling up\nconst ROLLING_WINDOW_SIZE = 30; // ~1 second of samples at 30fps\nconst MIN_SCALE_CHANGE_INTERVAL_MS = 2000; // Wait 2s between any scale changes\nconst SCALE_UP_STABILITY_SAMPLES = 60; // Need 60 samples (~2s) of good performance to scale up\n\n/** Size of the pressure history for histogram display */\nconst PRESSURE_HISTORY_SIZE = 60;\n\n/**\n * Tracks render time to recommend optimal preview resolution.\n */\nexport class AdaptiveResolutionTracker {\n private renderTimes: number[] = []; // Rolling window of render times (ms)\n private currentScaleIndex = 0; // Index into SCALE_STEPS (0 = highest quality)\n private lastScaleChangeTime = 0;\n private samplesAtCurrentScale = 0; // How many samples we've collected at current scale\n\n // Compute Pressure API\n private pressureObserver: PressureObserver | null = null;\n private pressureState: PressureState = \"nominal\";\n private pressureHistory: PressureState[] = [];\n\n // For display - track frame intervals separately from render times\n private lastFrameTime = 0;\n private frameIntervals: number[] = [];\n\n // Callbacks\n private onScaleChange?: (scale: ScaleStep) => void;\n\n constructor(options?: { onScaleChange?: (scale: ScaleStep) => void }) {\n this.onScaleChange = options?.onScaleChange;\n this.initPressureObserver();\n }\n\n /**\n * Initialize Compute Pressure API observer if available.\n */\n private initPressureObserver(): void {\n if (!(\"PressureObserver\" in globalThis)) {\n return;\n }\n\n try {\n this.pressureObserver = new PressureObserver((records) => {\n if (records.length > 0) {\n const latest = records[records.length - 1]!;\n this.pressureState = latest.state;\n\n this.pressureHistory.push(latest.state);\n if (this.pressureHistory.length > PRESSURE_HISTORY_SIZE) {\n this.pressureHistory.shift();\n }\n }\n });\n\n this.pressureObserver.observe(\"cpu\", { sampleInterval: 500 }).catch(() => {\n // Ignore errors from observe (e.g., AbortError if disconnect called before observe resolves)\n });\n } catch (e) {\n logger.warn(\"[AdaptiveResolutionTracker] Failed to initialize PressureObserver:\", e);\n this.pressureObserver = null;\n }\n }\n\n /**\n * Record a frame's render time.\n * Call this AFTER each render completes with how long the render took.\n *\n * @param renderTimeMs - How long the render() call took in milliseconds\n * @param timestamp - Optional rAF timestamp for frame interval tracking (display only)\n */\n recordFrame(renderTimeMs: number, timestamp?: number): void {\n // Track render times for adaptive decisions\n this.renderTimes.push(renderTimeMs);\n if (this.renderTimes.length > ROLLING_WINDOW_SIZE) {\n this.renderTimes.shift();\n }\n this.samplesAtCurrentScale++;\n\n // Track frame intervals for FPS display (separate from render time)\n if (timestamp !== undefined && this.lastFrameTime > 0) {\n const interval = timestamp - this.lastFrameTime;\n this.frameIntervals.push(interval);\n if (this.frameIntervals.length > ROLLING_WINDOW_SIZE) {\n this.frameIntervals.shift();\n }\n }\n if (timestamp !== undefined) {\n this.lastFrameTime = timestamp;\n }\n\n // Check if we should adjust scale\n this.checkForScaleAdjustment();\n }\n\n /**\n * Check if we should scale up or down based on render time trends.\n */\n private checkForScaleAdjustment(): void {\n if (this.renderTimes.length < 10) return; // Need some samples\n\n const now = performance.now();\n if (now - this.lastScaleChangeTime < MIN_SCALE_CHANGE_INTERVAL_MS) {\n return; // Rate limit changes\n }\n\n const avgRenderTime = this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length;\n\n // Scale DOWN if we're consistently slow\n if (avgRenderTime > SCALE_DOWN_THRESHOLD_MS) {\n this.scaleDown(\"slow\");\n return;\n }\n\n // Scale DOWN if CPU pressure is high (proactive)\n if (this.pressureState === \"critical\" || this.pressureState === \"serious\") {\n this.scaleDown(\"pressure\");\n return;\n }\n\n // Scale UP if we have sustained headroom and CPU isn't under pressure\n // (we already returned above if pressure is serious/critical, but check again for clarity)\n const pressureOk = this.pressureState === \"nominal\" || this.pressureState === \"fair\";\n if (\n avgRenderTime < SCALE_UP_THRESHOLD_MS &&\n this.samplesAtCurrentScale >= SCALE_UP_STABILITY_SAMPLES &&\n pressureOk\n ) {\n this.scaleUp();\n }\n }\n\n /**\n * Decrease resolution (increase scale index).\n */\n private scaleDown(reason: \"slow\" | \"pressure\"): void {\n if (this.currentScaleIndex < SCALE_STEPS.length - 1) {\n this.currentScaleIndex++;\n this.lastScaleChangeTime = performance.now();\n this.samplesAtCurrentScale = 0;\n this.renderTimes = []; // Clear history at new scale\n\n const newScale = SCALE_STEPS[this.currentScaleIndex]!;\n logger.debug(\n `[AdaptiveResolutionTracker] Scaling DOWN to ${(newScale * 100).toFixed(0)}% (reason: ${reason})`,\n );\n this.onScaleChange?.(newScale);\n }\n }\n\n /**\n * Increase resolution (decrease scale index).\n */\n private scaleUp(): void {\n if (this.currentScaleIndex > 0) {\n this.currentScaleIndex--;\n this.lastScaleChangeTime = performance.now();\n this.samplesAtCurrentScale = 0;\n this.renderTimes = []; // Clear history at new scale\n\n const newScale = SCALE_STEPS[this.currentScaleIndex]!;\n logger.debug(\n `[AdaptiveResolutionTracker] Scaling UP to ${(newScale * 100).toFixed(0)}% (reason: stable performance)`,\n );\n this.onScaleChange?.(newScale);\n }\n }\n\n /**\n * Get the current recommended scale factor.\n */\n getRecommendedScale(): ScaleStep {\n return SCALE_STEPS[this.currentScaleIndex]!;\n }\n\n /**\n * Get current statistics for display.\n */\n getStats(): {\n currentScale: ScaleStep;\n avgRenderTime: number;\n fps: number;\n pressureState: PressureState;\n pressureHistory: PressureState[];\n samplesAtCurrentScale: number;\n canScaleUp: boolean;\n canScaleDown: boolean;\n headroom: number; // How much faster than target we're rendering (negative = behind)\n } {\n const avgRenderTime =\n this.renderTimes.length > 0\n ? this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length\n : 0;\n\n // FPS based on frame intervals (how often we're called), not render time\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 const now = performance.now();\n const timeSinceLastChange = now - this.lastScaleChangeTime;\n const canChange = timeSinceLastChange >= MIN_SCALE_CHANGE_INTERVAL_MS;\n\n const pressureOk = this.pressureState === \"nominal\" || this.pressureState === \"fair\";\n const canScaleUp =\n canChange &&\n this.currentScaleIndex > 0 &&\n avgRenderTime < SCALE_UP_THRESHOLD_MS &&\n this.samplesAtCurrentScale >= SCALE_UP_STABILITY_SAMPLES &&\n pressureOk;\n\n const canScaleDown = canChange && this.currentScaleIndex < SCALE_STEPS.length - 1;\n\n // Headroom: positive = we're faster than needed, negative = we're behind\n const headroom = TARGET_FRAME_TIME_MS - avgRenderTime;\n\n return {\n currentScale: this.getRecommendedScale(),\n avgRenderTime,\n fps,\n pressureState: this.pressureState,\n pressureHistory: [...this.pressureHistory],\n samplesAtCurrentScale: this.samplesAtCurrentScale,\n canScaleUp,\n canScaleDown,\n headroom,\n };\n }\n\n /**\n * Reset the tracker state.\n */\n reset(): void {\n this.lastFrameTime = 0;\n this.frameIntervals = [];\n this.renderTimes = [];\n this.currentScaleIndex = 0;\n this.lastScaleChangeTime = 0;\n this.samplesAtCurrentScale = 0;\n }\n\n /**\n * Initialize the tracker to start at a specific scale.\n */\n initializeAtScale(targetScale: number): void {\n let bestIndex = 0;\n for (let i = 0; i < SCALE_STEPS.length; i++) {\n if (SCALE_STEPS[i]! <= targetScale) {\n bestIndex = i;\n break;\n }\n }\n\n this.currentScaleIndex = bestIndex;\n this.lastFrameTime = 0;\n this.frameIntervals = [];\n this.renderTimes = [];\n this.lastScaleChangeTime = 0;\n this.samplesAtCurrentScale = 0;\n\n logger.debug(\n `[AdaptiveResolutionTracker] Initialized at scale ${(SCALE_STEPS[bestIndex]! * 100).toFixed(0)}%`,\n );\n }\n\n /**\n * Clean up resources.\n */\n dispose(): void {\n if (this.pressureObserver) {\n try {\n this.pressureObserver.disconnect();\n } catch {\n // Ignore cleanup errors\n }\n this.pressureObserver = null;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FrameController.js","names":["#rootElement","#abortController","#lastRenderedTimeMs","#renderInProgress","#pendingRenderTime","#queryVisibleElements","result: FrameRenderable[]","#getChildrenIncludingSlotted","assignedElements: Element[]"],"sources":["../../src/preview/FrameController.ts"],"sourcesContent":["/**\n * FrameController: Centralized frame rendering control\n *\n * Replaces the distributed Lit Task hierarchy with a single control loop\n * that queries elements and coordinates rendering directly.\n *\n * Benefits over the previous Task-based system:\n * - Single abort controller instead of distributed abort handling\n * - Clear prepare → render phases\n * - All coordination visible in one place\n * - No Lit Task reactivity overhead\n */\n\nimport type { LitElement } from \"lit\";\n\n// ============================================================================\n// Priority Constants\n// ============================================================================\n// Lower numbers render first. Elements with dependencies should have higher\n// priority numbers than their dependencies.\n//\n// Example: Waveform depends on audio analysis data, so it renders after audio.\n// ============================================================================\n\n/**\n * Priority for video elements.\n * Video renders first as other elements may depend on video frames being ready.\n */\nexport const PRIORITY_VIDEO = 1;\n\n/**\n * Priority for captions elements.\n * Captions render after video so they can overlay correctly.\n */\nexport const PRIORITY_CAPTIONS = 2;\n\n/**\n * Priority for audio elements.\n * Audio renders after captions (no visual dependency, but keeps consistent ordering).\n */\nexport const PRIORITY_AUDIO = 3;\n\n/**\n * Priority for waveform elements.\n * Waveform renders after audio because it depends on audio analysis data.\n */\nexport const PRIORITY_WAVEFORM = 4;\n\n/**\n * Priority for image elements.\n * Images render with low priority as they're typically static.\n */\nexport const PRIORITY_IMAGE = 5;\n\n/**\n * Default priority for elements that don't specify one.\n * High number ensures custom elements render after standard elements.\n */\nexport const PRIORITY_DEFAULT = 100;\n\n/**\n * State returned by elements describing their readiness for a given time.\n */\nexport interface FrameState {\n /**\n * Whether async preparation is needed before rendering.\n * Examples: video needs to seek, captions need to load data.\n */\n needsPreparation: boolean;\n\n /**\n * Whether the element is ready to render synchronously.\n * True when all async work is complete and renderFrame() can be called.\n */\n isReady: boolean;\n\n /**\n * Rendering priority hint. Lower numbers render first.\n * Used to order render calls for elements with dependencies.\n *\n * Standard priorities:\n * - PRIORITY_VIDEO (1): Video elements\n * - PRIORITY_CAPTIONS (2): Caption overlays\n * - PRIORITY_AUDIO (3): Audio elements\n * - PRIORITY_WAVEFORM (4): Audio visualizers (depend on audio)\n * - PRIORITY_IMAGE (5): Static images\n * - PRIORITY_DEFAULT (100): Fallback for custom elements\n */\n priority: number;\n}\n\n/**\n * Interface that elements implement to participate in centralized frame rendering.\n * Elements keep their rendering logic local but expose a standardized interface.\n */\nexport interface FrameRenderable {\n /**\n * Query the element's readiness state for a given time.\n * Must be synchronous and cheap to call.\n */\n getFrameState(timeMs: number): FrameState;\n\n /**\n * Async preparation phase. Called when getFrameState().needsPreparation is true.\n * Performs any async work needed before rendering (seeking, loading, etc.).\n *\n * @param timeMs - The time to prepare for\n * @param signal - Abort signal for cancellation\n */\n prepareFrame(timeMs: number, signal: AbortSignal): Promise<void>;\n\n /**\n * Synchronous render phase. Called after all preparation is complete.\n * Performs the actual rendering (paint to canvas, update DOM, etc.).\n *\n * @param timeMs - The time to render\n */\n renderFrame(timeMs: number): void;\n}\n\n/**\n * Type guard to check if an element implements FrameRenderable.\n */\nexport function isFrameRenderable(\n element: unknown,\n): element is FrameRenderable {\n return (\n typeof element === \"object\" &&\n element !== null &&\n \"getFrameState\" in element &&\n \"prepareFrame\" in element &&\n \"renderFrame\" in element &&\n typeof (element as FrameRenderable).getFrameState === \"function\" &&\n typeof (element as FrameRenderable).prepareFrame === \"function\" &&\n typeof (element as FrameRenderable).renderFrame === \"function\"\n );\n}\n\n/**\n * Per-phase timing data returned by FrameController.renderFrame().\n * All values are in milliseconds.\n */\nexport interface RenderFrameTiming {\n queryMs: number;\n prepareMs: number;\n renderMs: number;\n animsMs: number;\n}\n\n/**\n * Options for FrameController.renderFrame()\n */\nexport interface RenderFrameOptions {\n /**\n * Whether to wait for Lit updateComplete before querying elements.\n * Default: true\n */\n waitForLitUpdate?: boolean;\n\n /**\n * Callback to update CSS animations after frame rendering completes.\n * Called with the root element after all elements have rendered.\n * This centralizes animation synchronization in one place.\n */\n onAnimationsUpdate?: (rootElement: Element) => void;\n}\n\n/**\n * Central controller for frame rendering.\n * Lives at the root timegroup and orchestrates all element rendering.\n */\nexport class FrameController {\n #rootElement: LitElement & { currentTimeMs: number };\n #abortController: AbortController | null = null;\n #renderInProgress = false;\n #pendingRenderTime: number | null = null;\n /**\n * Last successfully rendered time. Used for deduplication when multiple\n * callers (e.g., PlaybackController RAF loop and canvas render loop)\n * both try to render the same frame within one animation frame.\n */\n #lastRenderedTimeMs: number = -1;\n\n constructor(rootElement: LitElement & { currentTimeMs: number }) {\n this.#rootElement = rootElement;\n }\n\n /**\n * Cancel any in-progress render operation and reset deduplication state.\n */\n abort(): void {\n this.#abortController?.abort();\n this.#abortController = null;\n // Reset deduplication state so next render goes through even if same time\n this.#lastRenderedTimeMs = -1;\n }\n\n /**\n * Render a frame at the specified time.\n *\n * This is the main entry point for frame rendering. It:\n * 1. Cancels any previous in-progress render\n * 2. Queries all visible FrameRenderable elements\n * 3. Runs preparation in parallel for elements that need it\n * 4. Runs render in priority order\n *\n * @param timeMs - The time in milliseconds to render\n * @param options - Optional configuration\n */\n async renderFrame(\n timeMs: number,\n options: RenderFrameOptions = {},\n ): Promise<RenderFrameTiming | null> {\n const { waitForLitUpdate = true, onAnimationsUpdate } = options;\n\n // Deduplicate: skip if we just rendered this exact time.\n // This prevents double-rendering when multiple RAF loops (e.g., PlaybackController\n // and canvas render loop) both call renderFrame() for the same frame.\n if (timeMs === this.#lastRenderedTimeMs) {\n return null;\n }\n\n // If a render is in progress, queue this one\n if (this.#renderInProgress) {\n this.#pendingRenderTime = timeMs;\n return null;\n }\n\n // Cancel any previous render operation\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n const signal = this.#abortController.signal;\n\n this.#renderInProgress = true;\n\n try {\n if (waitForLitUpdate) {\n await this.#rootElement.updateComplete;\n signal.throwIfAborted();\n }\n\n const tQuery = performance.now();\n const elements = this.#queryVisibleElements(timeMs);\n const queryMs = performance.now() - tQuery;\n signal.throwIfAborted();\n\n const tPrepare = performance.now();\n const elementsNeedingPreparation = elements.filter(\n (el) => el.getFrameState(timeMs).needsPreparation,\n );\n\n if (elementsNeedingPreparation.length > 0) {\n await Promise.all(\n elementsNeedingPreparation.map((el) =>\n el.prepareFrame(timeMs, signal),\n ),\n );\n signal.throwIfAborted();\n }\n const prepareMs = performance.now() - tPrepare;\n\n const tRender = performance.now();\n const sortedElements = [...elements].sort(\n (a, b) =>\n a.getFrameState(timeMs).priority - b.getFrameState(timeMs).priority,\n );\n\n for (const element of sortedElements) {\n signal.throwIfAborted();\n element.renderFrame(timeMs);\n }\n const renderMs = performance.now() - tRender;\n\n const tAnims = performance.now();\n if (onAnimationsUpdate) {\n onAnimationsUpdate(this.#rootElement);\n }\n const animsMs = performance.now() - tAnims;\n\n this.#lastRenderedTimeMs = timeMs;\n return { queryMs, prepareMs, renderMs, animsMs };\n } finally {\n this.#renderInProgress = false;\n\n // Process any queued render\n if (this.#pendingRenderTime !== null) {\n const pendingTime = this.#pendingRenderTime;\n this.#pendingRenderTime = null;\n // Don't await - fire and forget to avoid recursive waiting\n this.renderFrame(pendingTime, options).catch(() => {\n // Silently ignore errors from queued renders (likely aborted)\n });\n }\n }\n }\n\n /**\n * Query all visible FrameRenderable elements in the tree.\n * Uses temporal visibility to filter out elements not visible at current time.\n *\n * IMPORTANT: For temporal elements, we use temporal visibility (startTimeMs/endTimeMs)\n * instead of CSS visibility. This is because updateAnimations sets display:none on\n * elements outside their time range, but that CSS state is from the PREVIOUS frame.\n * When seeking, we need to evaluate visibility based on the NEW time, not stale CSS.\n *\n * @param timeMs - The time to use for visibility checks. This should be the target\n * render time, not read from root element (which may be stale).\n */\n #queryVisibleElements(timeMs: number): FrameRenderable[] {\n const result: FrameRenderable[] = [];\n const currentTimeMs = timeMs;\n\n const walk = (element: Element): void => {\n // For temporal elements (ef-timegroup, ef-video, etc.), use temporal visibility\n // instead of CSS visibility. CSS display:none may be stale from previous frame.\n const isTemporal = \"startTimeMs\" in element && \"endTimeMs\" in element;\n\n if (isTemporal) {\n // Temporal element: check time-based visibility\n // Use exclusive end (< not <=) to avoid overlap at boundaries\n const startMs =\n (element as { startTimeMs?: number }).startTimeMs ?? -Infinity;\n const endMs = (element as { endTimeMs?: number }).endTimeMs ?? Infinity;\n const isTemporallyVisible =\n currentTimeMs >= startMs && currentTimeMs < endMs;\n\n if (!isTemporallyVisible) {\n // Skip this element AND its children (children's times are relative to parent)\n return;\n }\n\n // Element is temporally visible - include if it implements FrameRenderable\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n } else {\n // Non-temporal element: only check inline display style (fast path).\n // Skip getComputedStyle — it forces synchronous style recalc and is\n // unnecessary because FrameRenderable elements are always temporal.\n // We only walk non-temporal elements to reach temporal children.\n if (\n element instanceof HTMLElement &&\n element.style.display === \"none\"\n ) {\n return;\n }\n\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n }\n\n // Walk children - handle both regular children and slotted content\n const children = this.#getChildrenIncludingSlotted(element);\n for (const child of children) {\n walk(child);\n }\n };\n\n walk(this.#rootElement);\n return result;\n }\n\n /**\n * Gets all child elements including slotted content for shadow DOM elements.\n * For elements with shadow DOM that contain slots, this returns the assigned\n * elements (slotted content) instead of just the shadow DOM children.\n */\n #getChildrenIncludingSlotted(element: Element): Iterable<Element> {\n // If element has shadowRoot with slots, get assigned elements\n if (element.shadowRoot) {\n const slots = element.shadowRoot.querySelectorAll(\"slot\");\n if (slots.length > 0) {\n const assignedElements: Element[] = [];\n for (const slot of slots) {\n assignedElements.push(...slot.assignedElements());\n }\n // Also include shadow DOM children that aren't slots (for mixed content)\n for (const child of element.shadowRoot.children) {\n if (child.tagName !== \"SLOT\") {\n assignedElements.push(child);\n }\n }\n return assignedElements;\n }\n }\n\n // Return HTMLCollection directly (iterable, no allocation)\n return element.children;\n }\n\n /**\n * Check if a render is currently in progress.\n */\n get isRendering(): boolean {\n return this.#renderInProgress;\n }\n}\n\n/**\n * Default frame state for elements that don't need special handling.\n * Use this for simple elements that are always ready.\n */\nexport const DEFAULT_FRAME_STATE: FrameState = {\n needsPreparation: false,\n isReady: true,\n priority: PRIORITY_DEFAULT,\n};\n\n/**\n * Helper to create a FrameRenderable mixin for elements.\n * Provides default implementations that can be overridden.\n */\nexport function createFrameRenderableMixin<\n T extends { new (...args: any[]): HTMLElement },\n>(Base: T) {\n return class FrameRenderableMixin extends Base implements FrameRenderable {\n getFrameState(_timeMs: number): FrameState {\n return DEFAULT_FRAME_STATE;\n }\n\n async prepareFrame(_timeMs: number, _signal: AbortSignal): Promise<void> {\n // Default: no preparation needed\n }\n\n renderFrame(_timeMs: number): void {\n // Default: no explicit render needed\n }\n };\n}\n"],"mappings":";;;;;AA4BA,MAAa,iBAAiB;;;;;AAM9B,MAAa,oBAAoB;;;;;AAMjC,MAAa,iBAAiB;;;;;AAM9B,MAAa,oBAAoB;;;;;AAMjC,MAAa,iBAAiB;;;;;AAM9B,MAAa,mBAAmB;;;;AAiEhC,SAAgB,kBACd,SAC4B;AAC5B,QACE,OAAO,YAAY,YACnB,YAAY,QACZ,mBAAmB,WACnB,kBAAkB,WAClB,iBAAiB,WACjB,OAAQ,QAA4B,kBAAkB,cACtD,OAAQ,QAA4B,iBAAiB,cACrD,OAAQ,QAA4B,gBAAgB;;;;;;AAqCxD,IAAa,kBAAb,MAA6B;CAC3B;CACA,mBAA2C;CAC3C,oBAAoB;CACpB,qBAAoC;;;;;;CAMpC,sBAA8B;CAE9B,YAAY,aAAqD;AAC/D,QAAKA,cAAe;;;;;CAMtB,QAAc;AACZ,QAAKC,iBAAkB,OAAO;AAC9B,QAAKA,kBAAmB;AAExB,QAAKC,qBAAsB;;;;;;;;;;;;;;CAe7B,MAAM,YACJ,QACA,UAA8B,EAAE,EACG;EACnC,MAAM,EAAE,mBAAmB,MAAM,uBAAuB;AAKxD,MAAI,WAAW,MAAKA,mBAClB,QAAO;AAIT,MAAI,MAAKC,kBAAmB;AAC1B,SAAKC,oBAAqB;AAC1B,UAAO;;AAIT,QAAKH,iBAAkB,OAAO;AAC9B,QAAKA,kBAAmB,IAAI,iBAAiB;EAC7C,MAAM,SAAS,MAAKA,gBAAiB;AAErC,QAAKE,mBAAoB;AAEzB,MAAI;AACF,OAAI,kBAAkB;AACpB,UAAM,MAAKH,YAAa;AACxB,WAAO,gBAAgB;;GAGzB,MAAM,SAAS,YAAY,KAAK;GAChC,MAAM,WAAW,MAAKK,qBAAsB,OAAO;GACnD,MAAM,UAAU,YAAY,KAAK,GAAG;AACpC,UAAO,gBAAgB;GAEvB,MAAM,WAAW,YAAY,KAAK;GAClC,MAAM,6BAA6B,SAAS,QACzC,OAAO,GAAG,cAAc,OAAO,CAAC,iBAClC;AAED,OAAI,2BAA2B,SAAS,GAAG;AACzC,UAAM,QAAQ,IACZ,2BAA2B,KAAK,OAC9B,GAAG,aAAa,QAAQ,OAAO,CAChC,CACF;AACD,WAAO,gBAAgB;;GAEzB,MAAM,YAAY,YAAY,KAAK,GAAG;GAEtC,MAAM,UAAU,YAAY,KAAK;GACjC,MAAM,iBAAiB,CAAC,GAAG,SAAS,CAAC,MAClC,GAAG,MACF,EAAE,cAAc,OAAO,CAAC,WAAW,EAAE,cAAc,OAAO,CAAC,SAC9D;AAED,QAAK,MAAM,WAAW,gBAAgB;AACpC,WAAO,gBAAgB;AACvB,YAAQ,YAAY,OAAO;;GAE7B,MAAM,WAAW,YAAY,KAAK,GAAG;GAErC,MAAM,SAAS,YAAY,KAAK;AAChC,OAAI,mBACF,oBAAmB,MAAKL,YAAa;GAEvC,MAAM,UAAU,YAAY,KAAK,GAAG;AAEpC,SAAKE,qBAAsB;AAC3B,UAAO;IAAE;IAAS;IAAW;IAAU;IAAS;YACxC;AACR,SAAKC,mBAAoB;AAGzB,OAAI,MAAKC,sBAAuB,MAAM;IACpC,MAAM,cAAc,MAAKA;AACzB,UAAKA,oBAAqB;AAE1B,SAAK,YAAY,aAAa,QAAQ,CAAC,YAAY,GAEjD;;;;;;;;;;;;;;;;CAiBR,sBAAsB,QAAmC;EACvD,MAAME,SAA4B,EAAE;EACpC,MAAM,gBAAgB;EAEtB,MAAM,QAAQ,YAA2B;AAKvC,OAFmB,iBAAiB,WAAW,eAAe,SAE9C;IAGd,MAAM,UACH,QAAqC,eAAe;IACvD,MAAM,QAAS,QAAmC,aAAa;AAI/D,QAAI,EAFF,iBAAiB,WAAW,gBAAgB,OAI5C;AAIF,QAAI,kBAAkB,QAAQ,CAC5B,QAAO,KAAK,QAAQ;UAEjB;AAKL,QACE,mBAAmB,eACnB,QAAQ,MAAM,YAAY,OAE1B;AAGF,QAAI,kBAAkB,QAAQ,CAC5B,QAAO,KAAK,QAAQ;;GAKxB,MAAM,WAAW,MAAKC,4BAA6B,QAAQ;AAC3D,QAAK,MAAM,SAAS,SAClB,MAAK,MAAM;;AAIf,OAAK,MAAKP,YAAa;AACvB,SAAO;;;;;;;CAQT,6BAA6B,SAAqC;AAEhE,MAAI,QAAQ,YAAY;GACtB,MAAM,QAAQ,QAAQ,WAAW,iBAAiB,OAAO;AACzD,OAAI,MAAM,SAAS,GAAG;IACpB,MAAMQ,mBAA8B,EAAE;AACtC,SAAK,MAAM,QAAQ,MACjB,kBAAiB,KAAK,GAAG,KAAK,kBAAkB,CAAC;AAGnD,SAAK,MAAM,SAAS,QAAQ,WAAW,SACrC,KAAI,MAAM,YAAY,OACpB,kBAAiB,KAAK,MAAM;AAGhC,WAAO;;;AAKX,SAAO,QAAQ;;;;;CAMjB,IAAI,cAAuB;AACzB,SAAO,MAAKL"}
|
|
1
|
+
{"version":3,"file":"FrameController.js","names":["#rootElement","#abortController","#lastRenderedTimeMs","#renderInProgress","#pendingRenderTime","#queryVisibleElements","result: FrameRenderable[]","#getChildrenIncludingSlotted","assignedElements: Element[]"],"sources":["../../src/preview/FrameController.ts"],"sourcesContent":["/**\n * FrameController: Centralized frame rendering control\n *\n * Replaces the distributed Lit Task hierarchy with a single control loop\n * that queries elements and coordinates rendering directly.\n *\n * Benefits over the previous Task-based system:\n * - Single abort controller instead of distributed abort handling\n * - Clear prepare → render phases\n * - All coordination visible in one place\n * - No Lit Task reactivity overhead\n */\n\nimport type { LitElement } from \"lit\";\n\n// ============================================================================\n// Priority Constants\n// ============================================================================\n// Lower numbers render first. Elements with dependencies should have higher\n// priority numbers than their dependencies.\n//\n// Example: Waveform depends on audio analysis data, so it renders after audio.\n// ============================================================================\n\n/**\n * Priority for video elements.\n * Video renders first as other elements may depend on video frames being ready.\n */\nexport const PRIORITY_VIDEO = 1;\n\n/**\n * Priority for captions elements.\n * Captions render after video so they can overlay correctly.\n */\nexport const PRIORITY_CAPTIONS = 2;\n\n/**\n * Priority for audio elements.\n * Audio renders after captions (no visual dependency, but keeps consistent ordering).\n */\nexport const PRIORITY_AUDIO = 3;\n\n/**\n * Priority for waveform elements.\n * Waveform renders after audio because it depends on audio analysis data.\n */\nexport const PRIORITY_WAVEFORM = 4;\n\n/**\n * Priority for image elements.\n * Images render with low priority as they're typically static.\n */\nexport const PRIORITY_IMAGE = 5;\n\n/**\n * Default priority for elements that don't specify one.\n * High number ensures custom elements render after standard elements.\n */\nexport const PRIORITY_DEFAULT = 100;\n\n/**\n * State returned by elements describing their readiness for a given time.\n */\nexport interface FrameState {\n /**\n * Whether async preparation is needed before rendering.\n * Examples: video needs to seek, captions need to load data.\n */\n needsPreparation: boolean;\n\n /**\n * Whether the element is ready to render synchronously.\n * True when all async work is complete and renderFrame() can be called.\n */\n isReady: boolean;\n\n /**\n * Rendering priority hint. Lower numbers render first.\n * Used to order render calls for elements with dependencies.\n *\n * Standard priorities:\n * - PRIORITY_VIDEO (1): Video elements\n * - PRIORITY_CAPTIONS (2): Caption overlays\n * - PRIORITY_AUDIO (3): Audio elements\n * - PRIORITY_WAVEFORM (4): Audio visualizers (depend on audio)\n * - PRIORITY_IMAGE (5): Static images\n * - PRIORITY_DEFAULT (100): Fallback for custom elements\n */\n priority: number;\n}\n\n/**\n * Interface that elements implement to participate in centralized frame rendering.\n * Elements keep their rendering logic local but expose a standardized interface.\n */\nexport interface FrameRenderable {\n /**\n * Query the element's readiness state for a given time.\n * Must be synchronous and cheap to call.\n */\n getFrameState(timeMs: number): FrameState;\n\n /**\n * Async preparation phase. Called when getFrameState().needsPreparation is true.\n * Performs any async work needed before rendering (seeking, loading, etc.).\n *\n * @param timeMs - The time to prepare for\n * @param signal - Abort signal for cancellation\n */\n prepareFrame(timeMs: number, signal: AbortSignal): Promise<void>;\n\n /**\n * Synchronous render phase. Called after all preparation is complete.\n * Performs the actual rendering (paint to canvas, update DOM, etc.).\n *\n * @param timeMs - The time to render\n */\n renderFrame(timeMs: number): void;\n}\n\n/**\n * Type guard to check if an element implements FrameRenderable.\n */\nexport function isFrameRenderable(element: unknown): element is FrameRenderable {\n return (\n typeof element === \"object\" &&\n element !== null &&\n \"getFrameState\" in element &&\n \"prepareFrame\" in element &&\n \"renderFrame\" in element &&\n typeof (element as FrameRenderable).getFrameState === \"function\" &&\n typeof (element as FrameRenderable).prepareFrame === \"function\" &&\n typeof (element as FrameRenderable).renderFrame === \"function\"\n );\n}\n\n/**\n * Per-phase timing data returned by FrameController.renderFrame().\n * All values are in milliseconds.\n */\nexport interface RenderFrameTiming {\n queryMs: number;\n prepareMs: number;\n renderMs: number;\n animsMs: number;\n}\n\n/**\n * Options for FrameController.renderFrame()\n */\nexport interface RenderFrameOptions {\n /**\n * Whether to wait for Lit updateComplete before querying elements.\n * Default: true\n */\n waitForLitUpdate?: boolean;\n\n /**\n * Callback to update CSS animations after frame rendering completes.\n * Called with the root element after all elements have rendered.\n * This centralizes animation synchronization in one place.\n */\n onAnimationsUpdate?: (rootElement: Element) => void;\n}\n\n/**\n * Central controller for frame rendering.\n * Lives at the root timegroup and orchestrates all element rendering.\n */\nexport class FrameController {\n #rootElement: LitElement & { currentTimeMs: number };\n #abortController: AbortController | null = null;\n #renderInProgress = false;\n #pendingRenderTime: number | null = null;\n /**\n * Last successfully rendered time. Used for deduplication when multiple\n * callers (e.g., PlaybackController RAF loop and canvas render loop)\n * both try to render the same frame within one animation frame.\n */\n #lastRenderedTimeMs: number = -1;\n\n constructor(rootElement: LitElement & { currentTimeMs: number }) {\n this.#rootElement = rootElement;\n }\n\n /**\n * Cancel any in-progress render operation and reset deduplication state.\n */\n abort(): void {\n this.#abortController?.abort();\n this.#abortController = null;\n // Reset deduplication state so next render goes through even if same time\n this.#lastRenderedTimeMs = -1;\n }\n\n /**\n * Render a frame at the specified time.\n *\n * This is the main entry point for frame rendering. It:\n * 1. Cancels any previous in-progress render\n * 2. Queries all visible FrameRenderable elements\n * 3. Runs preparation in parallel for elements that need it\n * 4. Runs render in priority order\n *\n * @param timeMs - The time in milliseconds to render\n * @param options - Optional configuration\n */\n async renderFrame(\n timeMs: number,\n options: RenderFrameOptions = {},\n ): Promise<RenderFrameTiming | null> {\n const { waitForLitUpdate = true, onAnimationsUpdate } = options;\n\n // Deduplicate: skip if we just rendered this exact time.\n // This prevents double-rendering when multiple RAF loops (e.g., PlaybackController\n // and canvas render loop) both call renderFrame() for the same frame.\n if (timeMs === this.#lastRenderedTimeMs) {\n return null;\n }\n\n // If a render is in progress, queue this one\n if (this.#renderInProgress) {\n this.#pendingRenderTime = timeMs;\n return null;\n }\n\n // Cancel any previous render operation\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n const signal = this.#abortController.signal;\n\n this.#renderInProgress = true;\n\n try {\n if (waitForLitUpdate) {\n await this.#rootElement.updateComplete;\n signal.throwIfAborted();\n }\n\n const tQuery = performance.now();\n const elements = this.#queryVisibleElements(timeMs);\n const queryMs = performance.now() - tQuery;\n signal.throwIfAborted();\n\n const tPrepare = performance.now();\n const elementsNeedingPreparation = elements.filter(\n (el) => el.getFrameState(timeMs).needsPreparation,\n );\n\n if (elementsNeedingPreparation.length > 0) {\n await Promise.all(elementsNeedingPreparation.map((el) => el.prepareFrame(timeMs, signal)));\n signal.throwIfAborted();\n }\n const prepareMs = performance.now() - tPrepare;\n\n const tRender = performance.now();\n const sortedElements = [...elements].sort(\n (a, b) => a.getFrameState(timeMs).priority - b.getFrameState(timeMs).priority,\n );\n\n for (const element of sortedElements) {\n signal.throwIfAborted();\n element.renderFrame(timeMs);\n }\n const renderMs = performance.now() - tRender;\n\n const tAnims = performance.now();\n if (onAnimationsUpdate) {\n onAnimationsUpdate(this.#rootElement);\n }\n const animsMs = performance.now() - tAnims;\n\n this.#lastRenderedTimeMs = timeMs;\n return { queryMs, prepareMs, renderMs, animsMs };\n } finally {\n this.#renderInProgress = false;\n\n // Process any queued render\n if (this.#pendingRenderTime !== null) {\n const pendingTime = this.#pendingRenderTime;\n this.#pendingRenderTime = null;\n // Don't await - fire and forget to avoid recursive waiting\n this.renderFrame(pendingTime, options).catch(() => {\n // Silently ignore errors from queued renders (likely aborted)\n });\n }\n }\n }\n\n /**\n * Query all visible FrameRenderable elements in the tree.\n * Uses temporal visibility to filter out elements not visible at current time.\n *\n * IMPORTANT: For temporal elements, we use temporal visibility (startTimeMs/endTimeMs)\n * instead of CSS visibility. This is because updateAnimations sets display:none on\n * elements outside their time range, but that CSS state is from the PREVIOUS frame.\n * When seeking, we need to evaluate visibility based on the NEW time, not stale CSS.\n *\n * @param timeMs - The time to use for visibility checks. This should be the target\n * render time, not read from root element (which may be stale).\n */\n #queryVisibleElements(timeMs: number): FrameRenderable[] {\n const result: FrameRenderable[] = [];\n const currentTimeMs = timeMs;\n\n const walk = (element: Element): void => {\n // For temporal elements (ef-timegroup, ef-video, etc.), use temporal visibility\n // instead of CSS visibility. CSS display:none may be stale from previous frame.\n const isTemporal = \"startTimeMs\" in element && \"endTimeMs\" in element;\n\n if (isTemporal) {\n // Temporal element: check time-based visibility\n // Use exclusive end (< not <=) to avoid overlap at boundaries\n const startMs = (element as { startTimeMs?: number }).startTimeMs ?? -Infinity;\n const endMs = (element as { endTimeMs?: number }).endTimeMs ?? Infinity;\n const isTemporallyVisible = currentTimeMs >= startMs && currentTimeMs < endMs;\n\n if (!isTemporallyVisible) {\n // Skip this element AND its children (children's times are relative to parent)\n return;\n }\n\n // Element is temporally visible - include if it implements FrameRenderable\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n } else {\n // Non-temporal element: only check inline display style (fast path).\n // Skip getComputedStyle — it forces synchronous style recalc and is\n // unnecessary because FrameRenderable elements are always temporal.\n // We only walk non-temporal elements to reach temporal children.\n if (element instanceof HTMLElement && element.style.display === \"none\") {\n return;\n }\n\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n }\n\n // Walk children - handle both regular children and slotted content\n const children = this.#getChildrenIncludingSlotted(element);\n for (const child of children) {\n walk(child);\n }\n };\n\n walk(this.#rootElement);\n return result;\n }\n\n /**\n * Gets all child elements including slotted content for shadow DOM elements.\n * For elements with shadow DOM that contain slots, this returns the assigned\n * elements (slotted content) instead of just the shadow DOM children.\n */\n #getChildrenIncludingSlotted(element: Element): Iterable<Element> {\n // If element has shadowRoot with slots, get assigned elements\n if (element.shadowRoot) {\n const slots = element.shadowRoot.querySelectorAll(\"slot\");\n if (slots.length > 0) {\n const assignedElements: Element[] = [];\n for (const slot of slots) {\n assignedElements.push(...slot.assignedElements());\n }\n // Also include shadow DOM children that aren't slots (for mixed content)\n for (const child of element.shadowRoot.children) {\n if (child.tagName !== \"SLOT\") {\n assignedElements.push(child);\n }\n }\n return assignedElements;\n }\n }\n\n // Return HTMLCollection directly (iterable, no allocation)\n return element.children;\n }\n\n /**\n * Check if a render is currently in progress.\n */\n get isRendering(): boolean {\n return this.#renderInProgress;\n }\n}\n\n/**\n * Default frame state for elements that don't need special handling.\n * Use this for simple elements that are always ready.\n */\nexport const DEFAULT_FRAME_STATE: FrameState = {\n needsPreparation: false,\n isReady: true,\n priority: PRIORITY_DEFAULT,\n};\n\n/**\n * Helper to create a FrameRenderable mixin for elements.\n * Provides default implementations that can be overridden.\n */\nexport function createFrameRenderableMixin<T extends { new (...args: any[]): HTMLElement }>(\n Base: T,\n) {\n return class FrameRenderableMixin extends Base implements FrameRenderable {\n getFrameState(_timeMs: number): FrameState {\n return DEFAULT_FRAME_STATE;\n }\n\n async prepareFrame(_timeMs: number, _signal: AbortSignal): Promise<void> {\n // Default: no preparation needed\n }\n\n renderFrame(_timeMs: number): void {\n // Default: no explicit render needed\n }\n };\n}\n"],"mappings":";;;;;AA4BA,MAAa,iBAAiB;;;;;AAM9B,MAAa,oBAAoB;;;;;AAMjC,MAAa,iBAAiB;;;;;AAM9B,MAAa,oBAAoB;;;;;AAMjC,MAAa,iBAAiB;;;;;AAM9B,MAAa,mBAAmB;;;;AAiEhC,SAAgB,kBAAkB,SAA8C;AAC9E,QACE,OAAO,YAAY,YACnB,YAAY,QACZ,mBAAmB,WACnB,kBAAkB,WAClB,iBAAiB,WACjB,OAAQ,QAA4B,kBAAkB,cACtD,OAAQ,QAA4B,iBAAiB,cACrD,OAAQ,QAA4B,gBAAgB;;;;;;AAqCxD,IAAa,kBAAb,MAA6B;CAC3B;CACA,mBAA2C;CAC3C,oBAAoB;CACpB,qBAAoC;;;;;;CAMpC,sBAA8B;CAE9B,YAAY,aAAqD;AAC/D,QAAKA,cAAe;;;;;CAMtB,QAAc;AACZ,QAAKC,iBAAkB,OAAO;AAC9B,QAAKA,kBAAmB;AAExB,QAAKC,qBAAsB;;;;;;;;;;;;;;CAe7B,MAAM,YACJ,QACA,UAA8B,EAAE,EACG;EACnC,MAAM,EAAE,mBAAmB,MAAM,uBAAuB;AAKxD,MAAI,WAAW,MAAKA,mBAClB,QAAO;AAIT,MAAI,MAAKC,kBAAmB;AAC1B,SAAKC,oBAAqB;AAC1B,UAAO;;AAIT,QAAKH,iBAAkB,OAAO;AAC9B,QAAKA,kBAAmB,IAAI,iBAAiB;EAC7C,MAAM,SAAS,MAAKA,gBAAiB;AAErC,QAAKE,mBAAoB;AAEzB,MAAI;AACF,OAAI,kBAAkB;AACpB,UAAM,MAAKH,YAAa;AACxB,WAAO,gBAAgB;;GAGzB,MAAM,SAAS,YAAY,KAAK;GAChC,MAAM,WAAW,MAAKK,qBAAsB,OAAO;GACnD,MAAM,UAAU,YAAY,KAAK,GAAG;AACpC,UAAO,gBAAgB;GAEvB,MAAM,WAAW,YAAY,KAAK;GAClC,MAAM,6BAA6B,SAAS,QACzC,OAAO,GAAG,cAAc,OAAO,CAAC,iBAClC;AAED,OAAI,2BAA2B,SAAS,GAAG;AACzC,UAAM,QAAQ,IAAI,2BAA2B,KAAK,OAAO,GAAG,aAAa,QAAQ,OAAO,CAAC,CAAC;AAC1F,WAAO,gBAAgB;;GAEzB,MAAM,YAAY,YAAY,KAAK,GAAG;GAEtC,MAAM,UAAU,YAAY,KAAK;GACjC,MAAM,iBAAiB,CAAC,GAAG,SAAS,CAAC,MAClC,GAAG,MAAM,EAAE,cAAc,OAAO,CAAC,WAAW,EAAE,cAAc,OAAO,CAAC,SACtE;AAED,QAAK,MAAM,WAAW,gBAAgB;AACpC,WAAO,gBAAgB;AACvB,YAAQ,YAAY,OAAO;;GAE7B,MAAM,WAAW,YAAY,KAAK,GAAG;GAErC,MAAM,SAAS,YAAY,KAAK;AAChC,OAAI,mBACF,oBAAmB,MAAKL,YAAa;GAEvC,MAAM,UAAU,YAAY,KAAK,GAAG;AAEpC,SAAKE,qBAAsB;AAC3B,UAAO;IAAE;IAAS;IAAW;IAAU;IAAS;YACxC;AACR,SAAKC,mBAAoB;AAGzB,OAAI,MAAKC,sBAAuB,MAAM;IACpC,MAAM,cAAc,MAAKA;AACzB,UAAKA,oBAAqB;AAE1B,SAAK,YAAY,aAAa,QAAQ,CAAC,YAAY,GAEjD;;;;;;;;;;;;;;;;CAiBR,sBAAsB,QAAmC;EACvD,MAAME,SAA4B,EAAE;EACpC,MAAM,gBAAgB;EAEtB,MAAM,QAAQ,YAA2B;AAKvC,OAFmB,iBAAiB,WAAW,eAAe,SAE9C;IAGd,MAAM,UAAW,QAAqC,eAAe;IACrE,MAAM,QAAS,QAAmC,aAAa;AAG/D,QAAI,EAFwB,iBAAiB,WAAW,gBAAgB,OAItE;AAIF,QAAI,kBAAkB,QAAQ,CAC5B,QAAO,KAAK,QAAQ;UAEjB;AAKL,QAAI,mBAAmB,eAAe,QAAQ,MAAM,YAAY,OAC9D;AAGF,QAAI,kBAAkB,QAAQ,CAC5B,QAAO,KAAK,QAAQ;;GAKxB,MAAM,WAAW,MAAKC,4BAA6B,QAAQ;AAC3D,QAAK,MAAM,SAAS,SAClB,MAAK,MAAM;;AAIf,OAAK,MAAKP,YAAa;AACvB,SAAO;;;;;;;CAQT,6BAA6B,SAAqC;AAEhE,MAAI,QAAQ,YAAY;GACtB,MAAM,QAAQ,QAAQ,WAAW,iBAAiB,OAAO;AACzD,OAAI,MAAM,SAAS,GAAG;IACpB,MAAMQ,mBAA8B,EAAE;AACtC,SAAK,MAAM,QAAQ,MACjB,kBAAiB,KAAK,GAAG,KAAK,kBAAkB,CAAC;AAGnD,SAAK,MAAM,SAAS,QAAQ,WAAW,SACrC,KAAI,MAAM,YAAY,OACpB,kBAAiB,KAAK,MAAM;AAGhC,WAAO;;;AAKX,SAAO,QAAQ;;;;;CAMjB,IAAI,cAAuB;AACzB,SAAO,MAAKL"}
|