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 { useTextures } from "@/hooks/useTextures";
|
||||||
import { useBlockData } from "@/hooks/useBlockData";
|
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 selection from "@/utils/selection";
|
||||||
import * as clipboard from "@/utils/clipboard";
|
import * as clipboard from "@/utils/clipboard";
|
||||||
|
|
||||||
|
|
@ -35,7 +45,7 @@ function Canvas() {
|
||||||
const { settings } = useContext(SettingsContext);
|
const { settings } = useContext(SettingsContext);
|
||||||
const { missingTexture } = useContext(TexturesContext);
|
const { missingTexture } = useContext(TexturesContext);
|
||||||
const { isDark } = useContext(ThemeContext);
|
const { isDark } = useContext(ThemeContext);
|
||||||
const { tool, radius, selectedBlock, setTool, setSelectedBlock } = useContext(ToolContext);
|
const { tool, radius, setTool } = useContext(ToolContext);
|
||||||
|
|
||||||
const textures = useTextures(version);
|
const textures = useTextures(version);
|
||||||
const blockData = useBlockData(version);
|
const blockData = useBlockData(version);
|
||||||
|
|
@ -43,15 +53,36 @@ function Canvas() {
|
||||||
|
|
||||||
const [mousePosition, setMousePosition] = useState<Position>({ x: 0, y: 0 });
|
const [mousePosition, setMousePosition] = useState<Position>({ x: 0, y: 0 });
|
||||||
const [mouseCoords, setMouseCoords] = 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 [dragging, setDragging] = useState(false);
|
||||||
const dragStartCoordsRef = useRef<Position>();
|
const dragStartCoordsRef = useRef<Position>({ x: 0, y: 0 });
|
||||||
|
|
||||||
const holdingShiftRef = useRef(false);
|
const holdingShiftRef = useRef(false);
|
||||||
const holdingAltRef = useRef(false);
|
const holdingAltRef = useRef(false);
|
||||||
const oldToolRef = useRef<Tool>();
|
const oldToolRef = useRef<Tool>();
|
||||||
const [cssCursor, setCssCursor] = useState("crosshair");
|
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 visibleArea = useMemo(() => {
|
||||||
const blockSize = 16 * scale;
|
const blockSize = 16 * scale;
|
||||||
|
|
||||||
|
|
@ -75,17 +106,6 @@ function Canvas() {
|
||||||
);
|
);
|
||||||
}, [blocks, visibleArea]);
|
}, [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 updateCssCursor = useCallback(() => {
|
||||||
const cursorMapping: Partial<Record<Tool, string>> = {
|
const cursorMapping: Partial<Record<Tool, string>> = {
|
||||||
hand: dragging ? "grab" : "grabbing",
|
hand: dragging ? "grab" : "grabbing",
|
||||||
|
|
@ -97,126 +117,16 @@ function Canvas() {
|
||||||
}, [dragging, holdingAltRef, tool, setCssCursor]);
|
}, [dragging, holdingAltRef, tool, setCssCursor]);
|
||||||
|
|
||||||
const onToolUse = useCallback(() => {
|
const onToolUse = useCallback(() => {
|
||||||
// If number is odd, cursor is in the center
|
const tools: Partial<Record<Tool, { use: () => void }>> = {
|
||||||
// if number is even, cursor is in the top-left corner
|
move: moveTool,
|
||||||
const getRadiusPosition = (): Position => {
|
"rectangle-select": rectangleSelectTool,
|
||||||
const halfSize = Math.floor(radius / 2);
|
lasso: lassoTool,
|
||||||
const x = mouseCoords.x - (radius % 2 === 0 ? 0 : halfSize);
|
pencil: pencilTool,
|
||||||
const y = mouseCoords.y - (radius % 2 === 0 ? 0 : halfSize);
|
eraser: eraserTool,
|
||||||
return { x, y };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const eraseTool = () => {
|
tools[tool]?.use();
|
||||||
const radiusPosition = getRadiusPosition();
|
}, [tool, moveTool, lassoTool, pencilTool, eraserTool, rectangleSelectTool]);
|
||||||
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,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onMouseMove = useCallback(
|
const onMouseMove = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
|
|
@ -245,54 +155,17 @@ function Canvas() {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
switch (tool) {
|
if (tool === "hand") {
|
||||||
case "hand":
|
setCoords((prevCoords) => ({
|
||||||
setCoords((prevCoords) => ({
|
x: prevCoords.x + e.movementX,
|
||||||
x: prevCoords.x + e.movementX,
|
y: prevCoords.y + e.movementY,
|
||||||
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();
|
onToolUse();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dragging, coords, scale, tool, mouseCoords, onToolUse, setCoords, setSelectionCoords, radius]
|
[dragging, coords, scale, tool, mouseCoords, onToolUse, setCoords]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMouseDown = useCallback(() => {
|
const onMouseDown = useCallback(() => {
|
||||||
|
|
@ -322,115 +195,15 @@ function Canvas() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
// Directions for adjacent blocks (up, down, left, right)
|
const tools: Partial<Record<Tool, { use: () => void }>> = {
|
||||||
const directions = [
|
"magic-wand": magicWandTool,
|
||||||
{ dx: 0, dy: 1 },
|
"paint-bucket": paintBucketTool,
|
||||||
{ dx: 0, dy: -1 },
|
eyedropper: eyedropperTool,
|
||||||
{ dx: 1, dy: 0 },
|
zoom: zoomTool,
|
||||||
{ 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";
|
|
||||||
|
|
||||||
function depthFirstSearch(x: number, y: number) {
|
tools[tool]?.use();
|
||||||
const key = `${x},${y}`;
|
}, [tool, magicWandTool, paintBucketTool, eyedropperTool, zoomTool]);
|
||||||
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]);
|
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
const onKeyDown = useCallback(
|
||||||
async (e: React.KeyboardEvent) => {
|
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