diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index eeb8a061..f951594b 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -85,6 +85,19 @@ function getRegisteredFramesSignature() { .join("|"); } +function serializeExtensionSettingValue(value: unknown): string { + try { + const serialized = JSON.stringify(value); + return serialized ?? "undefined"; + } catch { + try { + return String(value); + } catch { + return "[unserializable]"; + } + } +} + function getExtensionSettingsSignature() { return extensionHost .getSettingsPanels() @@ -92,7 +105,7 @@ function getExtensionSettingsSignature() { const { extensionId, panel } = registeredPanel; return panel.fields.map((field) => { const value = extensionHost.getExtensionSetting(extensionId, field.id); - return `${extensionId}:${panel.id}:${field.id}:${JSON.stringify(value)}`; + return `${extensionId}:${panel.id}:${field.id}:${serializeExtensionSettingValue(value)}`; }); }) .sort() @@ -511,6 +524,7 @@ const VideoPlayback = forwardRef( const frameSpriteRef = useRef(null); const frameContainerRef = useRef(null); const frameIdRef = useRef(frame); + const frameReloadKeyRef = useRef(null); const isPlayingRef = useRef(isPlaying); const suspendRenderingRef = useRef(suspendRendering); const isSeekingRef = useRef(false); @@ -1042,6 +1056,18 @@ const VideoPlayback = forwardRef( useEffect(() => { const frameContainer = frameContainerRef.current; if (!frameContainer) return; + const nextFrameReloadKey = `${frame ?? ""}:${frameUpdateCounter}`; + const activeFrameData = frame + ? extensionHost.getFrames().find((registeredFrame) => registeredFrame.id === frame) + : null; + const shouldRedrawDynamicFrame = Boolean(activeFrameData?.draw && frameSpriteRef.current); + + // Layout-only changes should not force texture/sprite recreation. + if (frameReloadKeyRef.current === nextFrameReloadKey && !shouldRedrawDynamicFrame) { + layoutVideoContentRef.current?.(); + return; + } + frameReloadKeyRef.current = nextFrameReloadKey; // Clear existing frame sprite and its texture to free memory if (frameSpriteRef.current) { @@ -1116,6 +1142,11 @@ const VideoPlayback = forwardRef( }; }, [aspectRatio, borderRadius, cropRegion, frame, frameUpdateCounter, padding]); + // Always re-run geometric layout when layout props change, even if frame sprite isn't reloaded. + useEffect(() => { + layoutVideoContentRef.current?.(); + }, [aspectRatio, borderRadius, cropRegion, padding]); + const selectedZoom = useMemo(() => { if (!selectedZoomId) return null; return zoomRegions.find((region) => region.id === selectedZoomId) ?? null; diff --git a/src/lib/extensions/iconDraw.ts b/src/lib/extensions/iconDraw.ts index 4634db74..c948f4ad 100644 --- a/src/lib/extensions/iconDraw.ts +++ b/src/lib/extensions/iconDraw.ts @@ -1,6 +1,7 @@ import * as PhosphorIcons from "@phosphor-icons/react"; type IconWeight = "thin" | "light" | "regular" | "bold" | "fill"; +const missingIconPathCache = new Set(); function resolveIconComponent(name: string): { iconName: string; @@ -49,6 +50,9 @@ export function resolveIconPath( cache: Map, ): Path2D | null { const cacheKey = `${name}:${weight}`; + if (missingIconPathCache.has(cacheKey)) { + return null; + } const cached = cache.get(cacheKey); if (cached) { return cached; @@ -57,6 +61,7 @@ export function resolveIconPath( const resolved = resolveIconComponent(name); if (!resolved) { console.warn(`[extensions] Icon ${name} not found in Phosphor library`); + missingIconPathCache.add(cacheKey); return null; } @@ -70,14 +75,15 @@ export function resolveIconPath( const pathDataList: string[] = []; collectPathData(children, pathDataList); - if (pathDataList.length === 0) { - console.warn(`[extensions] No path data found for ${name}:${weight}`, { - iconName: resolved.iconName, - element, - children, - }); - return null; - } + if (pathDataList.length === 0) { + console.warn(`[extensions] No path data found for ${name}:${weight}`, { + iconName: resolved.iconName, + element, + children, + }); + missingIconPathCache.add(cacheKey); + return null; + } const combinedPath = new Path2D(); for (const pathData of pathDataList) { @@ -85,8 +91,9 @@ export function resolveIconPath( } cache.set(cacheKey, combinedPath); return combinedPath; - } catch (err) { - console.error(`[extensions] Failed to extract path for icon ${name}:`, err); - return null; - } + } catch (err) { + console.error(`[extensions] Failed to extract path for icon ${name}:`, err); + missingIconPathCache.add(cacheKey); + return null; + } }