feat: copy and paste selection
This commit is contained in:
parent
fd8f4aaca1
commit
8e8c568002
3 changed files with 102 additions and 21 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
37
src/utils/clipboard.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue