diff --git a/components/canvas/RealtimeCanvasDialog.tsx b/components/canvas/RealtimeCanvasDialog.tsx index ac3d63c28..1febf064e 100644 --- a/components/canvas/RealtimeCanvasDialog.tsx +++ b/components/canvas/RealtimeCanvasDialog.tsx @@ -804,6 +804,7 @@ export function RealtimeCanvasDialog({ const layerObjectUrlsRef = useRef>(new Set()); const sidebarDraggingLayerIdRef = useRef(null); const drawingHasContentRef = useRef(false); + const drawingLayerSnapshotRef = useRef(""); const dragDepthRef = useRef(0); // History Management @@ -876,9 +877,9 @@ export function RealtimeCanvasDialog({ ); const enhanceSourceImageUrl = useMemo(() => { return ( - previewUrl || selectedImageLayer?.src || imageLayers[0]?.src || + previewUrl || initialImageUrl || "" ); @@ -1640,6 +1641,19 @@ export function RealtimeCanvasDialog({ [adaptiveThrottleMs, markCanvasDirty, pumpFlush] ); + const cancelPendingRealtimeFlush = useCallback((clearDirty = false) => { + if (pendingFlushTimerRef.current) { + window.clearTimeout(pendingFlushTimerRef.current); + pendingFlushTimerRef.current = null; + } + pendingFlushModeRef.current = null; + pendingFlushForceRef.current = false; + dirtyRegionRef.current = null; + if (clearDirty) { + dirtyRef.current = false; + } + }, []); + const reorderLayersFromSidebar = useCallback( (draggedLayerId: string, targetLayerId: string) => { if (!draggedLayerId || !targetLayerId || draggedLayerId === targetLayerId) @@ -1796,16 +1810,83 @@ export function RealtimeCanvasDialog({ const canvas = canvasRef.current; const ctx = canvas?.getContext("2d"); - if (!canvas || !ctx) return; + if (!canvas || !ctx) { + return; + } clearDrawingCanvas(); const image = await loadRenderableImage(dataUrl); + const cssWidth = Math.max( + 1, + Number.parseFloat(canvas.style.width) || canvas.width + ); + const dpr = Math.max(1, canvas.width / cssWidth); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(image, 0, 0, canvas.width, canvas.height); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.scale(dpr, dpr); drawingHasContentRef.current = true; }, [clearDrawingCanvas, loadRenderableImage] ); + const captureDrawingLayerSnapshot = useCallback(() => { + if (!canvasRef.current) { + return; + } + drawingLayerSnapshotRef.current = drawingCanvasHasContent(true) + ? exportDrawingLayerDataUrl() + : ""; + }, [drawingCanvasHasContent, exportDrawingLayerDataUrl]); + + const syncRealtimeCanvasAfterRemount = useCallback(async () => { + const canvas = canvasRef.current; + const imageLayerCanvas = imageLayerCanvasRef.current; + const previewCanvas = previewCanvasRef.current; + const wrap = canvasWrapRef.current; + if (!canvas || !imageLayerCanvas || !previewCanvas || !wrap) { + return false; + } + + const rect = wrap.getBoundingClientRect(); + const cssWidth = Math.max(1, Math.floor(rect.width)); + const cssHeight = Math.max(1, Math.floor(rect.height)); + if (cssWidth <= 1 || cssHeight <= 1) { + return false; + } + + const dpr = Math.max(1, window.devicePixelRatio || 1); + const pixelWidth = Math.max(1, Math.floor(cssWidth * dpr)); + const pixelHeight = Math.max(1, Math.floor(cssHeight * dpr)); + const configureCanvas = (targetCanvas: HTMLCanvasElement) => { + targetCanvas.width = pixelWidth; + targetCanvas.height = pixelHeight; + targetCanvas.style.width = `${cssWidth}px`; + targetCanvas.style.height = `${cssHeight}px`; + const ctx = targetCanvas.getContext("2d"); + if (!ctx) { + return; + } + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.scale(dpr, dpr); + ctx.clearRect(0, 0, cssWidth, cssHeight); + }; + + const drawingSnapshot = drawingLayerSnapshotRef.current; + configureCanvas(imageLayerCanvas); + configureCanvas(canvas); + configureCanvas(previewCanvas); + + if (drawingSnapshot) { + await loadDrawingLayerFromDataUrl(drawingSnapshot); + } else { + drawingHasContentRef.current = false; + } + await renderImageLayers(); + return true; + }, [loadDrawingLayerFromDataUrl, renderImageLayers]); + const openSavedProject = useCallback( async (projectId: string) => { const project = readRealtimeProjectsFromStorage().find( @@ -1956,6 +2037,23 @@ export function RealtimeCanvasDialog({ previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height); }, []); + const resetRealtimeInteractionState = useCallback(() => { + pointerDownRef.current = false; + draggingLayerIdRef.current = null; + resizingLayerIdRef.current = null; + layerDragOriginRef.current = null; + layerResizeOriginRef.current = null; + lastPointRef.current = null; + shapeStartPointRef.current = null; + sidebarDraggingLayerIdRef.current = null; + dragDepthRef.current = 0; + setIsDragImportActive(false); + clearPreviewCanvas(); + if (cursorRef.current) { + cursorRef.current.style.opacity = "0"; + } + }, [clearPreviewCanvas]); + const undo = useCallback(() => { if (historyIndexRef.current <= 0) return; @@ -2557,6 +2655,36 @@ export function RealtimeCanvasDialog({ }; }, [drawInitialImage, initializeCanvas, open]); + useEffect(() => { + if (!open) { + return; + } + if (workspaceMode !== "realtime") { + cancelPendingRealtimeFlush(true); + resetRealtimeInteractionState(); + return; + } + + let frameId = 0; + let followUpFrameId = 0; + frameId = window.requestAnimationFrame(() => { + followUpFrameId = window.requestAnimationFrame(() => { + void syncRealtimeCanvasAfterRemount(); + }); + }); + + return () => { + window.cancelAnimationFrame(frameId); + window.cancelAnimationFrame(followUpFrameId); + }; + }, [ + cancelPendingRealtimeFlush, + open, + resetRealtimeInteractionState, + syncRealtimeCanvasAfterRemount, + workspaceMode, + ]); + useEffect(() => { if (!open) return; @@ -2872,6 +3000,28 @@ export function RealtimeCanvasDialog({ setEraserPopoverOpen(false); }, []); + const handleWorkspaceModeChange = useCallback( + (nextMode: RealtimeWorkspaceMode) => { + if (nextMode === workspaceMode) { + return; + } + if (workspaceMode === "realtime") { + captureDrawingLayerSnapshot(); + } + closeBrushSettingPopovers(); + cancelPendingRealtimeFlush(true); + resetRealtimeInteractionState(); + setWorkspaceMode(nextMode); + }, + [ + cancelPendingRealtimeFlush, + captureDrawingLayerSnapshot, + closeBrushSettingPopovers, + resetRealtimeInteractionState, + workspaceMode, + ] + ); + const activateSelectMode = useCallback(() => { setSelectedLayerId(DRAW_LAYER_ID); setBrushMode("select"); @@ -3008,7 +3158,7 @@ export function RealtimeCanvasDialog({ ? softActionButtonClass : "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111]" )} - onClick={() => setWorkspaceMode("realtime")} + onClick={() => handleWorkspaceModeChange("realtime")} type="button" > @@ -3021,7 +3171,7 @@ export function RealtimeCanvasDialog({ ? softActionButtonClass : "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111]" )} - onClick={() => setWorkspaceMode("enhance-pro")} + onClick={() => handleWorkspaceModeChange("enhance-pro")} type="button" >