refactor: use coordinate array for selection
allows upcoming feature for users to select non-rectangular blocks
This commit is contained in:
parent
2037ad8722
commit
527e29c448
5 changed files with 70 additions and 75 deletions
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
2
src/types.d.ts
vendored
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue