From 5ca33daa754395fe4bfee152d6454b048438b1b2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 8 Mar 2026 02:42:24 +0800 Subject: [PATCH] polish realtime canvas editing interactions --- components/canvas/RealtimeCanvasDialog.tsx | 112 +++++++++++++-------- 1 file changed, 71 insertions(+), 41 deletions(-) diff --git a/components/canvas/RealtimeCanvasDialog.tsx b/components/canvas/RealtimeCanvasDialog.tsx index bbff15a60..d0f308ede 100644 --- a/components/canvas/RealtimeCanvasDialog.tsx +++ b/components/canvas/RealtimeCanvasDialog.tsx @@ -35,7 +35,6 @@ import { Upload, Sparkles, Save, - Trash2, Undo2, Redo2, Send, @@ -686,6 +685,7 @@ export function RealtimeCanvasDialog({ const imageLayersRef = useRef([]); const colorInputRef = useRef(null); const layerObjectUrlsRef = useRef>(new Set()); + const sidebarDraggingLayerIdRef = useRef(null); const drawingHasContentRef = useRef(false); const dragDepthRef = useRef(0); @@ -702,7 +702,7 @@ export function RealtimeCanvasDialog({ const [, setStatusMessage] = useState("等待连接"); const [prompt, setPrompt] = useState(FALLBACK_PROMPT); const [previewUrl, setPreviewUrl] = useState(""); - const [brushMode, setBrushMode] = useState("brush"); + const [brushMode, setBrushMode] = useState("select"); const [brushColor, setBrushColor] = useState(DEFAULT_BRUSH_COLOR); const [canvasBackground, setCanvasBackground] = useState(canvasBackgroundRef.current); const [imageLayers, setImageLayers] = useState([]); @@ -850,28 +850,35 @@ export function RealtimeCanvasDialog({ if (!ctx) return; const renderSeq = layerRenderSeqRef.current + 1; layerRenderSeqRef.current = renderSeq; - await drawImageLayersToContext(ctx, layerCanvas.width, layerCanvas.height, { renderSeq }); + const cssWidth = Math.max(1, parseFloat(layerCanvas.style.width) || layerCanvas.width); + const cssHeight = Math.max(1, parseFloat(layerCanvas.style.height) || layerCanvas.height); + await drawImageLayersToContext(ctx, cssWidth, cssHeight, { renderSeq }); }, [drawImageLayersToContext]); const buildCompositeCanvas = useCallback(async () => { const drawingCanvas = canvasRef.current; if (!drawingCanvas) return null; + const cssWidth = Math.max(1, parseFloat(drawingCanvas.style.width) || drawingCanvas.width); + const cssHeight = Math.max(1, parseFloat(drawingCanvas.style.height) || drawingCanvas.height); + const dpr = Math.max(1, drawingCanvas.width / cssWidth); const compositeCanvas = document.createElement("canvas"); compositeCanvas.width = drawingCanvas.width; compositeCanvas.height = drawingCanvas.height; const compositeCtx = compositeCanvas.getContext("2d"); if (!compositeCtx) return null; + compositeCtx.setTransform(1, 0, 0, 1, 0, 0); + compositeCtx.scale(dpr, dpr); const didRenderLayers = await drawImageLayersToContext( compositeCtx, - compositeCanvas.width, - compositeCanvas.height + cssWidth, + cssHeight ); if (!didRenderLayers) { return null; } - compositeCtx.drawImage(drawingCanvas, 0, 0); + compositeCtx.drawImage(drawingCanvas, 0, 0, cssWidth, cssHeight); return compositeCanvas; }, [drawImageLayersToContext]); @@ -1374,6 +1381,30 @@ export function RealtimeCanvasDialog({ [adaptiveThrottleMs, markCanvasDirty, pumpFlush] ); + const reorderLayersFromSidebar = useCallback( + (draggedLayerId: string, targetLayerId: string) => { + if (!draggedLayerId || !targetLayerId || draggedLayerId === targetLayerId) return; + + const displayLayers = [...imageLayersRef.current].reverse(); + const sourceIndex = displayLayers.findIndex((layer) => layer.id === draggedLayerId); + const targetIndex = displayLayers.findIndex((layer) => layer.id === targetLayerId); + if (sourceIndex === -1 || targetIndex === -1 || sourceIndex === targetIndex) return; + + const nextDisplayLayers = [...displayLayers]; + const [movedLayer] = nextDisplayLayers.splice(sourceIndex, 1); + nextDisplayLayers.splice(targetIndex, 0, movedLayer); + const nextLayers = nextDisplayLayers.reverse(); + + imageLayersRef.current = nextLayers; + setImageLayers(nextLayers); + setSelectedLayerId(draggedLayerId); + saveHistory(nextLayers); + markCanvasDirty(null); + scheduleFlush(true, false, "refine"); + }, + [markCanvasDirty, saveHistory, scheduleFlush] + ); + const importFilesAsLayers = useCallback( async (files: File[]) => { if (files.length === 0) return 0; @@ -1780,7 +1811,7 @@ export function RealtimeCanvasDialog({ if (!point) return; pointerDownRef.current = true; lastPointRef.current = point; - if (selectedImageLayer) { + if (brushMode === "select" && selectedImageLayer) { const resizeHandle = getResizeHandleAtPoint(point, selectedImageLayer); if (resizeHandle) { resizingLayerIdRef.current = selectedImageLayer.id; @@ -1814,7 +1845,7 @@ export function RealtimeCanvasDialog({ return; } } - const hitLayer = findLayerAtPoint(point); + const hitLayer = brushMode === "select" ? findLayerAtPoint(point) : null; if (hitLayer) { setSelectedLayerId(hitLayer.id); draggingLayerIdRef.current = hitLayer.id; @@ -1883,9 +1914,9 @@ export function RealtimeCanvasDialog({ const handlePointerEnter = useCallback(() => { if (cursorRef.current) { cursorRef.current.style.opacity = - !selectedImageLayer && (brushMode === "brush" || brushMode === "eraser") ? "1" : "0"; + brushMode === "brush" || brushMode === "eraser" ? "1" : "0"; } - }, [brushMode, selectedImageLayer]); + }, [brushMode]); const handlePointerMove = useCallback( (event: ReactPointerEvent) => { @@ -1893,18 +1924,18 @@ export function RealtimeCanvasDialog({ // Update cursor const point = getCanvasPoint(event); if (point && cursorRef.current) { - const resizeHandle = getResizeHandleAtPoint(point, selectedImageLayer); + const resizeHandle = + brushMode === "select" ? getResizeHandleAtPoint(point, selectedImageLayer) : null; event.currentTarget.style.cursor = resizeHandle ? getResizeCursor(resizeHandle) - : selectedImageLayer + : brushMode === "select" && selectedImageLayer ? "move" : brushMode === "brush" || brushMode === "eraser" ? "none" : brushMode === "select" ? "default" : "crosshair"; - const showCursor = - !selectedImageLayer && (brushMode === "brush" || brushMode === "eraser"); + const showCursor = brushMode === "brush" || brushMode === "eraser"; if (showCursor) { cursorRef.current.style.opacity = "1"; cursorRef.current.style.transform = `translate(${point.x - brushSize / 2}px, ${point.y - brushSize / 2}px)`; @@ -2737,7 +2768,7 @@ export function RealtimeCanvasDialog({ ref={canvasRef} className={cn( "absolute inset-0 touch-none shadow-sm block", - selectedImageLayer + brushMode === "select" && selectedImageLayer ? "cursor-move" : brushMode === "brush" || brushMode === "eraser" ? "cursor-none" @@ -2756,7 +2787,7 @@ export function RealtimeCanvasDialog({ ref={previewCanvasRef} className="pointer-events-none absolute inset-0 touch-none" /> - {selectedImageLayer ? ( + {selectedImageLayer && brushMode === "select" ? (
) : null} - {selectedImageLayer + {selectedImageLayer && brushMode === "select" ? ([ { key: "nw", left: selectedImageLayer.x, top: selectedImageLayer.y }, { @@ -2796,26 +2827,6 @@ export function RealtimeCanvasDialog({ /> )) : null} - {selectedImageLayer ? ( -
- -
- ) : null} {/* Custom Brush Cursor */}
setSelectedLayerId(layer.id)} + onDragStart={() => { + sidebarDraggingLayerIdRef.current = layer.id; + }} + onDragEnd={() => { + sidebarDraggingLayerIdRef.current = null; + }} + onDragOver={(event) => { + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + }} + onDrop={(event) => { + event.preventDefault(); + const draggingId = sidebarDraggingLayerIdRef.current; + if (draggingId) { + reorderLayersFromSidebar(draggingId, layer.id); + } + sidebarDraggingLayerIdRef.current = null; + }} title={layer.name} > {layer.name} ))