mirror of
https://github.com/trafficlunar/blockmatic.git
synced 2026-06-28 06:34:13 +00:00
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
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…
Add table
Add a link
Reference in a new issue