mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: wire crop, selection, and transform tool overlays into canvas
- CropOverlay: rendered in its own Layer when crop tool is active, cropState auto-initialized when switching to crop tool - SelectionOverlay: marching ants rendered for active selections, selection tool mouse events dispatched through canvas handler - TransformToolTransformer: rendered when transform tool is active - Selection tool handlers wired for marquee-rect, marquee-ellipse, lasso-free, lasso-poly, and magic-wand tools - Active selection preview rect shown while dragging
This commit is contained in:
@@ -26,12 +26,15 @@ import { ContextMenu, useContextMenu } from "./common/context-menu";
|
|||||||
import { BrushCursorOverlay, useEditorCursor } from "./common/custom-cursor";
|
import { BrushCursorOverlay, useEditorCursor } from "./common/custom-cursor";
|
||||||
import { LoadingOverlay } from "./common/loading-overlay";
|
import { LoadingOverlay } from "./common/loading-overlay";
|
||||||
import { useBrushTool } from "./tools/brush-tool";
|
import { useBrushTool } from "./tools/brush-tool";
|
||||||
|
import { CropOverlay } from "./tools/crop-tool";
|
||||||
import { useEraserTool } from "./tools/eraser-tool";
|
import { useEraserTool } from "./tools/eraser-tool";
|
||||||
import { useFillTool } from "./tools/fill-tool";
|
import { useFillTool } from "./tools/fill-tool";
|
||||||
import { useGradientTool } from "./tools/gradient-tool";
|
import { useGradientTool } from "./tools/gradient-tool";
|
||||||
import { MoveToolTransformer, useMoveTool } from "./tools/move-tool";
|
import { MoveToolTransformer, useMoveTool } from "./tools/move-tool";
|
||||||
|
import { SelectionOverlay, useSelectionTool } from "./tools/selection-tool";
|
||||||
import { useShapeTool } from "./tools/shape-tool";
|
import { useShapeTool } from "./tools/shape-tool";
|
||||||
import { useTextTool } from "./tools/text-tool";
|
import { useTextTool } from "./tools/text-tool";
|
||||||
|
import { TransformToolTransformer, useTransformTool } from "./tools/transform-tool";
|
||||||
|
|
||||||
// Module-level stage ref for export dialog access (Issue #6)
|
// Module-level stage ref for export dialog access (Issue #6)
|
||||||
export const editorStageRefHolder: { current: Konva.Stage | null } = {
|
export const editorStageRefHolder: { current: Konva.Stage | null } = {
|
||||||
@@ -413,6 +416,8 @@ function CanvasObjectRenderer({
|
|||||||
|
|
||||||
function useActiveToolHandlers(stageRef: React.RefObject<Konva.Stage | null>) {
|
function useActiveToolHandlers(stageRef: React.RefObject<Konva.Stage | null>) {
|
||||||
const activeTool = useEditorStore((s) => s.activeTool);
|
const activeTool = useEditorStore((s) => s.activeTool);
|
||||||
|
const zoom = useEditorStore((s) => s.zoom);
|
||||||
|
const panOffset = useEditorStore((s) => s.panOffset);
|
||||||
|
|
||||||
const brushTool = useBrushTool();
|
const brushTool = useBrushTool();
|
||||||
const eraserTool = useEraserTool();
|
const eraserTool = useEraserTool();
|
||||||
@@ -421,6 +426,30 @@ function useActiveToolHandlers(stageRef: React.RefObject<Konva.Stage | null>) {
|
|||||||
const fillTool = useFillTool(stageRef);
|
const fillTool = useFillTool(stageRef);
|
||||||
const gradientTool = useGradientTool();
|
const gradientTool = useGradientTool();
|
||||||
const moveTool = useMoveTool();
|
const moveTool = useMoveTool();
|
||||||
|
const selectionTool = useSelectionTool();
|
||||||
|
const transformTool = useTransformTool();
|
||||||
|
|
||||||
|
const selectionHandlers = useMemo(
|
||||||
|
() => ({
|
||||||
|
handleMouseDown: (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
|
const stage = e.target.getStage();
|
||||||
|
const pointer = stage?.getPointerPosition();
|
||||||
|
if (!pointer) return;
|
||||||
|
const pos = { x: (pointer.x - panOffset.x) / zoom, y: (pointer.y - panOffset.y) / zoom };
|
||||||
|
selectionTool.onMouseDown(pos, stage ?? undefined);
|
||||||
|
},
|
||||||
|
handleMouseMove: (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
|
const pointer = e.target.getStage()?.getPointerPosition();
|
||||||
|
if (!pointer) return;
|
||||||
|
const pos = { x: (pointer.x - panOffset.x) / zoom, y: (pointer.y - panOffset.y) / zoom };
|
||||||
|
selectionTool.onMouseMove(pos);
|
||||||
|
},
|
||||||
|
handleMouseUp: () => {
|
||||||
|
selectionTool.onMouseUp();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[selectionTool, zoom, panOffset],
|
||||||
|
);
|
||||||
|
|
||||||
const handlers = useMemo(() => {
|
const handlers = useMemo(() => {
|
||||||
const toolMap: Record<
|
const toolMap: Record<
|
||||||
@@ -443,12 +472,26 @@ function useActiveToolHandlers(stageRef: React.RefObject<Konva.Stage | null>) {
|
|||||||
text: textTool,
|
text: textTool,
|
||||||
fill: fillTool,
|
fill: fillTool,
|
||||||
gradient: gradientTool,
|
gradient: gradientTool,
|
||||||
|
"marquee-rect": selectionHandlers,
|
||||||
|
"marquee-ellipse": selectionHandlers,
|
||||||
|
"lasso-free": selectionHandlers,
|
||||||
|
"lasso-poly": selectionHandlers,
|
||||||
|
"magic-wand": selectionHandlers,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toolMap[activeTool] ?? null;
|
return toolMap[activeTool] ?? null;
|
||||||
}, [activeTool, brushTool, eraserTool, shapeTool, textTool, fillTool, gradientTool]);
|
}, [
|
||||||
|
activeTool,
|
||||||
|
brushTool,
|
||||||
|
eraserTool,
|
||||||
|
shapeTool,
|
||||||
|
textTool,
|
||||||
|
fillTool,
|
||||||
|
gradientTool,
|
||||||
|
selectionHandlers,
|
||||||
|
]);
|
||||||
|
|
||||||
return { handlers, moveTool };
|
return { handlers, moveTool, selectionTool, transformTool };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -463,6 +506,7 @@ export function EditorCanvas({
|
|||||||
onImageResize?: () => void;
|
onImageResize?: () => void;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const selectionLayerRef = useRef<Konva.Layer>(null);
|
||||||
const { stageRef, handleWheel, fitToScreen } = useCanvasZoom();
|
const { stageRef, handleWheel, fitToScreen } = useCanvasZoom();
|
||||||
|
|
||||||
const zoom = useEditorStore((s) => s.zoom);
|
const zoom = useEditorStore((s) => s.zoom);
|
||||||
@@ -484,7 +528,7 @@ export function EditorCanvas({
|
|||||||
const selectedObjectIds = useEditorStore((s) => s.selectedObjectIds);
|
const selectedObjectIds = useEditorStore((s) => s.selectedObjectIds);
|
||||||
const contextMenu = useContextMenu();
|
const contextMenu = useContextMenu();
|
||||||
|
|
||||||
const { handlers, moveTool } = useActiveToolHandlers(stageRef);
|
const { handlers, moveTool, selectionTool, transformTool } = useActiveToolHandlers(stageRef);
|
||||||
|
|
||||||
const [stageWidth, setStageWidth] = useState(800);
|
const [stageWidth, setStageWidth] = useState(800);
|
||||||
const [stageHeight, setStageHeight] = useState(600);
|
const [stageHeight, setStageHeight] = useState(600);
|
||||||
@@ -626,7 +670,7 @@ export function EditorCanvas({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Render objects grouped by layer */}
|
{/* Render objects grouped by layer */}
|
||||||
<Layer>
|
<Layer ref={selectionLayerRef}>
|
||||||
{/* Issue #14: Render source image as background */}
|
{/* Issue #14: Render source image as background */}
|
||||||
{sourceImageUrl && (
|
{sourceImageUrl && (
|
||||||
<SourceImage url={sourceImageUrl} adjustments={adjustments} filters={filters} />
|
<SourceImage url={sourceImageUrl} adjustments={adjustments} filters={filters} />
|
||||||
@@ -657,8 +701,37 @@ export function EditorCanvas({
|
|||||||
{activeTool === "move" && (
|
{activeTool === "move" && (
|
||||||
<MoveToolTransformer transformerRef={moveTool.transformerRef} />
|
<MoveToolTransformer transformerRef={moveTool.transformerRef} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Transform tool transformer */}
|
||||||
|
{activeTool === "transform" && (
|
||||||
|
<TransformToolTransformer transformerRef={transformTool.transformerRef} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Selection overlay (marching ants) */}
|
||||||
|
<SelectionOverlay layerRef={selectionLayerRef} />
|
||||||
|
|
||||||
|
{/* Active selection preview (drawn while dragging) */}
|
||||||
|
{selectionTool.isDrawing && selectionTool.currentPoints.length >= 4 && (
|
||||||
|
<Rect
|
||||||
|
x={Math.min(selectionTool.currentPoints[0], selectionTool.currentPoints[2])}
|
||||||
|
y={Math.min(selectionTool.currentPoints[1], selectionTool.currentPoints[3])}
|
||||||
|
width={Math.abs(selectionTool.currentPoints[2] - selectionTool.currentPoints[0])}
|
||||||
|
height={Math.abs(selectionTool.currentPoints[3] - selectionTool.currentPoints[1])}
|
||||||
|
stroke="#3b82f6"
|
||||||
|
strokeWidth={1}
|
||||||
|
dash={[4, 4]}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Layer>
|
</Layer>
|
||||||
|
|
||||||
|
{/* Crop overlay layer */}
|
||||||
|
{activeTool === "crop" && (
|
||||||
|
<Layer>
|
||||||
|
<CropOverlay />
|
||||||
|
</Layer>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Grid overlay layer (Feature 49) - non-interactive */}
|
{/* Grid overlay layer (Feature 49) - non-interactive */}
|
||||||
{(gridVisible || zoom >= 8) && (
|
{(gridVisible || zoom >= 8) && (
|
||||||
<Layer listening={false}>
|
<Layer listening={false}>
|
||||||
|
|||||||
@@ -202,12 +202,25 @@ export const useEditorStore = create<EditorState>()(
|
|||||||
// ===== ACTIONS =====
|
// ===== ACTIONS =====
|
||||||
|
|
||||||
setTool: (tool) => {
|
setTool: (tool) => {
|
||||||
const { activeTool } = get();
|
const { activeTool, canvasSize, cropState } = get();
|
||||||
|
const leavingCrop = activeTool === "crop" && tool !== "crop";
|
||||||
|
const enteringCrop = tool === "crop" && activeTool !== "crop";
|
||||||
set({
|
set({
|
||||||
activeTool: tool,
|
activeTool: tool,
|
||||||
previousTool: activeTool,
|
previousTool: activeTool,
|
||||||
isCropping: tool === "crop",
|
isCropping: tool === "crop",
|
||||||
...(activeTool === "crop" && tool !== "crop" ? { cropState: null } : {}),
|
...(leavingCrop ? { cropState: null } : {}),
|
||||||
|
...(enteringCrop && !cropState
|
||||||
|
? {
|
||||||
|
cropState: {
|
||||||
|
x: canvasSize.width * 0.1,
|
||||||
|
y: canvasSize.height * 0.1,
|
||||||
|
width: canvasSize.width * 0.8,
|
||||||
|
height: canvasSize.height * 0.8,
|
||||||
|
aspectRatio: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user