feat: improved move tool
This commit is contained in:
parent
9873c068e8
commit
ee02e1ae47
7 changed files with 150 additions and 39 deletions
|
|
@ -12,12 +12,13 @@ interface Props {
|
|||
coords: Position;
|
||||
scale: number;
|
||||
version: number;
|
||||
isSelectionLayer?: boolean;
|
||||
}
|
||||
|
||||
// Lifts 16,000 tiles limit
|
||||
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 tilemapRef = useRef<CompositeTilemap>();
|
||||
|
||||
|
|
@ -33,10 +34,18 @@ function Blocks({ blocks, missingTexture, textures, coords, scale, version }: Pr
|
|||
};
|
||||
|
||||
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();
|
||||
tilemapRef.current = tilemap;
|
||||
app.stage.addChildAt(tilemap, 0);
|
||||
|
||||
if (isSelectionLayer) {
|
||||
container.filters = [new PIXI.AlphaFilter(0.5)];
|
||||
}
|
||||
|
||||
container.addChild(tilemap);
|
||||
tileBlocks();
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import * as PIXI from "pixi.js";
|
|||
import { Container, Stage } from "@pixi/react";
|
||||
|
||||
import { CanvasContext } from "@/context/Canvas";
|
||||
import { SelectionContext } from "@/context/Selection";
|
||||
import { SettingsContext } from "@/context/Settings";
|
||||
import { TexturesContext } from "@/context/Textures";
|
||||
import { ThemeContext } from "@/context/Theme";
|
||||
|
|
@ -19,17 +20,23 @@ import CanvasBorder from "./CanvasBorder";
|
|||
|
||||
import CursorInformation from "./information/Cursor";
|
||||
import CanvasInformation from "./information/Canvas";
|
||||
import SelectionToolbar from "./SelectionToolbar";
|
||||
|
||||
// Set scale mode to NEAREST
|
||||
PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
|
||||
|
||||
function Canvas() {
|
||||
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 { missingTexture } = useContext(TexturesContext);
|
||||
const { isDark } = useContext(ThemeContext);
|
||||
const { tool, radius, selectedBlock, selectionCoords, cssCursor, setTool, setSelectedBlock, setSelectionCoords, setCssCursor } =
|
||||
useContext(ToolContext);
|
||||
const { tool, radius, selectedBlock, cssCursor, setTool, setSelectedBlock, setCssCursor } = useContext(ToolContext);
|
||||
|
||||
const textures = useTextures(version);
|
||||
const stageContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -126,23 +133,27 @@ function Canvas() {
|
|||
const mouseMovement = mouseMovementRef.current;
|
||||
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
|
||||
setSelectionCoords((prev) => prev.map(([x, y]) => [x + mouseMovement.x, 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;
|
||||
})
|
||||
);
|
||||
setSelectionLayerBlocks((prev) => prev.map((b) => ({ ...b, x: b.x + mouseMovement.x, y: b.y + mouseMovement.y })));
|
||||
break;
|
||||
}
|
||||
case "lasso": {
|
||||
|
|
@ -205,7 +216,18 @@ function Canvas() {
|
|||
break;
|
||||
}
|
||||
}
|
||||
}, [tool, mouseCoords, selectedBlock, blocks, radius, selectionCoords, setSelectionCoords, setBlocks]);
|
||||
}, [
|
||||
tool,
|
||||
mouseCoords,
|
||||
selectedBlock,
|
||||
blocks,
|
||||
radius,
|
||||
selectionCoords,
|
||||
selectionLayerBlocks,
|
||||
setSelectionCoords,
|
||||
setSelectionLayerBlocks,
|
||||
setBlocks,
|
||||
]);
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
|
|
@ -485,6 +507,16 @@ function Canvas() {
|
|||
options={{ backgroundAlpha: 0 }}
|
||||
>
|
||||
<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}>
|
||||
{settings.canvasBorder && <CanvasBorder canvasSize={canvasSize} isDark={isDark} />}
|
||||
|
|
@ -501,6 +533,8 @@ function Canvas() {
|
|||
|
||||
<CursorInformation mouseCoords={mouseCoords} />
|
||||
<CanvasInformation />
|
||||
|
||||
<SelectionToolbar />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
38
src/components/canvas/SelectionToolbar.tsx
Normal file
38
src/components/canvas/SelectionToolbar.tsx
Normal 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;
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { useContext } from "react";
|
||||
|
||||
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";
|
||||
|
||||
function EditMenu() {
|
||||
const { setBlocks } = useContext(CanvasContext);
|
||||
const { selectionCoords, setSelectionCoords } = useContext(ToolContext);
|
||||
const { coords: selectionCoords, setCoords: setSelectionCoords } = useContext(SelectionContext);
|
||||
|
||||
const cut = () => {
|
||||
setBlocks((prev) => prev.filter((b) => !selectionCoords.some(([x2, y2]) => x2 === b.x && y2 === b.y)));
|
||||
|
|
|
|||
32
src/context/Selection.tsx
Normal file
32
src/context/Selection.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -4,12 +4,10 @@ interface Context {
|
|||
tool: Tool;
|
||||
radius: number;
|
||||
selectedBlock: string;
|
||||
selectionCoords: CoordinateArray;
|
||||
cssCursor: string;
|
||||
setTool: React.Dispatch<React.SetStateAction<Tool>>;
|
||||
setRadius: React.Dispatch<React.SetStateAction<number>>;
|
||||
setSelectedBlock: React.Dispatch<React.SetStateAction<string>>;
|
||||
setSelectionCoords: React.Dispatch<React.SetStateAction<CoordinateArray>>;
|
||||
setCssCursor: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
|
|
@ -23,7 +21,6 @@ export const ToolProvider = ({ children }: Props) => {
|
|||
const [tool, setTool] = useState<Tool>("hand");
|
||||
const [radius, setRadius] = useState(1);
|
||||
const [selectedBlock, setSelectedBlock] = useState("stone");
|
||||
const [selectionCoords, setSelectionCoords] = useState<CoordinateArray>([]);
|
||||
const [cssCursor, setCssCursor] = useState("crosshair");
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -42,12 +39,10 @@ export const ToolProvider = ({ children }: Props) => {
|
|||
tool,
|
||||
radius,
|
||||
selectedBlock,
|
||||
selectionCoords,
|
||||
cssCursor,
|
||||
setTool,
|
||||
setRadius,
|
||||
setSelectedBlock,
|
||||
setSelectionCoords,
|
||||
setCssCursor,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { CanvasProvider } from "@/context/Canvas";
|
||||
import { LoadingProvider } from "@/context/Loading";
|
||||
import { SelectionProvider } from "@/context/Selection";
|
||||
import { SettingsProvider } from "@/context/Settings";
|
||||
import { TexturesProvider } from "@/context/Textures";
|
||||
import { ToolProvider } from "@/context/Tool";
|
||||
|
|
@ -13,18 +14,20 @@ function AppPage() {
|
|||
return (
|
||||
<CanvasProvider>
|
||||
<LoadingProvider>
|
||||
<SettingsProvider>
|
||||
<TexturesProvider>
|
||||
<ToolProvider>
|
||||
<main className="h-screen grid grid-rows-[2.5rem_minmax(0,1fr)] grid-cols-[2.5rem_minmax(0,1fr)_auto]">
|
||||
<Menubar />
|
||||
<Toolbar />
|
||||
<Canvas />
|
||||
<ToolSettings />
|
||||
</main>
|
||||
</ToolProvider>
|
||||
</TexturesProvider>
|
||||
</SettingsProvider>
|
||||
<SelectionProvider>
|
||||
<SettingsProvider>
|
||||
<TexturesProvider>
|
||||
<ToolProvider>
|
||||
<main className="h-screen grid grid-rows-[2.5rem_minmax(0,1fr)] grid-cols-[2.5rem_minmax(0,1fr)_auto]">
|
||||
<Menubar />
|
||||
<Toolbar />
|
||||
<Canvas />
|
||||
<ToolSettings />
|
||||
</main>
|
||||
</ToolProvider>
|
||||
</TexturesProvider>
|
||||
</SettingsProvider>
|
||||
</SelectionProvider>
|
||||
</LoadingProvider>
|
||||
</CanvasProvider>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue