mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: wire 8 unwired tools, fix selection type sync, fix selection preview
Root cause analysis of the magic wand bug revealed the same dispatch pattern affected 13 other tools: - 8 tools (eyedropper, clone stamp, dodge, burn, sponge, blur brush, sharpen brush, smudge) had hook implementations but were never imported or wired into the canvas toolMap -- clicking did nothing - Selection type (rect/ellipse/lasso) was never synced from the global activeTool to the selection hook's internal state, so marquee-ellipse and lasso tools always produced rectangular selections - Selection drag preview was hardcoded to Rect instead of using the existing ActiveSelectionPreview component - Zoom tool click did nothing (now zooms in on click, out on alt+click) - Magic wand dispatched to generic selection drag instead of flood fill
This commit is contained in:
@@ -26,12 +26,16 @@ 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 { useCloneStampTool } from "./tools/clone-stamp-tool";
|
||||||
import { CropOverlay } from "./tools/crop-tool";
|
import { CropOverlay } from "./tools/crop-tool";
|
||||||
|
import { useDodgeBurnTool } from "./tools/dodge-burn-tool";
|
||||||
import { useEraserTool } from "./tools/eraser-tool";
|
import { useEraserTool } from "./tools/eraser-tool";
|
||||||
|
import { useEyedropperTool } from "./tools/eyedropper-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 { usePixelBrushTool } from "./tools/pixel-brush-tool";
|
||||||
|
import { ActiveSelectionPreview, 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";
|
import { TransformToolTransformer, useTransformTool } from "./tools/transform-tool";
|
||||||
@@ -418,9 +422,14 @@ 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 zoom = useEditorStore((s) => s.zoom);
|
||||||
const panOffset = useEditorStore((s) => s.panOffset);
|
const panOffset = useEditorStore((s) => s.panOffset);
|
||||||
|
const magicWandTolerance = useEditorStore((s) => s.magicWandTolerance);
|
||||||
|
const fillContiguous = useEditorStore((s) => s.fillContiguous);
|
||||||
|
|
||||||
const brushTool = useBrushTool();
|
const brushTool = useBrushTool();
|
||||||
const eraserTool = useEraserTool();
|
const eraserTool = useEraserTool();
|
||||||
|
const cloneStampTool = useCloneStampTool(stageRef);
|
||||||
|
const dodgeBurnTool = useDodgeBurnTool(stageRef);
|
||||||
|
const pixelBrushTool = usePixelBrushTool(stageRef);
|
||||||
const shapeTool = useShapeTool();
|
const shapeTool = useShapeTool();
|
||||||
const textTool = useTextTool();
|
const textTool = useTextTool();
|
||||||
const fillTool = useFillTool(stageRef);
|
const fillTool = useFillTool(stageRef);
|
||||||
@@ -428,6 +437,22 @@ function useActiveToolHandlers(stageRef: React.RefObject<Konva.Stage | null>) {
|
|||||||
const moveTool = useMoveTool();
|
const moveTool = useMoveTool();
|
||||||
const selectionTool = useSelectionTool();
|
const selectionTool = useSelectionTool();
|
||||||
const transformTool = useTransformTool();
|
const transformTool = useTransformTool();
|
||||||
|
const eyedropperTool = useEyedropperTool({ stageRef, sampleSize: 1 });
|
||||||
|
|
||||||
|
// Sync the selection hook's internal selectionType from the global activeTool.
|
||||||
|
// Without this, marquee-ellipse/lasso always produce rect selections.
|
||||||
|
useEffect(() => {
|
||||||
|
const typeMap: Record<string, "rect" | "ellipse" | "lasso"> = {
|
||||||
|
"marquee-rect": "rect",
|
||||||
|
"marquee-ellipse": "ellipse",
|
||||||
|
"lasso-free": "lasso",
|
||||||
|
"lasso-poly": "lasso",
|
||||||
|
};
|
||||||
|
const mapped = typeMap[activeTool];
|
||||||
|
if (mapped) {
|
||||||
|
selectionTool.setSelectionType(mapped);
|
||||||
|
}
|
||||||
|
}, [activeTool, selectionTool.setSelectionType]);
|
||||||
|
|
||||||
const selectionHandlers = useMemo(
|
const selectionHandlers = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -451,18 +476,66 @@ function useActiveToolHandlers(stageRef: React.RefObject<Konva.Stage | null>) {
|
|||||||
[selectionTool, zoom, panOffset],
|
[selectionTool, zoom, panOffset],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlers = useMemo(() => {
|
const magicWandHandlers = useMemo(
|
||||||
const toolMap: Record<
|
() => ({
|
||||||
string,
|
handleMouseDown: (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
{
|
const stage = e.target.getStage();
|
||||||
|
const pointer = stage?.getPointerPosition();
|
||||||
|
if (!pointer || !stage) return;
|
||||||
|
const pos = { x: (pointer.x - panOffset.x) / zoom, y: (pointer.y - panOffset.y) / zoom };
|
||||||
|
selectionTool.magicWandSelect(stage, pos.x, pos.y, magicWandTolerance, fillContiguous);
|
||||||
|
},
|
||||||
|
handleMouseMove: () => {},
|
||||||
|
handleMouseUp: () => {},
|
||||||
|
}),
|
||||||
|
[selectionTool, zoom, panOffset, magicWandTolerance, fillContiguous],
|
||||||
|
);
|
||||||
|
|
||||||
|
const eyedropperHandlers = useMemo(
|
||||||
|
() => ({
|
||||||
|
handleMouseDown: (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
|
eyedropperTool.handleEyedropperClick(e);
|
||||||
|
},
|
||||||
|
handleMouseMove: (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
|
eyedropperTool.handleEyedropperMove(e);
|
||||||
|
},
|
||||||
|
handleMouseUp: () => {},
|
||||||
|
}),
|
||||||
|
[eyedropperTool],
|
||||||
|
);
|
||||||
|
|
||||||
|
const zoomHandlers = useMemo(
|
||||||
|
() => ({
|
||||||
|
handleMouseDown: (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
|
const isAlt = e.evt.altKey;
|
||||||
|
const state = useEditorStore.getState();
|
||||||
|
const factor = isAlt ? 1 / 1.5 : 1.5;
|
||||||
|
state.setZoom(state.zoom * factor);
|
||||||
|
},
|
||||||
|
handleMouseMove: () => {},
|
||||||
|
handleMouseUp: () => {},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
type ToolHandlers = {
|
||||||
handleMouseDown: (e: Konva.KonvaEventObject<MouseEvent>) => void;
|
handleMouseDown: (e: Konva.KonvaEventObject<MouseEvent>) => void;
|
||||||
handleMouseMove: (e: Konva.KonvaEventObject<MouseEvent>) => void;
|
handleMouseMove: (e: Konva.KonvaEventObject<MouseEvent>) => void;
|
||||||
handleMouseUp: (e: Konva.KonvaEventObject<MouseEvent>) => void;
|
handleMouseUp: (e: Konva.KonvaEventObject<MouseEvent>) => void;
|
||||||
}
|
};
|
||||||
> = {
|
|
||||||
|
const handlers = useMemo(() => {
|
||||||
|
const toolMap: Record<string, ToolHandlers> = {
|
||||||
brush: brushTool,
|
brush: brushTool,
|
||||||
pencil: brushTool,
|
pencil: brushTool,
|
||||||
eraser: eraserTool,
|
eraser: eraserTool,
|
||||||
|
"clone-stamp": cloneStampTool,
|
||||||
|
dodge: dodgeBurnTool,
|
||||||
|
burn: dodgeBurnTool,
|
||||||
|
sponge: dodgeBurnTool,
|
||||||
|
"blur-brush": pixelBrushTool,
|
||||||
|
"sharpen-brush": pixelBrushTool,
|
||||||
|
smudge: pixelBrushTool,
|
||||||
"shape-rect": shapeTool,
|
"shape-rect": shapeTool,
|
||||||
"shape-ellipse": shapeTool,
|
"shape-ellipse": shapeTool,
|
||||||
"shape-line": shapeTool,
|
"shape-line": shapeTool,
|
||||||
@@ -472,11 +545,13 @@ function useActiveToolHandlers(stageRef: React.RefObject<Konva.Stage | null>) {
|
|||||||
text: textTool,
|
text: textTool,
|
||||||
fill: fillTool,
|
fill: fillTool,
|
||||||
gradient: gradientTool,
|
gradient: gradientTool,
|
||||||
|
eyedropper: eyedropperHandlers,
|
||||||
|
zoom: zoomHandlers,
|
||||||
"marquee-rect": selectionHandlers,
|
"marquee-rect": selectionHandlers,
|
||||||
"marquee-ellipse": selectionHandlers,
|
"marquee-ellipse": selectionHandlers,
|
||||||
"lasso-free": selectionHandlers,
|
"lasso-free": selectionHandlers,
|
||||||
"lasso-poly": selectionHandlers,
|
"lasso-poly": selectionHandlers,
|
||||||
"magic-wand": selectionHandlers,
|
"magic-wand": magicWandHandlers,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toolMap[activeTool] ?? null;
|
return toolMap[activeTool] ?? null;
|
||||||
@@ -484,11 +559,17 @@ function useActiveToolHandlers(stageRef: React.RefObject<Konva.Stage | null>) {
|
|||||||
activeTool,
|
activeTool,
|
||||||
brushTool,
|
brushTool,
|
||||||
eraserTool,
|
eraserTool,
|
||||||
|
cloneStampTool,
|
||||||
|
dodgeBurnTool,
|
||||||
|
pixelBrushTool,
|
||||||
shapeTool,
|
shapeTool,
|
||||||
textTool,
|
textTool,
|
||||||
fillTool,
|
fillTool,
|
||||||
gradientTool,
|
gradientTool,
|
||||||
|
eyedropperHandlers,
|
||||||
|
zoomHandlers,
|
||||||
selectionHandlers,
|
selectionHandlers,
|
||||||
|
magicWandHandlers,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { handlers, moveTool, selectionTool, transformTool };
|
return { handlers, moveTool, selectionTool, transformTool };
|
||||||
@@ -712,15 +793,9 @@ export function EditorCanvas({
|
|||||||
|
|
||||||
{/* Active selection preview (drawn while dragging) */}
|
{/* Active selection preview (drawn while dragging) */}
|
||||||
{selectionTool.isDrawing && selectionTool.currentPoints.length >= 4 && (
|
{selectionTool.isDrawing && selectionTool.currentPoints.length >= 4 && (
|
||||||
<Rect
|
<ActiveSelectionPreview
|
||||||
x={Math.min(selectionTool.currentPoints[0], selectionTool.currentPoints[2])}
|
type={selectionTool.selectionType}
|
||||||
y={Math.min(selectionTool.currentPoints[1], selectionTool.currentPoints[3])}
|
points={selectionTool.currentPoints}
|
||||||
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user