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;
|
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();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 { 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
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;
|
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,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue