feat: copy and paste selection

This commit is contained in:
trafficlunar 2025-01-29 21:47:59 +00:00
parent fd8f4aaca1
commit 8e8c568002
3 changed files with 102 additions and 21 deletions

View file

@ -12,7 +12,9 @@ import { ToolContext } from "@/context/Tool";
import { useTextures } from "@/hooks/useTextures"; import { useTextures } from "@/hooks/useTextures";
import { useBlockData } from "@/hooks/useBlockData"; import { useBlockData } from "@/hooks/useBlockData";
import { confirmSelection, isInSelection } from "@/utils/selection"; import { confirmSelection, isInSelection } from "@/utils/selection";
import * as clipboard from "@/utils/clipboard";
import Blocks from "./Blocks"; import Blocks from "./Blocks";
import Cursor from "./Cursor"; import Cursor from "./Cursor";
@ -50,8 +52,8 @@ function Canvas() {
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
const dragStartCoordsRef = useRef<Position>(); const dragStartCoordsRef = useRef<Position>();
const holdingAltRef = useRef(false);
const holdingShiftRef = useRef(false); const holdingShiftRef = useRef(false);
const holdingAltRef = useRef(false);
const oldToolRef = useRef<Tool>(); const oldToolRef = useRef<Tool>();
const [cssCursor, setCssCursor] = useState("crosshair"); const [cssCursor, setCssCursor] = useState("crosshair");
@ -126,7 +128,16 @@ function Canvas() {
switch (tool) { switch (tool) {
case "move": { case "move": {
const mouseMovement = mouseMovementRef.current; const mouseMovement = mouseMovementRef.current;
if (!mouseMovement) return; if (!mouseMovement) return; // Get all blocks within selection
const selectorBlocks = selectionCoords
.map((coord) => {
const [x, y] = coord;
return blocks.find((block) => block.x === x && block.y === y);
})
.filter((block) => block !== undefined);
// Write to clipboard
navigator.clipboard.writeText(JSON.stringify(selectorBlocks));
// If there is no selection currently being moved... // If there is no selection currently being moved...
if (selectionLayerBlocks.length == 0) { if (selectionLayerBlocks.length == 0) {
@ -397,7 +408,7 @@ function Canvas() {
}, [tool, holdingAltRef, scale, mouseCoords, blocks, setSelectionCoords, setSelectedBlock, zoom]); }, [tool, holdingAltRef, scale, mouseCoords, blocks, setSelectionCoords, setSelectedBlock, zoom]);
const onKeyDown = useCallback( const onKeyDown = useCallback(
(e: KeyboardEvent) => { async (e: KeyboardEvent) => {
switch (e.key) { switch (e.key) {
case "Escape": case "Escape":
setSelectionLayerBlocks([]); setSelectionLayerBlocks([]);
@ -422,6 +433,16 @@ function Canvas() {
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)));
break; break;
} }
case "c": {
if (!e.ctrlKey) return;
clipboard.copy(selectionCoords, blocks);
break;
}
case "v": {
if (!e.ctrlKey) return;
clipboard.paste(setSelectionLayerBlocks, setSelectionCoords, setTool);
break;
}
case "1": case "1":
setTool("hand"); setTool("hand");
break; break;
@ -449,19 +470,21 @@ function Canvas() {
case "9": case "9":
setTool("zoom"); setTool("zoom");
break; break;
case "ArrowRight": case "ArrowRight": {
if (holdingAltRef.current && holdingShiftRef.current) { // Debug key combination
const newBlocks: Block[] = []; if (!e.altKey && !e.shiftKey) return;
Object.keys(blockData).forEach((name, index) => { const newBlocks: Block[] = [];
const x = index % 16;
const y = Math.floor(index / 16);
newBlocks.push({ name, x, y });
});
setBlocks(newBlocks); Object.keys(blockData).forEach((name, index) => {
} const x = index % 16;
const y = Math.floor(index / 16);
newBlocks.push({ name, x, y });
});
setBlocks(newBlocks);
break; break;
}
} }
}, },
[tool, blocks, selectionCoords, selectionLayerBlocks, blockData, setBlocks, setCssCursor, setSelectionLayerBlocks, setTool] [tool, blocks, selectionCoords, selectionLayerBlocks, blockData, setBlocks, setCssCursor, setSelectionLayerBlocks, setTool]
@ -471,9 +494,10 @@ function Canvas() {
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
switch (e.key) { switch (e.key) {
case " ": // Space case " ": // Space
if (!oldToolRef.current) return;
setDragging(false); setDragging(false);
setCssCursor("grab"); setCssCursor("grab");
if (!oldToolRef.current) return;
setTool(oldToolRef.current); setTool(oldToolRef.current);
break; break;
case "Shift": case "Shift":
@ -481,11 +505,11 @@ function Canvas() {
break; break;
case "Alt": case "Alt":
holdingAltRef.current = false; holdingAltRef.current = false;
setCssCursor("zoom-in"); if (tool === "zoom") setCssCursor("zoom-in");
break; break;
} }
}, },
[setCssCursor, setTool] [setCssCursor, setTool, tool]
); );
// Tool cursor handler // Tool cursor handler

View file

@ -2,12 +2,16 @@ import { useContext } from "react";
import { CanvasContext } from "@/context/Canvas"; import { CanvasContext } from "@/context/Canvas";
import { SelectionContext } from "@/context/Selection"; import { SelectionContext } from "@/context/Selection";
import { ToolContext } from "@/context/Tool";
import { MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarTrigger } from "@/components/ui/menubar"; import * as clipboard from "@/utils/clipboard";
import { MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarShortcut, MenubarTrigger } from "@/components/ui/menubar";
function EditMenu() { function EditMenu() {
const { setBlocks } = useContext(CanvasContext); const { blocks, setBlocks } = useContext(CanvasContext);
const { coords: selectionCoords, setCoords: setSelectionCoords } = useContext(SelectionContext); const { coords: selectionCoords, setCoords: setSelectionCoords, setLayerBlocks: setSelectionLayerBlocks } = useContext(SelectionContext);
const { setTool } = useContext(ToolContext);
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)));
@ -17,8 +21,24 @@ function EditMenu() {
<MenubarMenu> <MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger> <MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent> <MenubarContent>
<MenubarItem>Undo</MenubarItem> <MenubarItem>
<MenubarItem>Redo</MenubarItem> Undo
<MenubarShortcut>Ctrl Z</MenubarShortcut>
</MenubarItem>
<MenubarItem>
Redo
<MenubarShortcut>Ctrl Y</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={() => clipboard.copy(selectionCoords, blocks)}>
Copy
<MenubarShortcut>Ctrl C</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={() => clipboard.paste(setSelectionLayerBlocks, setSelectionCoords, setTool)}>
Paste
<MenubarShortcut>Ctrl V</MenubarShortcut>
</MenubarItem>
<MenubarSeparator /> <MenubarSeparator />
<MenubarItem onClick={cut}>Cut</MenubarItem> <MenubarItem onClick={cut}>Cut</MenubarItem>

37
src/utils/clipboard.ts Normal file
View file

@ -0,0 +1,37 @@
export function copy(selectionCoords: CoordinateArray, blocks: Block[]) {
// Get all blocks within selection
const selectorBlocks = selectionCoords
.map((coord) => {
const [x, y] = coord;
return blocks.find((block) => block.x === x && block.y === y);
})
.filter((block) => block !== undefined);
// Write to clipboard
navigator.clipboard.writeText(JSON.stringify(selectorBlocks));
}
export async function paste(
setSelectionLayerBlocks: React.Dispatch<React.SetStateAction<Block[]>>,
setSelectionCoords: React.Dispatch<React.SetStateAction<CoordinateArray>>,
setTool: React.Dispatch<React.SetStateAction<Tool>>
) {
try {
// Read clipboard then parse it
const clipboardText = await navigator.clipboard.readText();
const clipboardBlocks = JSON.parse(clipboardText);
// Check if pasted object is of type Block[]
if (
!Array.isArray(clipboardBlocks) ||
!clipboardBlocks.every((block) => typeof block.x === "number" && typeof block.y === "number" && typeof block.name === "string")
)
return;
setSelectionLayerBlocks(clipboardBlocks);
setSelectionCoords(clipboardBlocks.map((block) => [block.x, block.y]));
setTool("move");
} catch (error) {
console.error("Failed to read/parse clipboard:", error);
}
}