From 527e29c448aa1b5a0736e233628fb859012d3fed Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Sat, 18 Jan 2025 21:59:42 +0000 Subject: [PATCH] refactor: use coordinate array for selection allows upcoming feature for users to select non-rectangular blocks --- src/components/canvas/Canvas.tsx | 111 ++++++++++++------------- src/components/canvas/SelectionBox.tsx | 20 +++-- src/components/menubar/EditMenu.tsx | 2 +- src/context/Tool.tsx | 10 +-- src/types.d.ts | 2 + 5 files changed, 70 insertions(+), 75 deletions(-) diff --git a/src/components/canvas/Canvas.tsx b/src/components/canvas/Canvas.tsx index e740335..c19a254 100644 --- a/src/components/canvas/Canvas.tsx +++ b/src/components/canvas/Canvas.tsx @@ -30,7 +30,7 @@ function Canvas() { const { settings } = useContext(SettingsContext); const { missingTexture, solidTextures } = useContext(TexturesContext); const { isDark } = useContext(ThemeContext); - const { tool, radius, selectedBlock, selectionBoxBounds, cssCursor, setTool, setSelectedBlock, setSelectionBoxBounds, setCssCursor } = + const { tool, radius, selectedBlock, selectionCoords, cssCursor, setTool, setSelectedBlock, setSelectionCoords, setCssCursor } = useContext(ToolContext); const textures = useTextures(version); @@ -38,12 +38,13 @@ function Canvas() { const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); const [mouseCoords, setMouseCoords] = useState({ x: 0, y: 0 }); - const mouseMovement = useRef(); + const mouseMovementRef = useRef(); const [dragging, setDragging] = useState(false); + const dragStartCoordsRef = useRef(); const [holdingAlt, setHoldingAlt] = useState(false); - const selectionBoxBoundsRef = useRef(); const oldToolRef = useRef(); + const selectionCoordsRef = useRef(selectionCoords); const visibleArea = useMemo(() => { const blockSize = 16 * scale; @@ -97,9 +98,12 @@ function Canvas() { return { x, y }; }; - // Check if a block is within the selection bounds - const isInSelection = (x: number, y: number) => { - return x >= selectionBoxBounds.minX && x < selectionBoxBounds.maxX && y >= selectionBoxBounds.minY && y < selectionBoxBounds.maxY; + // Check if a block is within the selection + const isInSelection = (x: number, y: number): boolean => { + if (selectionCoords.length !== 0) { + return selectionCoords.some(([x2, y2]) => x2 === x && y2 === y); + } + return false; }; const eraseTool = () => { @@ -118,39 +122,26 @@ function Canvas() { switch (tool) { case "move": { - if (!mouseMovement.current) return; - const { x: movementX, y: movementY } = mouseMovement.current; + const mouseMovement = mouseMovementRef.current; + if (!mouseMovement) return; - setSelectionBoxBounds((prev) => { - const newBounds = { - minX: prev.minX + movementX, - minY: prev.minY + movementY, - maxX: prev.maxX + movementX, - maxY: prev.maxY + movementY, - }; + // Increase each coordinate in the selection by the mouse movement + setSelectionCoords((prev) => prev.map(([x, y]) => [x + mouseMovement.x, y + mouseMovement.y])); - selectionBoxBoundsRef.current = newBounds; - return newBounds; - }); - - setBlocks((prev) => { - return prev.map((block) => { - if ( - block.x >= selectionBoxBounds.minX && - block.x < selectionBoxBounds.maxX && - block.y >= selectionBoxBounds.minY && - block.y < selectionBoxBounds.maxY - ) { + // Increase each block in the selection by the mouse movement + setBlocks((prev) => + prev.map((block) => { + if (isInSelection(block.x, block.y)) { return { ...block, - x: block.x + movementX, - y: block.y + movementY, + x: block.x + mouseMovement.x, + y: block.y + mouseMovement.y, }; } - return block; - }); - }); + return block; + }) + ); break; } case "pencil": { @@ -190,7 +181,7 @@ function Canvas() { break; } } - }, [tool, mouseCoords, selectedBlock, blocks, radius, selectionBoxBounds, setBlocks]); + }, [tool, mouseCoords, selectedBlock, blocks, radius, selectionCoords, setSelectionCoords, setBlocks]); const onMouseMove = useCallback( (e: React.MouseEvent) => { @@ -213,7 +204,7 @@ function Canvas() { }); setMouseCoords(newMouseCoords); - mouseMovement.current = { + mouseMovementRef.current = { x: newMouseCoords.x - oldMouseCoords.x, y: newMouseCoords.y - oldMouseCoords.y, }; @@ -226,24 +217,30 @@ function Canvas() { y: prevCoords.y + e.movementY, })); break; - case "rectangle-select": - setSelectionBoxBounds((prev) => { - const newBounds = { - ...prev, - maxX: mouseCoords.x + 1, - maxY: mouseCoords.y + 1, - }; + case "rectangle-select": { + const dragStartCoords = dragStartCoordsRef.current; + if (!dragStartCoords) return; - selectionBoxBoundsRef.current = newBounds; - return newBounds; + setSelectionCoords(() => { + const newSelection: CoordinateArray = []; + + // todo: fix dragging from bottom to top + for (let x = dragStartCoords.x; x < mouseCoords.x + 1; x++) { + for (let y = dragStartCoords.y; y < mouseCoords.y + 1; y++) { + newSelection.push([x, y]); + } + } + + return newSelection; }); break; + } } onToolUse(); } }, - [dragging, coords, scale, tool, mouseCoords, onToolUse, setCoords, setSelectionBoxBounds] + [dragging, coords, scale, tool, mouseCoords, onToolUse, setCoords, setSelectionCoords] ); const onMouseDown = useCallback(() => { @@ -251,18 +248,11 @@ function Canvas() { onToolUse(); updateCssCursor(); - if (tool == "rectangle-select") { - const newBounds = { - minX: mouseCoords.x, - minY: mouseCoords.y, - maxX: mouseCoords.x, - maxY: mouseCoords.y, - }; + dragStartCoordsRef.current = mouseCoords; - selectionBoxBoundsRef.current = newBounds; - setSelectionBoxBounds(newBounds); - } - }, [onToolUse, updateCssCursor, tool, setSelectionBoxBounds, mouseCoords]); + // Clear selection on click + if (tool === "rectangle-select") setSelectionCoords([]); + }, [onToolUse, updateCssCursor, mouseCoords, tool, setSelectionCoords]); const onMouseUp = useCallback(() => { setDragging(false); @@ -332,10 +322,7 @@ function Canvas() { setCssCursor("zoom-out"); break; case "Delete": { - if (!selectionBoxBoundsRef.current) return; - const bounds = selectionBoxBoundsRef.current; - - setBlocks((prev) => prev.filter((b) => !(b.x >= bounds.minX && b.x < bounds.maxX && b.y >= bounds.minY && b.y < bounds.maxY))); + setBlocks((prev) => prev.filter((b) => !selectionCoordsRef.current.some(([x2, y2]) => x2 === b.x && y2 === b.y))); break; } } @@ -356,6 +343,10 @@ function Canvas() { } }; + useEffect(() => { + selectionCoordsRef.current = selectionCoords; + }, [selectionCoords]); + useEffect(() => { const container = stageContainerRef.current; if (!container) return; @@ -421,7 +412,7 @@ function Canvas() { {settings.canvasBorder && } - + {settings.grid && ( diff --git a/src/components/canvas/SelectionBox.tsx b/src/components/canvas/SelectionBox.tsx index 2f889f2..ad764d6 100644 --- a/src/components/canvas/SelectionBox.tsx +++ b/src/components/canvas/SelectionBox.tsx @@ -3,7 +3,7 @@ import { useApp } from "@pixi/react"; import { DashLineShader, SmoothGraphics } from "@pixi/graphics-smooth"; interface Props { - bounds: BoundingBox; + selection: CoordinateArray; coords: Position; scale: number; isDark: boolean; @@ -11,7 +11,7 @@ interface Props { const shader = new DashLineShader({ dash: 8, gap: 5 }); -function SelectionBox({ bounds, coords, scale, isDark }: Props) { +function SelectionBox({ selection, coords, scale, isDark }: Props) { const app = useApp(); const selectionRef = useRef(); @@ -20,12 +20,14 @@ function SelectionBox({ bounds, coords, scale, isDark }: Props) { const graphics = selectionRef.current; graphics.clear(); graphics.lineStyle({ width: 1, color: isDark ? 0xffffff : 0x000000, shader }); - graphics.drawRect( - bounds.minX * 16 * scale, - bounds.minY * 16 * scale, - (bounds.maxX - bounds.minX) * 16 * scale, - (bounds.maxY - bounds.minY) * 16 * scale - ); + + selection.forEach(([x, y]) => { + const rectX = x * 16 * scale; + const rectY = y * 16 * scale; + + graphics.drawRect(rectX, rectY, 16 * scale, 16 * scale); + // todo: remove lines on adjacent rectangles + }); }; useEffect(() => { @@ -44,7 +46,7 @@ function SelectionBox({ bounds, coords, scale, isDark }: Props) { drawSelection(); }, [coords]); - useEffect(drawSelection, [bounds]); + useEffect(drawSelection, [selection]); return null; } diff --git a/src/components/menubar/EditMenu.tsx b/src/components/menubar/EditMenu.tsx index afbccc8..565b3f6 100644 --- a/src/components/menubar/EditMenu.tsx +++ b/src/components/menubar/EditMenu.tsx @@ -24,7 +24,7 @@ function EditMenu() { Cut - setSelectionBoxBounds({ minX: 0, minY: 0, maxX: 0, maxY: 0 })}>Clear Selection + setSelectionBoxBounds([])}>Clear Selection ); diff --git a/src/context/Tool.tsx b/src/context/Tool.tsx index b5b04c3..8f3e2f7 100644 --- a/src/context/Tool.tsx +++ b/src/context/Tool.tsx @@ -4,12 +4,12 @@ interface Context { tool: Tool; radius: number; selectedBlock: string; - selectionBoxBounds: BoundingBox; + selectionCoords: CoordinateArray; cssCursor: string; setTool: React.Dispatch>; setRadius: React.Dispatch>; setSelectedBlock: React.Dispatch>; - setSelectionBoxBounds: React.Dispatch>; + setSelectionCoords: React.Dispatch>; setCssCursor: React.Dispatch>; } @@ -23,7 +23,7 @@ export const ToolProvider = ({ children }: Props) => { const [tool, setTool] = useState("hand"); const [radius, setRadius] = useState(1); const [selectedBlock, setSelectedBlock] = useState("stone"); - const [selectionBoxBounds, setSelectionBoxBounds] = useState({ minX: 0, minY: 0, maxX: 0, maxY: 0 }); + const [selectionCoords, setSelectionCoords] = useState([]); const [cssCursor, setCssCursor] = useState("crosshair"); useEffect(() => { @@ -47,12 +47,12 @@ export const ToolProvider = ({ children }: Props) => { tool, radius, selectedBlock, - selectionBoxBounds, + selectionCoords, cssCursor, setTool, setRadius, setSelectedBlock, - setSelectionBoxBounds, + setSelectionCoords, setCssCursor, }} > diff --git a/src/types.d.ts b/src/types.d.ts index ac60dc7..fce134d 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -21,6 +21,8 @@ interface Block extends Position { name: string; } +type CoordinateArray = [number, number][]; + type Tool = "hand" | "move" | "rectangle-select" | "pencil" | "eraser" | "eyedropper" | "zoom"; interface Settings {