feat: improved move tool

This commit is contained in:
trafficlunar 2025-01-24 13:23:34 +00:00
parent 9873c068e8
commit ee02e1ae47
7 changed files with 150 additions and 39 deletions

View file

@ -12,12 +12,13 @@ interface Props {
coords: Position; coords: Position;
scale: number; scale: number;
version: number; version: number;
isSelectionLayer?: boolean;
} }
// Lifts 16,000 tiles limit // Lifts 16,000 tiles limit
settings.use32bitIndex = true; settings.use32bitIndex = true;
function Blocks({ blocks, missingTexture, textures, coords, scale, version }: Props) { function Blocks({ blocks, missingTexture, textures, coords, scale, version, isSelectionLayer }: Props) {
const app = useApp(); const app = useApp();
const tilemapRef = useRef<CompositeTilemap>(); const tilemapRef = useRef<CompositeTilemap>();
@ -33,10 +34,18 @@ function Blocks({ blocks, missingTexture, textures, coords, scale, version }: Pr
}; };
useEffect(() => { useEffect(() => {
const container = new PIXI.Container();
// Put selection layer on top of the blocks layer
app.stage.addChildAt(container, isSelectionLayer ? 1 : 0);
const tilemap = new CompositeTilemap(); const tilemap = new CompositeTilemap();
tilemapRef.current = tilemap; tilemapRef.current = tilemap;
app.stage.addChildAt(tilemap, 0);
if (isSelectionLayer) {
container.filters = [new PIXI.AlphaFilter(0.5)];
}
container.addChild(tilemap);
tileBlocks(); tileBlocks();
}, []); }, []);

View file

@ -4,6 +4,7 @@ import * as PIXI from "pixi.js";
import { Container, Stage } from "@pixi/react"; import { Container, Stage } from "@pixi/react";
import { CanvasContext } from "@/context/Canvas"; import { CanvasContext } from "@/context/Canvas";
import { SelectionContext } from "@/context/Selection";
import { SettingsContext } from "@/context/Settings"; import { SettingsContext } from "@/context/Settings";
import { TexturesContext } from "@/context/Textures"; import { TexturesContext } from "@/context/Textures";
import { ThemeContext } from "@/context/Theme"; import { ThemeContext } from "@/context/Theme";
@ -19,17 +20,23 @@ import CanvasBorder from "./CanvasBorder";
import CursorInformation from "./information/Cursor"; import CursorInformation from "./information/Cursor";
import CanvasInformation from "./information/Canvas"; import CanvasInformation from "./information/Canvas";
import SelectionToolbar from "./SelectionToolbar";
// Set scale mode to NEAREST // Set scale mode to NEAREST
PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
function Canvas() { function Canvas() {
const { stageSize, canvasSize, blocks, coords, scale, version, setStageSize, setBlocks, setCoords, setScale } = useContext(CanvasContext); const { stageSize, canvasSize, blocks, coords, scale, version, setStageSize, setBlocks, setCoords, setScale } = useContext(CanvasContext);
const {
coords: selectionCoords,
layerBlocks: selectionLayerBlocks,
setCoords: setSelectionCoords,
setLayerBlocks: setSelectionLayerBlocks,
} = useContext(SelectionContext);
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, selectionCoords, cssCursor, setTool, setSelectedBlock, setSelectionCoords, setCssCursor } = const { tool, radius, selectedBlock, cssCursor, setTool, setSelectedBlock, setCssCursor } = useContext(ToolContext);
useContext(ToolContext);
const textures = useTextures(version); const textures = useTextures(version);
const stageContainerRef = useRef<HTMLDivElement>(null); const stageContainerRef = useRef<HTMLDivElement>(null);
@ -126,23 +133,27 @@ function Canvas() {
const mouseMovement = mouseMovementRef.current; const mouseMovement = mouseMovementRef.current;
if (!mouseMovement) return; 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 = selectionCoords.some(([x, y]) => b.x === x && b.y === 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 // Increase each coordinate in the selection by the mouse movement
setSelectionCoords((prev) => prev.map(([x, y]) => [x + mouseMovement.x, y + mouseMovement.y])); 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 })));
// 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 + mouseMovement.x,
y: block.y + mouseMovement.y,
};
}
return block;
})
);
break; break;
} }
case "lasso": { case "lasso": {
@ -205,7 +216,18 @@ function Canvas() {
break; break;
} }
} }
}, [tool, mouseCoords, selectedBlock, blocks, radius, selectionCoords, setSelectionCoords, setBlocks]); }, [
tool,
mouseCoords,
selectedBlock,
blocks,
radius,
selectionCoords,
selectionLayerBlocks,
setSelectionCoords,
setSelectionLayerBlocks,
setBlocks,
]);
const onMouseMove = useCallback( const onMouseMove = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
@ -485,6 +507,16 @@ function Canvas() {
options={{ backgroundAlpha: 0 }} options={{ backgroundAlpha: 0 }}
> >
<Blocks blocks={visibleBlocks} missingTexture={missingTexture} textures={textures} coords={coords} scale={scale} version={version} /> <Blocks blocks={visibleBlocks} missingTexture={missingTexture} textures={textures} coords={coords} scale={scale} version={version} />
{/* Selection layer */}
<Blocks
isSelectionLayer
blocks={selectionLayerBlocks}
missingTexture={missingTexture}
textures={textures}
coords={coords}
scale={scale}
version={version}
/>
<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} />}
@ -501,6 +533,8 @@ function Canvas() {
<CursorInformation mouseCoords={mouseCoords} /> <CursorInformation mouseCoords={mouseCoords} />
<CanvasInformation /> <CanvasInformation />
<SelectionToolbar />
</div> </div>
); );
} }

View file

@ -0,0 +1,38 @@
import { useContext } from "react";
import { CheckIcon, XIcon } from "lucide-react";
import { CanvasContext } from "@/context/Canvas";
import { SelectionContext } from "@/context/Selection";
import { Button } from "@/components/ui/button";
function SelectionToolbar() {
const { blocks, setBlocks } = useContext(CanvasContext);
const { layerBlocks, setLayerBlocks } = useContext(SelectionContext);
const confirmSelection = () => {
const combinedBlocks = [...blocks, ...layerBlocks];
const uniqueBlocks = Array.from(new Map(combinedBlocks.map((block) => [`${block.x},${block.y}`, block])).values());
setBlocks(uniqueBlocks);
setLayerBlocks([]);
};
return (
layerBlocks.length != 0 && (
<div className="absolute left-1/2 -translate-x-1/2 bottom-4 flex items-center bg-white dark:bg-zinc-950 rounded shadow-xl border border-zinc-200 dark:border-zinc-800">
<span className="mr-4 ml-2">Selection</span>
{/* todo: place back blocks removed */}
<Button variant="ghost" className="w-8 h-8" onClick={() => setLayerBlocks([])}>
<XIcon />
</Button>
<Button variant="ghost" className="w-8 h-8" onClick={confirmSelection}>
<CheckIcon />
</Button>
</div>
)
);
}
export default SelectionToolbar;

View file

