diff --git a/packages/studio/src/components/editor/propertyPanelColorScopes.tsx b/packages/studio/src/components/editor/propertyPanelColorScopes.tsx index a1ef9df45..7d2298a57 100644 --- a/packages/studio/src/components/editor/propertyPanelColorScopes.tsx +++ b/packages/studio/src/components/editor/propertyPanelColorScopes.tsx @@ -189,7 +189,14 @@ export function PropertyPanelColorScopes({ }); useEffect(() => { - if (analysis && canvasRef.current) drawScope(canvasRef.current, mode, analysis); + const canvas = canvasRef.current; + if (!canvas) return; + if (analysis) { + drawScope(canvas, mode, analysis); + return; + } + const context = canvas.getContext("2d"); + if (context) drawGrid(context, canvas.width, canvas.height); }, [analysis, mode]); return ( diff --git a/packages/studio/src/components/editor/useColorGradingScopes.test.tsx b/packages/studio/src/components/editor/useColorGradingScopes.test.tsx index e0cce935a..61395ad22 100644 --- a/packages/studio/src/components/editor/useColorGradingScopes.test.tsx +++ b/packages/studio/src/components/editor/useColorGradingScopes.test.tsx @@ -27,6 +27,7 @@ function Harness({ return ( <> {scopes.status} + {scopes.analysis ? "ready" : "empty"} @@ -104,4 +105,39 @@ describe("useColorGradingScopes", () => { expect(host.querySelector("[data-status]")?.textContent).toBe("unavailable"); act(() => root.unmount()); }); + + it("clears stale analysis while a refresh is pending and when it is unavailable", async () => { + vi.useFakeTimers(); + installPixelDecode(); + let resolveRefresh: (frame: ColorGradingCapturedFrame | null) => void = () => {}; + const pendingRefresh = new Promise((resolve) => { + resolveRefresh = resolve; + }); + const captureFrame = vi + .fn() + .mockResolvedValueOnce({ + dataUrl: "data:image/png;base64,scope", + width: 1, + height: 1, + }) + .mockReturnValueOnce(pendingRefresh); + const host = document.body.appendChild(document.createElement("div")); + const root = createRoot(host); + act(() => root.render()); + + await act(async () => vi.advanceTimersByTimeAsync(180)); + expect(host.querySelector("[data-analysis]")?.textContent).toBe("ready"); + + act(() => { + host.querySelector("button")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + act(() => vi.advanceTimersByTime(180)); + expect(host.querySelector("[data-status]")?.textContent).toBe("loading"); + expect(host.querySelector("[data-analysis]")?.textContent).toBe("empty"); + + await act(async () => resolveRefresh(null)); + expect(host.querySelector("[data-status]")?.textContent).toBe("unavailable"); + expect(host.querySelector("[data-analysis]")?.textContent).toBe("empty"); + act(() => root.unmount()); + }); }); diff --git a/packages/studio/src/components/editor/useColorGradingScopes.ts b/packages/studio/src/components/editor/useColorGradingScopes.ts index adf912908..dad6cd154 100644 --- a/packages/studio/src/components/editor/useColorGradingScopes.ts +++ b/packages/studio/src/components/editor/useColorGradingScopes.ts @@ -26,11 +26,13 @@ export function useColorGradingScopes({ useEffect(() => { if (!open) { + setAnalysis(null); setStatus("idle"); return; } let cancelled = false; const timer = window.setTimeout(async () => { + setAnalysis(null); setStatus("loading"); try { const frame = await captureRef.current();