Skip to content

Commit 7f7e6d5

Browse files
committed
feat: copy paste, duplicate
1 parent d811eda commit 7f7e6d5

3 files changed

Lines changed: 379 additions & 1 deletion

File tree

src/NodeCanvas.jsx

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import HoverVisionAid from './components/HoverVisionAid.jsx'; // Import the Hove
2020
import { getNodeDimensions } from './utils.js';
2121
import { getTextColor, hexToHsl } from './utils/colorUtils.js';
2222
import { getPrototypeIdFromItem } from './utils/abstraction.js';
23+
import { copySelection, pasteClipboard } from './utils/clipboard.js';
2324
import { analyzeNodeDistribution, getClusterBoundingBox } from './utils/clusterAnalysis.js';
2425
import { v4 as uuidv4 } from 'uuid'; // Import UUID generator
2526
import { Edit3, Trash2, Link, Package, PackageOpen, Expand, ArrowUpFromDot, Triangle, Layers, ArrowLeft, SendToBack, ArrowBigRightDash, Palette, MoreHorizontal, Bookmark, Plus, CornerUpLeft, CornerDownLeft, Merge, Undo2, Clock } from 'lucide-react'; // Icons for PieMenu
@@ -1832,6 +1833,9 @@ function NodeCanvas() {
18321833
// --- Local UI State (Keep these) ---
18331834
const [selectedInstanceIds, setSelectedInstanceIds] = useState(new Set());
18341835

1836+
// Clipboard ref for copy/paste operations
1837+
const clipboardRef = useRef(null);
1838+
18351839
// Onboarding modal state
18361840
const [showOnboardingModal, setShowOnboardingModal] = useState(false);
18371841
const [showStorageSetupModal, setShowStorageSetupModal] = useState(false);
@@ -8076,6 +8080,85 @@ function NodeCanvas() {
80768080
}
80778081
}
80788082

8083+
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
8084+
const cmdOrCtrl = isMac ? e.metaKey : e.ctrlKey;
8085+
8086+
// Copy (Ctrl/Cmd+C)
8087+
if (cmdOrCtrl && e.key === 'c' && selectedInstanceIds.size > 0) {
8088+
e.preventDefault();
8089+
const currentGraph = graphsMap.get(activeGraphId);
8090+
if (currentGraph) {
8091+
const copied = copySelection(selectedInstanceIds, currentGraph, nodePrototypesMap, edgesMap);
8092+
clipboardRef.current = copied;
8093+
}
8094+
return;
8095+
}
8096+
8097+
// Paste (Ctrl/Cmd+V)
8098+
if (cmdOrCtrl && e.key === 'v' && clipboardRef.current) {
8099+
e.preventDefault();
8100+
const currentGraph = graphsMap.get(activeGraphId);
8101+
if (currentGraph) {
8102+
// Determine target position
8103+
let targetPos;
8104+
const svgElement = document.querySelector('.node-canvas-svg');
8105+
const rect = svgElement?.getBoundingClientRect();
8106+
8107+
if (rect && !isTouchDeviceRef.current && mousePositionRef.current) {
8108+
// Desktop: use mouse position converted to canvas coords
8109+
const clientX = mousePositionRef.current.x;
8110+
const clientY = mousePositionRef.current.y;
8111+
targetPos = {
8112+
x: (clientX - rect.left - panOffset.x) / zoomLevel + canvasSize.offsetX,
8113+
y: (clientY - rect.top - panOffset.y) / zoomLevel + canvasSize.offsetY
8114+
};
8115+
} else {
8116+
// Mobile fallback: offset from original center
8117+
targetPos = {
8118+
x: clipboardRef.current.originalCenter.x + 50,
8119+
y: clipboardRef.current.originalCenter.y + 50
8120+
};
8121+
}
8122+
8123+
const result = pasteClipboard(
8124+
clipboardRef.current,
8125+
activeGraphId,
8126+
targetPos,
8127+
storeActions,
8128+
currentGraph,
8129+
getNodeDimensions
8130+
);
8131+
setSelectedInstanceIds(new Set(result.newInstanceIds));
8132+
}
8133+
return;
8134+
}
8135+
8136+
// Duplicate (Ctrl/Cmd+D)
8137+
if (cmdOrCtrl && e.key === 'd' && selectedInstanceIds.size > 0) {
8138+
e.preventDefault();
8139+
const currentGraph = graphsMap.get(activeGraphId);
8140+
if (currentGraph) {
8141+
const copied = copySelection(selectedInstanceIds, currentGraph, nodePrototypesMap, edgesMap);
8142+
if (copied) {
8143+
// Paste immediately with fixed offset
8144+
const targetPos = {
8145+
x: copied.originalCenter.x + 50,
8146+
y: copied.originalCenter.y + 50
8147+
};
8148+
const result = pasteClipboard(
8149+
copied,
8150+
activeGraphId,
8151+
targetPos,
8152+
storeActions,
8153+
currentGraph,
8154+
getNodeDimensions
8155+
);
8156+
setSelectedInstanceIds(new Set(result.newInstanceIds));
8157+
}
8158+
}
8159+
return;
8160+
}
8161+
80798162
const isDeleteKey = e.key === 'Delete' || e.key === 'Backspace';
80808163
const nodesSelected = selectedInstanceIds.size > 0;
80818164
const edgeSelected = selectedEdgeId !== null || selectedEdgeIds.size > 0;
@@ -8121,7 +8204,7 @@ function NodeCanvas() {
81218204
};
81228205
window.addEventListener('keydown', handleKeyDown);
81238206
return () => window.removeEventListener('keydown', handleKeyDown);
8124-
}, [selectedInstanceIds, selectedEdgeId, selectedEdgeIds, isHeaderEditing, isRightPanelInputFocused, isLeftPanelInputFocused, nodeNamePrompt.visible, connectionNamePrompt.visible, activeGraphId, storeActions.removeNodeInstance, storeActions.removeEdge, storeActions.clearSelectedEdgeIds]);
8207+
}, [selectedInstanceIds, selectedEdgeId, selectedEdgeIds, isHeaderEditing, isRightPanelInputFocused, isLeftPanelInputFocused, nodeNamePrompt.visible, connectionNamePrompt.visible, activeGraphId, storeActions, graphsMap, nodePrototypesMap, edgesMap, panOffset, zoomLevel, canvasSize]);
81258208

81268209
const handleProjectTitleChange = (newTitle) => {
81278210
// Get CURRENT activeGraphId directly from store

src/store/graphStore.jsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1628,6 +1628,33 @@ const useGraphStore = create(saveCoordinatorMiddleware((set, get, api) => {
16281628
}));
16291629
},
16301630

1631+
// Paste nodes and edges as a single atomic operation
1632+
pasteNodesAndEdges: (graphId, nodes, edges, contextOptions = {}) => {
1633+
api.setChangeContext({ type: 'paste', target: 'batch', ...contextOptions });
1634+
return set(produce((draft) => {
1635+
const graph = draft.graphs.get(graphId);
1636+
if (!graph) return;
1637+
1638+
// Add all node instances
1639+
for (const node of nodes) {
1640+
graph.instances.set(node.instanceId, {
1641+
id: node.instanceId,
1642+
prototypeId: node.prototypeId,
1643+
x: node.x,
1644+
y: node.y,
1645+
scale: node.scale || 1
1646+
});
1647+
}
1648+
1649+
// Add all edges
1650+
for (const edge of edges) {
1651+
const edgeId = edge.id;
1652+
draft.edges.set(edgeId, edge);
1653+
graph.edgeIds.push(edgeId);
1654+
}
1655+
}));
1656+
},
1657+
16311658
// Adds a NEW edge connecting two instances.
16321659
// Adds a NEW edge connecting two instances.
16331660
addEdge: (graphId, newEdgeData, contextOptions = {}) => {

0 commit comments

Comments
 (0)