refactor: use coordinate array for selection

allows upcoming feature for users to select non-rectangular blocks
This commit is contained in:
trafficlunar 2025-01-18 21:59:42 +00:00
parent 2037ad8722
commit 527e29c448
5 changed files with 70 additions and 75 deletions

View file

@ -30,7 +30,7 @@ function Canvas() {
const { settings } = useContext(SettingsContext); const { settings } = useContext(SettingsContext);
const { missingTexture, solidTextures } = useContext(TexturesContext); const { missingTexture, solidTextures } = useContext(TexturesContext);
const { isDark } = useContext(ThemeContext); 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); useContext(ToolContext);
const textures = useTextures(version); const textures = useTextures(version);
@ -38,12 +38,13 @@ 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 mouseMovement = useRef<Position>(); const mouseMovementRef = useRef<Position>();
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
const dragStartCoordsRef = useRef<Position>();
const [holdingAlt, setHoldingAlt] = useState(false); const [holdingAlt, setHoldingAlt] = useState(false);
const selectionBoxBoundsRef = useRef<BoundingBox>();
const oldToolRef = useRef<Tool>(); const oldToolRef = useRef<Tool>();
const selectionCoordsRef = useRef<CoordinateArray>(selectionCoords);
const visibleArea = useMemo(() => { const visibleArea = useMemo(() => {
const blockSize = 16 * scale; const blockSize = 16 * scale;
@ -97,9 +98,12 @@ function Canvas() {
return { x, y }; return { x, y };
}; };
// Check if a block is within the selection bounds // Check if a block is within the selection
const isInSelection = (x: number, y: number) => { const isInSelection = (x: number, y: number): boolean => {
return x >= selectionBoxBounds.minX && x < selectionBoxBounds.maxX && y >= selectionBoxBounds.minY && y < selectionBoxBounds.maxY; if (selectionCoords.length !== 0) {
return selectionCoords.some(([x2, y2]) => x2 === x && y2 === y);
}
return false;
}; };
const eraseTool = () => { const eraseTool = () => {
@ -118,39 +122,26 @@ function Canvas() {
switch (tool) { switch (tool) {
case "move": { case "move": {
if (!mouseMovement.current) return; const mouseMovement = mouseMovementRef.current;
const { x: movementX, y: movementY } = mouseMovement.current; if (!mouseMovement) return;
setSelectionBoxBounds((prev) => { // Increase each coordinate in the selection by the mouse movement
const newBounds = { setSelectionCoords((prev) => prev.map(([x, y]) => [x + mouseMovement.x, y + mouseMovement.y]));
minX: prev.minX + movementX,
minY: prev.minY + movementY,
maxX: prev.maxX + movementX,
maxY: prev.maxY + movementY,
};
selectionBoxBoundsRef.current = newBounds; // Increase each block in the selection by the mouse movement
return newBounds; setBlocks((prev) =>
}); prev.map((block) => {
if (isInSelection(block.x, block.y)) {
setBlocks((prev) => {
return prev.map((block) => {
if (
block.x >= selectionBoxBounds.minX &&
block.x < selectionBoxBounds.maxX &&
block.y >= selectionBoxBounds.minY &&
block.y < selectionBoxBounds.maxY
) {
return { return {
...block, ...block,
x: block.x + movementX, x: block.x + mouseMovement.x,
y: block.y + movementY, y: block.y + mouseMovement.y,
}; };
} }
return block;
});
});
return block;
})
);
break; break;
} }
case "pencil": { case "pencil": {
@ -190,7 +181,7 @@ function Canvas() {
break; break;
} }
} }
}, [tool, mouseCoords, selectedBlock, blocks, radius, selectionBoxBounds, setBlocks]); }, [tool, mouseCoords, selectedBlock, blocks, radius, selectionCoords, setSelectionCoords, setBlocks]);
const onMouseMove = useCallback( const onMouseMove = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
@ -213,7 +204,7 @@ function Canvas() {
}); });
setMouseCoords(newMouseCoords); setMouseCoords(newMouseCoords);
mouseMovement.current = { mouseMovementRef.current = {
x: newMouseCoords.x - oldMouseCoords.x, x: newMouseCoords.x - oldMouseCoords.x,
y: newMouseCoords.y - oldMouseCoords.y, y: newMouseCoords.y - oldMouseCoords.y,
}; };
@ -226,24 +217,30 @@ function Canvas() {
y: prevCoords.y + e.movementY, y: prevCoords.y + e.movementY,
})); }));
break; break;
case "rectangle-select": case "rectangle-select": {
setSelectionBoxBounds((prev) => { const dragStartCoords = dragStartCoordsRef.current;
const newBounds = { if (!dragStartCoords) return;
...prev,
maxX: mouseCoords.x + 1,
maxY: mouseCoords.y + 1,
};
selectionBoxBoundsRef.current = newBounds; setSelectionCoords(() => {
return newBounds; 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; break;
}
} }
onToolUse(); onToolUse();
} }
}, },
[dragging, coords, scale, tool, mouseCoords, onToolUse, setCoords, setSelectionBoxBounds] [dragging, coords, scale, tool, mouseCoords, onToolUse, setCoords, setSelectionCoords]
); );
const onMouseDown = useCallback(() => { const onMouseDown = useCallback(() => {
@ -251,18 +248,11 @@ function Canvas() {
onToolUse(); onToolUse();
updateCssCursor(); updateCssCursor();
if (tool == "rectangle-select") { dragStartCoordsRef.current = mouseCoords;
const newBounds = {
minX: mouseCoords.x,
minY: mouseCoords.y,
maxX: mouseCoords.x,
maxY: mouseCoords.y,
};
selectionBoxBoundsRef.current = newBounds; // Clear selection on click
setSelectionBoxBounds(newBounds); if (tool === "rectangle-select") setSelectionCoords([]);
} }, [onToolUse, updateCssCursor, mouseCoords, tool, setSelectionCoords]);
}, [onToolUse, updateCssCursor, tool, setSelectionBoxBounds, mouseCoords]);
const onMouseUp = useCallback(() => { const onMouseUp = useCallback(() => {
setDragging(false); setDragging(false);
@ -332,10 +322,7 @@ function Canvas() {
setCssCursor("zoom-out"); setCssCursor("zoom-out");
break; break;
case "Delete": { case "Delete": {
if (!selectionBoxBoundsRef.current) return; setBlocks((prev) => prev.filter((b) => !selectionCoordsRef.current.some(([x2, y2]) => x2 === b.x && y2 === b.y)));
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)));
break; break;
} }
} }
@ -356,6 +343,10 @@ function Canvas() {
} }
}; };
useEffect(() => {
selectionCoordsRef.current = selectionCoords;
}, [selectionCoords]);
useEffect(() => { useEffect(() => {
const container = stageContainerRef.current; const container = stageContainerRef.current;
if (!container) return; if (!container) return;
@ -421,7 +412,7 @@ function Canvas() {
<Container x={coords.x} y={coords.y} scale={scale}> <Container x={coords.x} y={coords.y} scale={scale}>
{settings.canvasBorder && <CanvasBorder canvasSize={canvasSize} isDark={isDark} />} {settings.canvasBorder && <CanvasBorder canvasSize={canvasSize} isDark={isDark} />}
<Cursor mouseCoords={mouseCoords} radius={radius} isDark={isDark} /> <Cursor mouseCoords={mouseCoords} radius={radius} isDark={isDark} />
<SelectionBox bounds={selectionBoxBounds} coords={coords} scale={scale} isDark={isDark} /> <SelectionBox selection={selectionCoords} coords={coords} scale={scale} isDark={isDark} />
</Container> </Container>
{settings.grid && ( {settings.grid && (

View file

@ -3,7 +3,7 @@ import { useApp } from "@pixi/react";
import { DashLineShader, SmoothGraphics } from "@pixi/graphics-smooth"; import { DashLineShader, SmoothGraphics } from "@pixi/graphics-smooth";
interface Props { interface Props {
bounds: BoundingBox; selection: CoordinateArray;
coords: Position; coords: Position;
scale: number; scale: number;
isDark: boolean; isDark: boolean;
@ -11,7 +11,7 @@ interface Props {
const shader = new DashLineShader({ dash: 8, gap: 5 }); 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 app = useApp();
const selectionRef = useRef<SmoothGraphics>(); const selectionRef = useRef<SmoothGraphics>();
@ -20,12 +20,14 @@ function SelectionBox({ bounds, coords, scale, isDark }: Props) {
const graphics = selectionRef.current; const graphics = selectionRef.current;
graphics.clear(); graphics.clear();
graphics.lineStyle({ width: 1, color: isDark ? 0xffffff : 0x000000, shader }); graphics.lineStyle({ width: 1, color: isDark ? 0xffffff : 0x000000, shader });
graphics.drawRect(
bounds.minX * 16 * scale, selection.forEach(([x, y]) => {
bounds.minY * 16 * scale, const rectX = x * 16 * scale;
(bounds.maxX - bounds.minX) * 16 * scale, const rectY = y * 16 * scale;
(bounds.maxY - bounds.minY) * 16 * scale
); graphics.drawRect(rectX, rectY, 16 * scale, 16 * scale);
// todo: remove lines on adjacent rectangles
});
}; };
useEffect(() => { useEffect(() => {
@ -44,7 +46,7 @@ function SelectionBox({ bounds, coords, scale, isDark }: Props) {
drawSelection(); drawSelection();
}, [coords]); }, [coords]);
useEffect(drawSelection, [bounds]); useEffect(drawSelection, [selection]);
return null; return null;
} }

View file

@ -24,7 +24,7 @@ function EditMenu() {
<MenubarItem onClick={cut}>Cut</MenubarItem> <MenubarItem onClick={cut}>Cut</MenubarItem>
<MenubarSeparator /> <MenubarSeparator />
<MenubarItem onClick={() => setSelectionBoxBounds({ minX: 0, minY: 0, maxX: 0, maxY: 0 })}>Clear Selection</MenubarItem> <MenubarItem onClick={() => setSelectionBoxBounds([])}>Clear Selection</MenubarItem>
</MenubarContent> </MenubarContent>
</MenubarMenu> </MenubarMenu>
); );

View file

@ -4,12 +4,12 @@ interface Context {
tool: Tool; tool: Tool;
radius: number; radius: number;
selectedBlock: string; selectedBlock: string;
selectionBoxBounds: BoundingBox; selectionCoords: CoordinateArray;
cssCursor: string; cssCursor: string;
setTool: React.Dispatch<React.SetStateAction<Tool>>; setTool: React.Dispatch<React.SetStateAction<Tool>>;
setRadius: React.Dispatch<React.SetStateAction<number>>; setRadius: React.Dispatch<React.SetStateAction<number>>;
setSelectedBlock: React.Dispatch<React.SetStateAction<string>>; setSelectedBlock: React.Dispatch<React.SetStateAction<string>>;
setSelectionBoxBounds: React.Dispatch<React.SetStateAction<BoundingBox>>; setSelectionCoords: React.Dispatch<React.SetStateAction<CoordinateArray>>;
setCssCursor: React.Dispatch<React.SetStateAction<string>>; setCssCursor: React.Dispatch<React.SetStateAction<string>>;
} }
@ -23,7 +23,7 @@ export const ToolProvider = ({ children }: Props) => {
const [tool, setTool] = useState<Tool>("hand"); const [tool, setTool] = useState<Tool>("hand");
const [radius, setRadius] = useState(1); const [radius, setRadius] = useState(1);
const [selectedBlock, setSelectedBlock] = useState("stone"); const [selectedBlock, setSelectedBlock] = useState("stone");
const [selectionBoxBounds, setSelectionBoxBounds] = useState<BoundingBox>({ minX: 0, minY: 0, maxX: 0, maxY: 0 }); const [selectionCoords, setSelectionCoords] = useState<CoordinateArray>([]);
const [cssCursor, setCssCursor] = useState("crosshair"); const [cssCursor, setCssCursor] = useState("crosshair");
useEffect(() => { useEffect(() => {
@ -47,12 +47,12 @@ export const ToolProvider = ({ children }: Props) => {
tool, tool,
radius, radius,
selectedBlock, selectedBlock,
selectionBoxBounds, selectionCoords,
cssCursor, cssCursor,
setTool, setTool,
setRadius, setRadius,
setSelectedBlock, setSelectedBlock,
setSelectionBoxBounds, setSelectionCoords,
setCssCursor, setCssCursor,
}} }}
> >

2
src/types.d.ts vendored
View file

@ -21,6 +21,8 @@ interface Block extends Position {
name: string; name: string;
} }
type CoordinateArray = [number, number][];
type Tool = "hand" | "move" | "rectangle-select" | "pencil" | "eraser" | "eyedropper" | "zoom"; type Tool = "hand" | "move" | "rectangle-select" | "pencil" | "eraser" | "eyedropper" | "zoom";
interface Settings { interface Settings {