@ -1,13 +1,13 @@
import { useContext } from "react"; import { useContext } from "react";
import { CanvasContext } from "@/context/Canvas"; import { CanvasContext } from "@/context/Canvas";
import { ToolContext } from "@/context/Tool"; import { SelectionContext } from "@/context/Selection";
import { MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarTrigger } from "@/components/ui/menubar"; import { MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarTrigger } from "@/components/ui/menubar";
function EditMenu() { function EditMenu() {
const { setBlocks } = useContext(CanvasContext); const { setBlocks } = useContext(CanvasContext);
const { selectionCoords, setSelectionCoords } = useContext(ToolContext); const { coords: selectionCoords, setCoords: setSelectionCoords } = useContext(SelectionContext);
const cut = () => { const cut = () => {
setBlocks((prev) => prev.filter((b) => !selectionCoords.some(([x2, y2]) => x2 === b.x && y2 === b.y))); setBlocks((prev) => prev.filter((b) => !selectionCoords.some(([x2, y2]) => x2 === b.x && y2 === b.y)));

32
src/context/Selection.tsx Normal file
View file

@ -0,0 +1,32 @@
import { createContext, ReactNode, useState } from "react";
interface Context {
coords: CoordinateArray;
layerBlocks: Block[];
setCoords: React.Dispatch<React.SetStateAction<CoordinateArray>>;
setLayerBlocks: React.Dispatch<React.SetStateAction<Block[]>>;
}
interface Props {
children: ReactNode;
}
export const SelectionContext = createContext<Context>({} as Context);
export const SelectionProvider = ({ children }: Props) => {
const [coords, setCoords] = useState<CoordinateArray>([]);
const [layerBlocks, setLayerBlocks] = useState<Block[]>([]);
return (
<SelectionContext.Provider
value={{
coords,
layerBlocks,
setCoords,
setLayerBlocks,
}}
>
{children}
</SelectionContext.Provider>
);
};

View file

@ -4,12 +4,10 @@ interface Context {
tool: Tool; tool: Tool;
radius: number; radius: number;
selectedBlock: string; selectedBlock: string;
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>>;
setSelectionCoords: React.Dispatch<React.SetStateAction<CoordinateArray>>;
setCssCursor: React.Dispatch<React.SetStateAction<string>>; setCssCursor: React.Dispatch<React.SetStateAction<string>>;
} }
@ -23,7 +21,6 @@ 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 [selectionCoords, setSelectionCoords] = useState<CoordinateArray>([]);
const [cssCursor, setCssCursor] = useState("crosshair"); const [cssCursor, setCssCursor] = useState("crosshair");
useEffect(() => { useEffect(() => {
@ -42,12 +39,10 @@ export const ToolProvider = ({ children }: Props) => {
tool, tool,
radius, radius,
selectedBlock, selectedBlock,
selectionCoords,
cssCursor, cssCursor,
setTool, setTool,
setRadius, setRadius,
setSelectedBlock, setSelectedBlock,
setSelectionCoords,
setCssCursor, setCssCursor,
}} }}
> >

View file

@ -1,5 +1,6 @@
import { CanvasProvider } from "@/context/Canvas"; import { CanvasProvider } from "@/context/Canvas";
import { LoadingProvider } from "@/context/Loading"; import { LoadingProvider } from "@/context/Loading";
import { SelectionProvider } from "@/context/Selection";
import { SettingsProvider } from "@/context/Settings"; import { SettingsProvider } from "@/context/Settings";
import { TexturesProvider } from "@/context/Textures"; import { TexturesProvider } from "@/context/Textures";
import { ToolProvider } from "@/context/Tool"; import { ToolProvider } from "@/context/Tool";
@ -13,18 +14,20 @@ function AppPage() {
return ( return (
<CanvasProvider> <CanvasProvider>
<LoadingProvider> <LoadingProvider>
<SettingsProvider> <SelectionProvider>
<TexturesProvider> <SettingsProvider>
<ToolProvider> <TexturesProvider>
<main className="h-screen grid grid-rows-[2.5rem_minmax(0,1fr)] grid-cols-[2.5rem_minmax(0,1fr)_auto]"> <ToolProvider>
<Menubar /> <main className="h-screen grid grid-rows-[2.5rem_minmax(0,1fr)] grid-cols-[2.5rem_minmax(0,1fr)_auto]">
<Toolbar /> <Menubar />
<Canvas /> <Toolbar />
<ToolSettings /> <Canvas />
</main> <ToolSettings />
</ToolProvider> </main>
</TexturesProvider> </ToolProvider>
</SettingsProvider> </TexturesProvider>
</SettingsProvider>
</SelectionProvider>
</LoadingProvider> </LoadingProvider>
</CanvasProvider> </CanvasProvider>
); );