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 sidebarDraggingLayerIdRef = useRef<string | null>(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"
>
<Paintbrush className="h-4 w-4" />
@ -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"
>
<Wand2 className="h-4 w-4" />