fix: resync realtime canvas after mode switch
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-11 20:16:59 +08:00
parent 8757f76b1f
commit 36f7eea13b

View File

@ -804,6 +804,7 @@ export function RealtimeCanvasDialog({
const layerObjectUrlsRef = useRef<Set<string>>(new Set()); const layerObjectUrlsRef = useRef<Set<string>>(new Set());
const sidebarDraggingLayerIdRef = useRef<string | null>(null); const sidebarDraggingLayerIdRef = useRef<string | null>(null);
const drawingHasContentRef = useRef(false); const drawingHasContentRef = useRef(false);
const drawingLayerSnapshotRef = useRef("");
const dragDepthRef = useRef(0); const dragDepthRef = useRef(0);
// History Management // History Management
@ -876,9 +877,9 @@ export function RealtimeCanvasDialog({
); );
const enhanceSourceImageUrl = useMemo(() => { const enhanceSourceImageUrl = useMemo(() => {
return ( return (
previewUrl ||
selectedImageLayer?.src || selectedImageLayer?.src ||
imageLayers[0]?.src || imageLayers[0]?.src ||
previewUrl ||
initialImageUrl || initialImageUrl ||
"" ""
); );
@ -1640,6 +1641,19 @@ export function RealtimeCanvasDialog({
[adaptiveThrottleMs, markCanvasDirty, pumpFlush] [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( const reorderLayersFromSidebar = useCallback(
(draggedLayerId: string, targetLayerId: string) => { (draggedLayerId: string, targetLayerId: string) => {
if (!draggedLayerId || !targetLayerId || draggedLayerId === targetLayerId) if (!draggedLayerId || !targetLayerId || draggedLayerId === targetLayerId)
@ -1796,16 +1810,83 @@ export function RealtimeCanvasDialog({
const canvas = canvasRef.current; const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d"); const ctx = canvas?.getContext("2d");
if (!canvas || !ctx) return; if (!canvas || !ctx) {
return;
}
clearDrawingCanvas(); clearDrawingCanvas();
const image = await loadRenderableImage(dataUrl); 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.drawImage(image, 0, 0, canvas.width, canvas.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
drawingHasContentRef.current = true; drawingHasContentRef.current = true;
}, },
[clearDrawingCanvas, loadRenderableImage] [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( const openSavedProject = useCallback(
async (projectId: string) => { async (projectId: string) => {
const project = readRealtimeProjectsFromStorage().find( const project = readRealtimeProjectsFromStorage().find(
@ -1956,6 +2037,23 @@ export function RealtimeCanvasDialog({
previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height); 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(() => { const undo = useCallback(() => {
if (historyIndexRef.current <= 0) return; if (historyIndexRef.current <= 0) return;
@ -2557,6 +2655,36 @@ export function RealtimeCanvasDialog({
}; };
}, [drawInitialImage, initializeCanvas, open]); }, [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(() => { useEffect(() => {
if (!open) return; if (!open) return;
@ -2872,6 +3000,28 @@ export function RealtimeCanvasDialog({
setEraserPopoverOpen(false); 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(() => { const activateSelectMode = useCallback(() => {
setSelectedLayerId(DRAW_LAYER_ID); setSelectedLayerId(DRAW_LAYER_ID);
setBrushMode("select"); setBrushMode("select");
@ -3008,7 +3158,7 @@ export function RealtimeCanvasDialog({
? softActionButtonClass ? softActionButtonClass
: "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111]" : "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111]"
)} )}
onClick={() => setWorkspaceMode("realtime")} onClick={() => handleWorkspaceModeChange("realtime")}
type="button" type="button"
> >
<Paintbrush className="h-4 w-4" /> <Paintbrush className="h-4 w-4" />
@ -3021,7 +3171,7 @@ export function RealtimeCanvasDialog({
? softActionButtonClass ? softActionButtonClass
: "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111]" : "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111]"
)} )}
onClick={() => setWorkspaceMode("enhance-pro")} onClick={() => handleWorkspaceModeChange("enhance-pro")}
type="button" type="button"
> >
<Wand2 className="h-4 w-4" /> <Wand2 className="h-4 w-4" />