refactor: move tool code into custom hooks
This commit is contained in:
parent
df016ddf74
commit
58c30b22bf
11 changed files with 412 additions and 283 deletions
|
|
@ -13,6 +13,16 @@ import { ToolContext } from "@/context/Tool";
|
|||
import { useTextures } from "@/hooks/useTextures";
|
||||
import { useBlockData } from "@/hooks/useBlockData";
|
||||
|
||||
import { useMoveTool } from "@/hooks/tools/useMoveTool";
|
||||
import { useRectangleSelectTool } from "@/hooks/tools/useRectangleSelectTool";
|
||||
import { useLassoTool } from "@/hooks/tools/useLassoTool";
|
||||
import { useMagicWandTool } from "@/hooks/tools/useMagicWandTool";
|
||||
import { usePencilTool } from "@/hooks/tools/usePencilTool";
|
||||
import { useEraserTool } from "@/hooks/tools/useEraserTool";
|
||||
import { usePaintBucketTool } from "@/hooks/tools/usePaintBucketTool";
|
||||
import { useEyedropperTool } from "@/hooks/tools/useEyedropperTool";
|
||||
import { useZoomTool } from "@/hooks/tools/useZoomTool";
|
||||
|
||||
import * as selection from "@/utils/selection";
|
||||
import * as clipboard from "@/utils/clipboard";
|
||||
|
||||
|
|
@ -35,7 +45,7 @@ function Canvas() {
|
|||
const { settings } = useContext(SettingsContext);
|
||||
const { missingTexture } = useContext(TexturesContext);
|
||||
const { isDark } = useContext(ThemeContext);
|
||||
const { tool, radius, selectedBlock, setTool, setSelectedBlock } = useContext(ToolContext);
|
||||
const { tool, radius, setTool } = useContext(ToolContext);
|
||||
|
||||
const textures = useTextures(version);
|
||||
const blockData = useBlockData(version);
|
||||
|
|
@ -43,15 +53,36 @@ function Canvas() {
|
|||
|
||||
const [mousePosition, setMousePosition] = useState<Position>({ x: 0, y: 0 });
|
||||
const [mouseCoords, setMouseCoords] = useState<Position>({ x: 0, y: 0 });
|
||||
const mouseMovementRef = useRef<Position>();
|
||||
const mouseMovementRef = useRef<Position>({ x: 0, y: 0 });
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const dragStartCoordsRef = useRef<Position>();
|
||||
const dragStartCoordsRef = useRef<Position>({ x: 0, y: 0 });
|
||||
|
||||
const holdingShiftRef = useRef(false);
|
||||
const holdingAltRef = useRef(false);
|
||||
const oldToolRef = useRef<Tool>();
|
||||
const [cssCursor, setCssCursor] = useState("crosshair");
|
||||
|
||||
const zoom = useCallback(
|
||||
(newScale: number) => {
|
||||
setScale(newScale);
|
||||
setCoords({
|
||||
x: mousePosition.x - ((mousePosition.x - coords.x) / scale) * newScale,
|
||||
y: mousePosition.y - ((mousePosition.y - coords.y) / scale) * newScale,
|
||||
});
|
||||
},
|
||||
[coords, mousePosition, scale, setCoords, setScale]
|
||||
);
|
||||
|
||||
const moveTool = useMoveTool(mouseMovementRef.current);
|
||||
const rectangleSelectTool = useRectangleSelectTool(mouseCoords, dragStartCoordsRef.current, holdingShiftRef.current);
|
||||
const lassoTool = useLassoTool(mouseCoords, holdingAltRef.current);
|
||||
const magicWandTool = useMagicWandTool(mouseCoords, holdingShiftRef.current, holdingAltRef.current);
|
||||
const pencilTool = usePencilTool(mouseCoords);
|
||||
const eraserTool = useEraserTool(mouseCoords);
|
||||
const paintBucketTool = usePaintBucketTool(mouseCoords);
|
||||
const eyedropperTool = useEyedropperTool(mouseCoords);
|
||||
const zoomTool = useZoomTool(zoom, holdingAltRef.current);
|
||||
|
||||
const visibleArea = useMemo(() => {
|
||||
const blockSize = 16 * scale;
|
||||
|
||||
|
|
@ -75,17 +106,6 @@ function Canvas() {
|
|||
);
|
||||
}, [blocks, visibleArea]);
|
||||
|
||||
const zoom = useCallback(
|
||||
(newScale: number) => {
|
||||
setScale(newScale);
|
||||
setCoords({
|
||||
x: mousePosition.x - ((mousePosition.x - coords.x) / scale) * newScale,
|
||||
y: mousePosition.y - ((mousePosition.y - coords.y) / scale) * newScale,
|
||||
});
|
||||
},
|
||||
[coords, mousePosition, scale, setCoords, setScale]
|
||||
);
|
||||
|
||||
const updateCssCursor = useCallback(() => {
|
||||
const cursorMapping: Partial<Record<Tool, string>> = {
|
||||
hand: dragging ? "grab" : "grabbing",
|
||||
|
|
@ -97,126 +117,16 @@ function Canvas() {
|
|||
}, [dragging, holdingAltRef, tool, setCssCursor]);
|
||||
|
||||
const onToolUse = useCallback(() => {
|
||||
// If number is odd, cursor is in the center
|
||||
// if number is even, cursor is in the top-left corner
|
||||
const getRadiusPosition = (): Position => {
|
||||
const halfSize = Math.floor(radius / 2);
|
||||
const x = mouseCoords.x - (radius % 2 === 0 ? 0 : halfSize);
|
||||
const y = mouseCoords.y - (radius % 2 === 0 ? 0 : halfSize);
|
||||
return { x, y };
|
||||
const tools: Partial<Record<Tool, { use: () => void }>> = {
|
||||
move: moveTool,
|
||||
"rectangle-select": rectangleSelectTool,
|
||||
lasso: lassoTool,
|
||||
pencil: pencilTool,
|
||||
eraser: eraserTool,
|
||||
};
|
||||
|
||||
const eraseTool = () => {
|
||||
const radiusPosition = getRadiusPosition();
|
||||
const updated = blocks.filter((block) => {
|
||||
const withinRadius =
|
||||
block.x >= radiusPosition.x && block.x < radiusPosition.x + radius && block.y >= radiusPosition.y && block.y < radiusPosition.y + radius;
|
||||
return !withinRadius || !selection.isIn(selectionCoords, block.x, block.y);
|
||||
});
|
||||
|
||||
setBlocks(updated);
|
||||
};
|
||||
|
||||
switch (tool) {
|
||||
case "move": {
|
||||
const mouseMovement = mouseMovementRef.current;
|
||||
if (!mouseMovement) return;
|
||||
|
||||
// If there is no selection currently being moved...
|
||||
if (selectionLayerBlocks.length == 0) {
|
||||
const result: Block[] = [];
|
||||
|
||||
setBlocks((prev) =>
|
||||
prev.filter((b) => {
|
||||
const isSelected = selection.isIn(selectionCoords, b.x, b.y);
|
||||
|
||||
// Add blocks in the selection coords to the selection layer
|
||||
if (isSelected) result.push(b);
|
||||
|
||||
// Remove blocks originally there
|
||||
return !isSelected;
|
||||
})
|
||||
);
|
||||
setSelectionLayerBlocks(result);
|
||||
}
|
||||
|
||||
// Increase each coordinate in the selection by the mouse movement
|
||||
setSelectionCoords((prev) => prev.map(([x, y]) => [x + mouseMovement.x, y + mouseMovement.y]));
|
||||
setSelectionLayerBlocks((prev) => prev.map((b) => ({ ...b, x: b.x + mouseMovement.x, y: b.y + mouseMovement.y })));
|
||||
break;
|
||||
}
|
||||
case "lasso": {
|
||||
setSelectionCoords((prev) => {
|
||||
const radiusPosition = getRadiusPosition();
|
||||
const radiusCoords: CoordinateArray = [];
|
||||
|
||||
for (let x = 0; x < radius; x++) {
|
||||
for (let y = 0; y < radius; y++) {
|
||||
const tileX = radiusPosition.x + x;
|
||||
const tileY = radiusPosition.y + y;
|
||||
|
||||
const exists = prev.some(([x2, y2]) => x2 === tileX && y2 === tileY);
|
||||
if ((holdingAltRef.current && exists) || !exists) radiusCoords.push([tileX, tileY]);
|
||||
}
|
||||
}
|
||||
|
||||
if (holdingAltRef.current) {
|
||||
return prev.filter(([x, y]) => !radiusCoords.some(([x2, y2]) => x2 === x && y2 === y));
|
||||
} else {
|
||||
return [...prev, ...radiusCoords];
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "pencil": {
|
||||
if (selectedBlock == "air") {
|
||||
eraseTool();
|
||||
break;
|
||||
}
|
||||
|
||||
const radiusPosition = getRadiusPosition();
|
||||
const radiusBlocks: Block[] = [];
|
||||
|
||||
for (let x = 0; x < radius; x++) {
|
||||
for (let y = 0; y < radius; y++) {
|
||||
const tileX = radiusPosition.x + x;
|
||||
const tileY = radiusPosition.y + y;
|
||||
|
||||
// Only add blocks within the selection
|
||||
if (selection.isIn(selectionCoords, tileX, tileY)) {
|
||||
radiusBlocks.push({
|
||||
name: selectedBlock,
|
||||
x: tileX,
|
||||
y: tileY,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mergedBlocks = blocks.filter((block) => {
|
||||
return !radiusBlocks.some((newBlock) => block.x === newBlock.x && block.y === newBlock.y);
|
||||
});
|
||||
|
||||
setBlocks([...mergedBlocks, ...radiusBlocks]);
|
||||
break;
|
||||
}
|
||||
case "eraser": {
|
||||
eraseTool();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [
|
||||
tool,
|
||||
mouseCoords,
|
||||
selectedBlock,
|
||||
blocks,
|
||||
radius,
|
||||
selectionCoords,
|
||||
selectionLayerBlocks,
|
||||
setSelectionCoords,
|
||||
setSelectionLayerBlocks,
|
||||
setBlocks,
|
||||
]);
|
||||
tools[tool]?.use();
|
||||
}, [tool, moveTool, lassoTool, pencilTool, eraserTool, rectangleSelectTool]);
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
|
|
@ -245,54 +155,17 @@ function Canvas() {
|
|||
};
|
||||
|
||||
if (dragging) {
|
||||
switch (tool) {
|
||||
case "hand":
|
||||
if (tool === "hand") {
|
||||
setCoords((prevCoords) => ({
|
||||
x: prevCoords.x + e.movementX,
|
||||
y: prevCoords.y + e.movementY,
|
||||
}));
|
||||
break;
|
||||
case "rectangle-select": {
|
||||
const dragStartCoords = dragStartCoordsRef.current;
|
||||
if (!dragStartCoords) return;
|
||||
|
||||
setSelectionCoords(() => {
|
||||
const newSelection: CoordinateArray = [];
|
||||
|
||||
const startX = Math.min(dragStartCoords.x, mouseCoords.x);
|
||||
let endX = Math.max(dragStartCoords.x, mouseCoords.x);
|
||||
const startY = Math.min(dragStartCoords.y, mouseCoords.y);
|
||||
let endY = Math.max(dragStartCoords.y, mouseCoords.y);
|
||||
|
||||
const isRadiusEven = radius == 1 || radius % 2 == 0;
|
||||
const radiusOffset = isRadiusEven ? radius : radius - 1;
|
||||
|
||||
// If holding shift, create a square selection
|
||||
if (holdingShiftRef.current) {
|
||||
const width = Math.abs(endX - startX);
|
||||
const height = Math.abs(endY - startY);
|
||||
const size = Math.max(width, height);
|
||||
|
||||
endX = startX + (endX < startX ? -size : size);
|
||||
endY = startY + (endY < startY ? -size : size);
|
||||
}
|
||||
|
||||
for (let x = startX; x < endX + radiusOffset; x++) {
|
||||
for (let y = startY; y < endY + radiusOffset; y++) {
|
||||
newSelection.push([x, y]);
|
||||
}
|
||||
}
|
||||
|
||||
return newSelection;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onToolUse();
|
||||
}
|
||||
},
|
||||
[dragging, coords, scale, tool, mouseCoords, onToolUse, setCoords, setSelectionCoords, radius]
|
||||
[dragging, coords, scale, tool, mouseCoords, onToolUse, setCoords]
|
||||
);
|
||||
|
||||
const onMouseDown = useCallback(() => {
|
||||
|
|
@ -322,115 +195,15 @@ function Canvas() {
|
|||
);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
// Directions for adjacent blocks (up, down, left, right)
|
||||
const directions = [
|
||||
{ dx: 0, dy: 1 },
|
||||
{ dx: 0, dy: -1 },
|
||||
{ dx: 1, dy: 0 },
|
||||
{ dx: -1, dy: 0 },
|
||||
];
|
||||
switch (tool) {
|
||||
case "magic-wand": {
|
||||
const visited = new Set<string>();
|
||||
const result: CoordinateArray = [];
|
||||
const startBlock = blocks.find((block) => block.x === mouseCoords.x && block.y === mouseCoords.y);
|
||||
const startName = startBlock ? startBlock.name : "air";
|
||||
const tools: Partial<Record<Tool, { use: () => void }>> = {
|
||||
"magic-wand": magicWandTool,
|
||||
"paint-bucket": paintBucketTool,
|
||||
eyedropper: eyedropperTool,
|
||||
zoom: zoomTool,
|
||||
};
|
||||
|
||||
function depthFirstSearch(x: number, y: number) {
|
||||
const key = `${x},${y}`;
|
||||
if (visited.has(key)) return;
|
||||
visited.add(key);
|
||||
|
||||
const withinCanvas = x >= canvasSize.minX && x < canvasSize.maxX && y >= canvasSize.minY && y < canvasSize.maxY;
|
||||
if (!withinCanvas) return;
|
||||
|
||||
result.push([x, y]);
|
||||
|
||||
|
||||
for (const { dx, dy } of directions) {
|
||||
const newX = x + dx;
|
||||
const newY = y + dy;
|
||||
const adjacentBlock = blocks.find((b) => b.x === newX && b.y === newY);
|
||||
const adjacentName = adjacentBlock ? adjacentBlock.name : "air";
|
||||
|
||||
if (adjacentName === startName) {
|
||||
depthFirstSearch(newX, newY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
depthFirstSearch(mouseCoords.x, mouseCoords.y);
|
||||
setSelectionCoords((prev) => {
|
||||
if (holdingAltRef.current) {
|
||||
// If holding alt, remove new magic wand selection
|
||||
return prev.filter(([x, y]) => !result.some(([x2, y2]) => x2 === x && y2 === y));
|
||||
} else if (holdingShiftRef.current) {
|
||||
// If holding shift, add magic wand selection to existing selection
|
||||
const existing = new Set(prev.map(([x, y]) => `${x},${y}`));
|
||||
const newCoords = result.filter(([x, y]) => !existing.has(`${x},${y}`));
|
||||
return [...prev, ...newCoords];
|
||||
}
|
||||
|
||||
// If not holding alt or shift, replace the existing selection with the magic wand selection
|
||||
return result;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "paint-bucket": {
|
||||
const visited = new Set<string>();
|
||||
const startBlock = blocks.find((block) => block.x === mouseCoords.x && block.y === mouseCoords.y);
|
||||
const startName = startBlock ? startBlock.name : "air";
|
||||
|
||||
// If the target area is already the selected block, break
|
||||
if (startName === selectedBlock) break;
|
||||
|
||||
function floodFill(x: number, y: number) {
|
||||
const key = `${x},${y}`;
|
||||
if (visited.has(key)) return;
|
||||
visited.add(key);
|
||||
|
||||
const withinCanvas = x >= canvasSize.minX && x < canvasSize.maxX && y >= canvasSize.minY && y < canvasSize.maxY;
|
||||
if (!withinCanvas) return;
|
||||
|
||||
const block = blocks.find((b) => b.x === x && b.y === y);
|
||||
const currentName = block ? block.name : "air";
|
||||
|
||||
// Only fill if the current block name matches the target block name.
|
||||
if (currentName !== startName) return;
|
||||
|
||||
// Update block name or push new one
|
||||
if (block) {
|
||||
block.name = selectedBlock;
|
||||
} else {
|
||||
blocks.push({ x, y, name: selectedBlock });
|
||||
}
|
||||
|
||||
// Recursive
|
||||
for (const { dx, dy } of directions) {
|
||||
floodFill(x + dx, y + dy);
|
||||
}
|
||||
}
|
||||
|
||||
floodFill(mouseCoords.x, mouseCoords.y);
|
||||
setBlocks([...blocks]);
|
||||
break;
|
||||
}
|
||||
case "eyedropper": {
|
||||
const mouseBlock = blocks.find((block) => block.x === mouseCoords.x && block.y === mouseCoords.y);
|
||||
if (mouseBlock) setSelectedBlock(mouseBlock.name);
|
||||
break;
|
||||
}
|
||||
case "zoom": {
|
||||
const scaleChange = holdingAltRef.current ? -0.1 : 0.1;
|
||||
const newScale = Math.min(Math.max(scale + scaleChange * scale, 0.1), 32);
|
||||
zoom(newScale);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [tool, holdingAltRef, scale, mouseCoords, blocks, selectedBlock, canvasSize, setSelectionCoords, setBlocks, setSelectedBlock, zoom]);
|
||||
tools[tool]?.use();
|
||||
}, [tool, magicWandTool, paintBucketTool, eyedropperTool, zoomTool]);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
async (e: React.KeyboardEvent) => {
|
||||
|
|
|
|||
27
src/hooks/tools/useEraserTool.ts
Normal file
27
src/hooks/tools/useEraserTool.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
import { SelectionContext } from "@/context/Selection";
|
||||
import { ToolContext } from "@/context/Tool";
|
||||
|
||||
import { useRadiusPosition } from "../useRadiusPosition";
|
||||
|
||||
export function useEraserTool(mouseCoords: Position) {
|
||||
const { blocks, setBlocks } = useContext(CanvasContext);
|
||||
const { isInSelection } = useContext(SelectionContext);
|
||||
const { radius } = useContext(ToolContext);
|
||||
|
||||
const radiusPosition = useRadiusPosition(mouseCoords);
|
||||
|
||||
const use = () => {
|
||||
const updated = blocks.filter((block) => {
|
||||
const withinRadius =
|
||||
block.x >= radiusPosition.x && block.x < radiusPosition.x + radius && block.y >= radiusPosition.y && block.y < radiusPosition.y + radius;
|
||||
return !withinRadius || !isInSelection(block.x, block.y);
|
||||
});
|
||||
|
||||
setBlocks(updated);
|
||||
};
|
||||
|
||||
return { use };
|
||||
}
|
||||
16
src/hooks/tools/useEyedropperTool.ts
Normal file
16
src/hooks/tools/useEyedropperTool.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
import { ToolContext } from "@/context/Tool";
|
||||
|
||||
export function useEyedropperTool(mouseCoords: Position) {
|
||||
const { blocks } = useContext(CanvasContext);
|
||||
const { setSelectedBlock } = useContext(ToolContext);
|
||||
|
||||
const use = () => {
|
||||
const mouseBlock = blocks.find((block) => block.x === mouseCoords.x && block.y === mouseCoords.y);
|
||||
if (mouseBlock) setSelectedBlock(mouseBlock.name);
|
||||
};
|
||||
|
||||
return { use };
|
||||
}
|
||||
37
src/hooks/tools/useLassoTool.ts
Normal file
37
src/hooks/tools/useLassoTool.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { SelectionContext } from "@/context/Selection";
|
||||
import { ToolContext } from "@/context/Tool";
|
||||
|
||||
import { useRadiusPosition } from "../useRadiusPosition";
|
||||
|
||||
export function useLassoTool(mouseCoords: Position, holdingAlt: boolean) {
|
||||
const { setSelectionCoords } = useContext(SelectionContext);
|
||||
const { radius } = useContext(ToolContext);
|
||||
|
||||
const radiusPosition = useRadiusPosition(mouseCoords);
|
||||
|
||||
const use = () => {
|
||||
setSelectionCoords((prev) => {
|
||||
const radiusCoords: CoordinateArray = [];
|
||||
|
||||
for (let x = 0; x < radius; x++) {
|
||||
for (let y = 0; y < radius; y++) {
|
||||
const tileX = radiusPosition.x + x;
|
||||
const tileY = radiusPosition.y + y;
|
||||
|
||||
const exists = prev.some(([x2, y2]) => x2 === tileX && y2 === tileY);
|
||||
if ((holdingAlt && exists) || !exists) radiusCoords.push([tileX, tileY]);
|
||||
}
|
||||
}
|
||||
|
||||
if (holdingAlt) {
|
||||
return prev.filter(([x, y]) => !radiusCoords.some(([x2, y2]) => x2 === x && y2 === y));
|
||||
} else {
|
||||
return [...prev, ...radiusCoords];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return { use };
|
||||
}
|
||||
64
src/hooks/tools/useMagicWandTool.ts
Normal file
64
src/hooks/tools/useMagicWandTool.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
import { SelectionContext } from "@/context/Selection";
|
||||
|
||||
export function useMagicWandTool(mouseCoords: Position, holdingShift: boolean, holdingAlt: boolean) {
|
||||
const { blocks, canvasSize } = useContext(CanvasContext);
|
||||
const { setSelectionCoords } = useContext(SelectionContext);
|
||||
|
||||
// Directions for adjacent blocks (up, down, left, right)
|
||||
const directions = [
|
||||
{ dx: 0, dy: 1 },
|
||||
{ dx: 0, dy: -1 },
|
||||
{ dx: 1, dy: 0 },
|
||||
{ dx: -1, dy: 0 },
|
||||
];
|
||||
|
||||
const use = () => {
|
||||
const visited = new Set<string>();
|
||||
const result: CoordinateArray = [];
|
||||
const startBlock = blocks.find((block) => block.x === mouseCoords.x && block.y === mouseCoords.y);
|
||||
const startName = startBlock ? startBlock.name : "air";
|
||||
|
||||
function depthFirstSearch(x: number, y: number) {
|
||||
const key = `${x},${y}`;
|
||||
if (visited.has(key)) return;
|
||||
visited.add(key);
|
||||
|
||||
const withinCanvas = x >= canvasSize.minX && x < canvasSize.maxX && y >= canvasSize.minY && y < canvasSize.maxY;
|
||||
if (!withinCanvas) return;
|
||||
|
||||
result.push([x, y]);
|
||||
|
||||
for (const { dx, dy } of directions) {
|
||||
const newX = x + dx;
|
||||
const newY = y + dy;
|
||||
const adjacentBlock = blocks.find((b) => b.x === newX && b.y === newY);
|
||||
const adjacentName = adjacentBlock ? adjacentBlock.name : "air";
|
||||
|
||||
if (adjacentName === startName) {
|
||||
depthFirstSearch(newX, newY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
depthFirstSearch(mouseCoords.x, mouseCoords.y);
|
||||
setSelectionCoords((prev) => {
|
||||
if (holdingAlt) {
|
||||
// If holding alt, remove new magic wand selection
|
||||
return prev.filter(([x, y]) => !result.some(([x2, y2]) => x2 === x && y2 === y));
|
||||
} else if (holdingShift) {
|
||||
// If holding shift, add magic wand selection to existing selection
|
||||
const existing = new Set(prev.map(([x, y]) => `${x},${y}`));
|
||||
const newCoords = result.filter(([x, y]) => !existing.has(`${x},${y}`));
|
||||
return [...prev, ...newCoords];
|
||||
}
|
||||
|
||||
// If not holding alt or shift, replace the existing selection with the magic wand selection
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
return { use };
|
||||
}
|
||||
35
src/hooks/tools/useMoveTool.ts
Normal file
35
src/hooks/tools/useMoveTool.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { SelectionContext } from "@/context/Selection";
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
|
||||
export function useMoveTool(mouseMovement: Position) {
|
||||
const { setBlocks } = useContext(CanvasContext);
|
||||
const { selectionLayerBlocks, setSelectionCoords, setSelectionLayerBlocks, isInSelection } = useContext(SelectionContext);
|
||||
|
||||
const use = () => {
|
||||
// If there is no selection currently being moved...
|
||||
if (selectionLayerBlocks.length == 0) {
|
||||
const result: Block[] = [];
|
||||
|
||||
setBlocks((prev) =>
|
||||
prev.filter((b) => {
|
||||
const isSelected = isInSelection(b.x, b.y);
|
||||
|
||||
// Add blocks in the selection coords to the selection layer
|
||||
if (isSelected) result.push(b);
|
||||
|
||||
// Remove blocks originally there
|
||||
return !isSelected;
|
||||
})
|
||||
);
|
||||
setSelectionLayerBlocks(result);
|
||||
}
|
||||
|
||||
// Increase each coordinate in the selection by the mouse movement
|
||||
setSelectionCoords((prev) => prev.map(([x, y]) => [x + mouseMovement.x, y + mouseMovement.y]));
|
||||
setSelectionLayerBlocks((prev) => prev.map((b) => ({ ...b, x: b.x + mouseMovement.x, y: b.y + mouseMovement.y })));
|
||||
};
|
||||
|
||||
return { use };
|
||||
}
|
||||
58
src/hooks/tools/usePaintBucketTool.ts
Normal file
58
src/hooks/tools/usePaintBucketTool.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
import { ToolContext } from "@/context/Tool";
|
||||
|
||||
export function usePaintBucketTool(mouseCoords: Position) {
|
||||
const { blocks, canvasSize, setBlocks } = useContext(CanvasContext);
|
||||
const { selectedBlock } = useContext(ToolContext);
|
||||
|
||||
// Directions for adjacent blocks (up, down, left, right)
|
||||
const directions = [
|
||||
{ dx: 0, dy: 1 },
|
||||
{ dx: 0, dy: -1 },
|
||||
{ dx: 1, dy: 0 },
|
||||
{ dx: -1, dy: 0 },
|
||||
];
|
||||
|
||||
const use = () => {
|
||||
const visited = new Set<string>();
|
||||
const startBlock = blocks.find((block) => block.x === mouseCoords.x && block.y === mouseCoords.y);
|
||||
const startName = startBlock ? startBlock.name : "air";
|
||||
|
||||
// If the target area is already the selected block, return
|
||||
if (startName === selectedBlock) return;
|
||||
|
||||
function floodFill(x: number, y: number) {
|
||||
const key = `${x},${y}`;
|
||||
if (visited.has(key)) return;
|
||||
visited.add(key);
|
||||
|
||||
const withinCanvas = x >= canvasSize.minX && x < canvasSize.maxX && y >= canvasSize.minY && y < canvasSize.maxY;
|
||||
if (!withinCanvas) return;
|
||||
|
||||
const block = blocks.find((b) => b.x === x && b.y === y);
|
||||
const currentName = block ? block.name : "air";
|
||||
|
||||
// Only fill if the current block name matches the target block name.
|
||||
if (currentName !== startName) return;
|
||||
|
||||
// Update block name or push new one
|
||||
if (block) {
|
||||
block.name = selectedBlock;
|
||||
} else {
|
||||
blocks.push({ x, y, name: selectedBlock });
|
||||
}
|
||||
|
||||
// Recursive
|
||||
for (const { dx, dy } of directions) {
|
||||
floodFill(x + dx, y + dy);
|
||||
}
|
||||
}
|
||||
|
||||
floodFill(mouseCoords.x, mouseCoords.y);
|
||||
setBlocks(() => [...blocks]);
|
||||
};
|
||||
|
||||
return { use };
|
||||
}
|
||||
47
src/hooks/tools/usePencilTool.ts
Normal file
47
src/hooks/tools/usePencilTool.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
import { SelectionContext } from "@/context/Selection";
|
||||
import { ToolContext } from "@/context/Tool";
|
||||
|
||||
import { useRadiusPosition } from "../useRadiusPosition";
|
||||
|
||||
export function usePencilTool(mouseCoords: Position) {
|
||||
const { blocks, setBlocks } = useContext(CanvasContext);
|
||||
const { isInSelection } = useContext(SelectionContext);
|
||||
const { selectedBlock, radius } = useContext(ToolContext);
|
||||
|
||||
const radiusPosition = useRadiusPosition(mouseCoords);
|
||||
|
||||
const use = () => {
|
||||
// if (selectedBlock == "air") {
|
||||
// eraseTool();
|
||||
// break;
|
||||
// }
|
||||
const radiusBlocks: Block[] = [];
|
||||
|
||||
for (let x = 0; x < radius; x++) {
|
||||
for (let y = 0; y < radius; y++) {
|
||||
const tileX = radiusPosition.x + x;
|
||||
const tileY = radiusPosition.y + y;
|
||||
|
||||
// Only add blocks within the selection
|
||||
if (isInSelection(tileX, tileY)) {
|
||||
radiusBlocks.push({
|
||||
name: selectedBlock,
|
||||
x: tileX,
|
||||
y: tileY,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mergedBlocks = blocks.filter((block) => {
|
||||
return !radiusBlocks.some((newBlock) => block.x === newBlock.x && block.y === newBlock.y);
|
||||
});
|
||||
|
||||
setBlocks([...mergedBlocks, ...radiusBlocks]);
|
||||
};
|
||||
|
||||
return { use };
|
||||
}
|
||||
41
src/hooks/tools/useRectangleSelectTool.ts
Normal file
41
src/hooks/tools/useRectangleSelectTool.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { SelectionContext } from "@/context/Selection";
|
||||
import { ToolContext } from "@/context/Tool";
|
||||
|
||||
export function useRectangleSelectTool(mouseCoords: Position, dragStartCoords: Position, holdingShift: boolean) {
|
||||
const { setSelectionCoords } = useContext(SelectionContext);
|
||||
const { radius } = useContext(ToolContext);
|
||||
|
||||
const use = () => {
|
||||
const newSelection: CoordinateArray = [];
|
||||
|
||||
const startX = Math.min(dragStartCoords.x, mouseCoords.x);
|
||||
let endX = Math.max(dragStartCoords.x, mouseCoords.x);
|
||||
const startY = Math.min(dragStartCoords.y, mouseCoords.y);
|
||||
let endY = Math.max(dragStartCoords.y, mouseCoords.y);
|
||||
|
||||
const isRadiusEven = radius == 1 || radius % 2 == 0;
|
||||
const radiusOffset = isRadiusEven ? radius : radius - 1;
|
||||
|
||||
// If holding shift, create a square selection
|
||||
if (holdingShift) {
|
||||
const width = Math.abs(endX - startX);
|
||||
const height = Math.abs(endY - startY);
|
||||
const size = Math.max(width, height);
|
||||
|
||||
endX = startX + (endX < startX ? -size : size);
|
||||
endY = startY + (endY < startY ? -size : size);
|
||||
}
|
||||
|
||||
for (let x = startX; x < endX + radiusOffset; x++) {
|
||||
for (let y = startY; y < endY + radiusOffset; y++) {
|
||||
newSelection.push([x, y]);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectionCoords(newSelection);
|
||||
};
|
||||
|
||||
return { use };
|
||||
}
|
||||
15
src/hooks/tools/useZoomTool.ts
Normal file
15
src/hooks/tools/useZoomTool.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
|
||||
export function useZoomTool(zoom: (newScale: number) => void, holdingAlt: boolean) {
|
||||
const { scale } = useContext(CanvasContext);
|
||||
|
||||
const use = () => {
|
||||
const scaleChange = holdingAlt ? -0.1 : 0.1;
|
||||
const newScale = Math.min(Math.max(scale + scaleChange * scale, 0.1), 32);
|
||||
zoom(newScale);
|
||||
};
|
||||
|
||||
return { use };
|
||||
}
|
||||
16
src/hooks/useRadiusPosition.ts
Normal file
16
src/hooks/useRadiusPosition.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { useContext, useMemo } from "react";
|
||||
import { ToolContext } from "@/context/Tool";
|
||||
|
||||
export function useRadiusPosition(mouseCoords: Position): Position {
|
||||
const { radius } = useContext(ToolContext);
|
||||
|
||||
// If number is odd, cursor is in the center
|
||||
// If number is even, cursor is in the top-left corner
|
||||
|
||||
return useMemo(() => {
|
||||
const halfSize = Math.floor(radius / 2);
|
||||
const x = mouseCoords.x - (radius % 2 === 0 ? 0 : halfSize);
|
||||
const y = mouseCoords.y - (radius % 2 === 0 ? 0 : halfSize);
|
||||
return { x, y };
|
||||
}, [radius, mouseCoords]);
|
||||
}
|
||||
Loading…
Reference in a new issue