From 8e8c5680021183f7c9e0ebc1dec24f91afc4cea7 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Wed, 29 Jan 2025 21:47:59 +0000 Subject: [PATCH] feat: copy and paste selection --- src/components/canvas/Canvas.tsx | 56 ++++++++++++++++++++--------- src/components/menubar/EditMenu.tsx | 30 +++++++++++++--- src/utils/clipboard.ts | 37 +++++++++++++++++++ 3 files changed, 102 insertions(+), 21 deletions(-) create mode 100644 src/utils/clipboard.ts diff --git a/src/components/canvas/Canvas.tsx b/src/components/canvas/Canvas.tsx index cbf00a6..d2b71ad 100644 --- a/src/components/canvas/Canvas.tsx +++ b/src/components/canvas/Canvas.tsx @@ -12,7 +12,9 @@ import { ToolContext } from "@/context/Tool"; import { useTextures } from "@/hooks/useTextures"; import { useBlockData } from "@/hooks/useBlockData"; + import { confirmSelection, isInSelection } from "@/utils/selection"; +import * as clipboard from "@/utils/clipboard"; import Blocks from "./Blocks"; import Cursor from "./Cursor"; @@ -50,8 +52,8 @@ function Canvas() { const [dragging, setDragging] = useState(false); const dragStartCoordsRef = useRef(); - const holdingAltRef = useRef(false); const holdingShiftRef = useRef(false); + const holdingAltRef = useRef(false); const oldToolRef = useRef(); const [cssCursor, setCssCursor] = useState("crosshair"); @@ -126,7 +128,16 @@ function Canvas() { switch (tool) { case "move": { 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 (selectionLayerBlocks.length == 0) { @@ -397,7 +408,7 @@ function Canvas() { }, [tool, holdingAltRef, scale, mouseCoords, blocks, setSelectionCoords, setSelectedBlock, zoom]); const onKeyDown = useCallback( - (e: KeyboardEvent) => { + async (e: KeyboardEvent) => { switch (e.key) { case "Escape": setSelectionLayerBlocks([]); @@ -422,6 +433,16 @@ function Canvas() { setBlocks((prev) => prev.filter((b) => !selectionCoords.some(([x2, y2]) => x2 === b.x && y2 === b.y))); 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": setTool("hand"); break; @@ -449,19 +470,21 @@ function Canvas() { case "9": setTool("zoom"); break; - case "ArrowRight": - if (holdingAltRef.current && holdingShiftRef.current) { - const newBlocks: Block[] = []; + case "ArrowRight": { + // Debug key combination + if (!e.altKey && !e.shiftKey) return; - Object.keys(blockData).forEach((name, index) => { - const x = index % 16; - const y = Math.floor(index / 16); - newBlocks.push({ name, x, y }); - }); + const newBlocks: Block[] = []; - 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; + } } }, [tool, blocks, selectionCoords, selectionLayerBlocks, blockData, setBlocks, setCssCursor, setSelectionLayerBlocks, setTool] @@ -471,9 +494,10 @@ function Canvas() { (e: KeyboardEvent) => { switch (e.key) { case " ": // Space - if (!oldToolRef.current) return; setDragging(false); setCssCursor("grab"); + + if (!oldToolRef.current) return; setTool(oldToolRef.current); break; case "Shift": @@ -481,11 +505,11 @@ function Canvas() { break; case "Alt": holdingAltRef.current = false; - setCssCursor("zoom-in"); + if (tool === "zoom") setCssCursor("zoom-in"); break; } }, - [setCssCursor, setTool] + [setCssCursor, setTool, tool] ); // Tool cursor handler diff --git a/src/components/menubar/EditMenu.tsx b/src/components/menubar/EditMenu.tsx index e500340..76e50f2 100644 --- a/src/components/menubar/EditMenu.tsx +++ b/src/components/menubar/EditMenu.tsx @@ -2,12 +2,16 @@ import { useContext } from "react"; import { CanvasContext } from "@/context/Canvas"; 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() { - const { setBlocks } = useContext(CanvasContext); - const { coords: selectionCoords, setCoords: setSelectionCoords } = useContext(SelectionContext); + const { blocks, setBlocks } = useContext(CanvasContext); + const { coords: selectionCoords, setCoords: setSelectionCoords, setLayerBlocks: setSelectionLayerBlocks } = useContext(SelectionContext); + const { setTool } = useContext(ToolContext); const cut = () => { setBlocks((prev) => prev.filter((b) => !selectionCoords.some(([x2, y2]) => x2 === b.x && y2 === b.y))); @@ -17,8 +21,24 @@ function EditMenu() { Edit - Undo - Redo + + Undo + Ctrl Z + + + Redo + Ctrl Y + + + + clipboard.copy(selectionCoords, blocks)}> + Copy + Ctrl C + + clipboard.paste(setSelectionLayerBlocks, setSelectionCoords, setTool)}> + Paste + Ctrl V + Cut diff --git a/src/utils/clipboard.ts b/src/utils/clipboard.ts new file mode 100644 index 0000000..9a69fa8 --- /dev/null +++ b/src/utils/clipboard.ts @@ -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>, + setSelectionCoords: React.Dispatch>, + setTool: React.Dispatch> +) { + 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); + } +}