Skip to content

Commit 86dcdf8

Browse files
Merge pull request #3 from jaseci-labs/make-copy-paste-smooth
Make copy paste functionalitiy smooth
2 parents 4be53f8 + cfceebc commit 86dcdf8

5 files changed

Lines changed: 277 additions & 66 deletions

File tree

components/Canvas.cl.jac

Lines changed: 211 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ def:pub Canvas(props: dict) -> JsxElement {
3636
textInputRef = useRef(None);
3737
[spacePressed, setSpacePressed] = useState(False);
3838
[contextMenu, setContextMenu] = useState(None);
39+
clipboardRef = useRef([]);
40+
selectedElementRef = useRef(None);
41+
selectedElementsRef = useRef([]);
42+
altDragCopiedRef = useRef(False);
43+
44+
# Keep refs in sync with selection state
45+
selectedElementRef.current = selectionHook.selectedElement;
46+
selectedElementsRef.current = selectionHook.selectedElements;
3947

4048
can with [elementsHook.elements, canvasHook.canvasSize, viewportHook.zoom, viewportHook.panOffset, selectionHook.selectedElement, selectionHook.selectedElements, selectionHook.isBoxSelecting, selectionHook.boxStart, selectionHook.boxEnd] entry {
4149
canvas = canvasRef.current;
@@ -78,6 +86,65 @@ def:pub Canvas(props: dict) -> JsxElement {
7886
}
7987
selectionHook.clearSelection();
8088
}
89+
# Copy (use refs for always-current selection state)
90+
if not textInputHook.textInput and (e.key == "c" or e.key == "C") and (e.ctrlKey or e.metaKey) {
91+
e.preventDefault();
92+
curMulti = selectedElementsRef.current;
93+
curSingle = selectedElementRef.current;
94+
if curMulti and curMulti.length > 0 {
95+
copied = [];
96+
ci = 0;
97+
while ci < curMulti.length {
98+
copied.push(Object.assign({}, curMulti[ci].element));
99+
ci = ci + 1;
100+
}
101+
clipboardRef.current = copied;
102+
} elif curSingle and curSingle.element {
103+
clipboardRef.current = [Object.assign({}, curSingle.element)];
104+
}
105+
}
106+
# Paste
107+
if not textInputHook.textInput and (e.key == "v" or e.key == "V") and (e.ctrlKey or e.metaKey) {
108+
e.preventDefault();
109+
if clipboardRef.current.length > 0 {
110+
newElements = [];
111+
pi = 0;
112+
while pi < clipboardRef.current.length {
113+
original = clipboardRef.current[pi];
114+
newEl = Object.assign({}, original);
115+
if newEl.type == "text" or newEl.type == "rectangle" or newEl.type == "circle" or newEl.type == "image" or newEl.type == "diamond" {
116+
newEl.x = original.x + 20;
117+
newEl.y = original.y + 20;
118+
} elif newEl.type == "line" or newEl.type == "arrow" {
119+
newEl.x1 = original.x1 + 20;
120+
newEl.y1 = original.y1 + 20;
121+
newEl.x2 = original.x2 + 20;
122+
newEl.y2 = original.y2 + 20;
123+
} elif newEl.type == "freehand" {
124+
newEl.points = original.points.map(lambda pt: dict -> dict {
125+
return {"x": pt.x + 20, "y": pt.y + 20};
126+
});
127+
}
128+
newElements.push(newEl);
129+
pi = pi + 1;
130+
}
131+
baseIndex = elementsHook.elements.length;
132+
elementsHook.addMultipleElements(newElements);
133+
# Auto-select pasted elements
134+
pastedSelection = [];
135+
psi = 0;
136+
while psi < newElements.length {
137+
pastedSelection.push({"element": newElements[psi], "index": baseIndex + psi});
138+
psi = psi + 1;
139+
}
140+
selectionHook.setSelectedElements(pastedSelection);
141+
if pastedSelection.length == 1 {
142+
selectionHook.setSelectedElement(pastedSelection[0]);
143+
} else {
144+
selectionHook.setSelectedElement(None);
145+
}
146+
}
147+
}
81148
# Tool switching shortcuts
82149
if not textInputHook.textInput and not e.ctrlKey and not e.metaKey and not e.shiftKey {
83150
shortcuts = TOOL_SHORTCUTS();
@@ -100,7 +167,7 @@ def:pub Canvas(props: dict) -> JsxElement {
100167
window.removeEventListener("keydown", handleKeyDown);
101168
window.removeEventListener("keyup", handleKeyUp);
102169
};
103-
}, [selectionHook.selectedElement, selectionHook.selectedElements, textInputHook.textInput, elementsHook.elements]);
170+
}, [textInputHook.textInput, elementsHook.elements]);
104171

105172
useEffect(lambda -> any {
106173
def handlePaste(e: dict) -> None {
@@ -275,17 +342,19 @@ def:pub Canvas(props: dict) -> JsxElement {
275342
}
276343

277344
def deleteSelected() -> None {
278-
if selectionHook.selectedElements.length > 0 {
279-
indices = [];
280-
i = 0;
281-
while i < selectionHook.selectedElements.length {
282-
indices.push(selectionHook.selectedElements[i].index);
283-
i = i + 1;
345+
curMulti = selectedElementsRef.current;
346+
curSingle = selectedElementRef.current;
347+
if curMulti and curMulti.length > 0 {
348+
delIndices = [];
349+
di = 0;
350+
while di < curMulti.length {
351+
delIndices.push(curMulti[di].index);
352+
di = di + 1;
284353
}
285-
elementsHook.removeMultipleElements(indices);
354+
elementsHook.removeMultipleElements(delIndices);
286355
selectionHook.clearSelection();
287-
} elif selectionHook.selectedElement {
288-
elementsHook.removeElement(selectionHook.selectedElement.index);
356+
} elif curSingle {
357+
elementsHook.removeElement(curSingle.index);
289358
selectionHook.clearSelection();
290359
}
291360
}
@@ -304,24 +373,47 @@ def:pub Canvas(props: dict) -> JsxElement {
304373
setContextMenu(None);
305374
}
306375

376+
def offsetElement(original: dict, dx: float, dy: float) -> dict {
377+
dup = Object.assign({}, original);
378+
if original.type == "text" or original.type == "rectangle" or original.type == "circle" or original.type == "image" or original.type == "diamond" {
379+
dup.x = original.x + dx;
380+
dup.y = original.y + dy;
381+
} elif original.type == "line" or original.type == "arrow" {
382+
dup.x1 = original.x1 + dx;
383+
dup.y1 = original.y1 + dy;
384+
dup.x2 = original.x2 + dx;
385+
dup.y2 = original.y2 + dy;
386+
} elif original.type == "freehand" {
387+
dup.points = original.points.map(lambda p: dict -> dict {
388+
return {"x": p.x + dx, "y": p.y + dy};
389+
});
390+
}
391+
return dup;
392+
}
393+
307394
def handleDuplicate() -> None {
308-
if selectionHook.selectedElement {
309-
original = selectionHook.selectedElement.element;
310-
duplicate = Object.assign({}, original);
311-
if original.type == "text" or original.type == "rectangle" or original.type == "circle" or original.type == "image" or original.type == "diamond" {
312-
duplicate.x = original.x + 20;
313-
duplicate.y = original.y + 20;
314-
} elif original.type == "line" or original.type == "arrow" {
315-
duplicate.x1 = original.x1 + 20;
316-
duplicate.y1 = original.y1 + 20;
317-
duplicate.x2 = original.x2 + 20;
318-
duplicate.y2 = original.y2 + 20;
319-
} elif original.type == "freehand" {
320-
duplicate.points = original.points.map(lambda p: dict -> dict {
321-
return {"x": p.x + 20, "y": p.y + 20};
322-
});
395+
curMultiDup = selectedElementsRef.current;
396+
curSingleDup = selectedElementRef.current;
397+
if curMultiDup and curMultiDup.length > 0 {
398+
dups = [];
399+
ddi = 0;
400+
while ddi < curMultiDup.length {
401+
dups.push(offsetElement(curMultiDup[ddi].element, 20, 20));
402+
ddi = ddi + 1;
323403
}
324-
elementsHook.addElement(duplicate);
404+
dupBase = elementsHook.elements.length;
405+
elementsHook.addMultipleElements(dups);
406+
dupSel = [];
407+
dsi = 0;
408+
while dsi < dups.length {
409+
dupSel.push({"element": dups[dsi], "index": dupBase + dsi});
410+
dsi = dsi + 1;
411+
}
412+
selectionHook.setSelectedElements(dupSel);
413+
selectionHook.setSelectedElement(None);
414+
} elif curSingleDup and curSingleDup.element {
415+
singleDup = offsetElement(curSingleDup.element, 20, 20);
416+
elementsHook.addElement(singleDup);
325417
}
326418
setContextMenu(None);
327419
}
@@ -416,22 +508,53 @@ def:pub Canvas(props: dict) -> JsxElement {
416508

417509
elementInfo = selectionHook.findElementAtPoint(pos, elementsHook.elements);
418510
if elementInfo {
419-
isInSelection = False;
420-
if selectionHook.selectedElements.length > 0 {
421-
i = 0;
422-
while i < selectionHook.selectedElements.length {
423-
if selectionHook.selectedElements[i].index == elementInfo.index {
424-
isInSelection = True;
511+
altDragCopiedRef.current = False;
512+
513+
if e.shiftKey {
514+
# Shift+Click: toggle element in multi-selection
515+
isAlreadySelected = False;
516+
si = 0;
517+
while si < selectionHook.selectedElements.length {
518+
if selectionHook.selectedElements[si].index == elementInfo.index {
519+
isAlreadySelected = True;
425520
}
426-
i = i + 1;
521+
si = si + 1;
427522
}
428-
}
429523

430-
if isInSelection {
431-
selectionHook.setIsDragging(True);
432-
selectionHook.setDragStartPos(pos);
524+
if isAlreadySelected {
525+
selectionHook.removeFromSelection(elementInfo.index);
526+
} else {
527+
# Build the full list in one go to avoid stale state
528+
newMulti = selectionHook.selectedElements.slice();
529+
# If there's a single selectedElement not yet in the list, include it
530+
if selectionHook.selectedElement and newMulti.length == 0 {
531+
newMulti.push(selectionHook.selectedElement);
532+
}
533+
newMulti.push(elementInfo);
534+
selectionHook.setSelectedElements(newMulti);
535+
selectionHook.setSelectedElement(None);
536+
}
433537
} else {
434-
selectionHook.startDragging(elementInfo, pos);
538+
# Normal click (no Shift)
539+
isInMulti = False;
540+
if selectionHook.selectedElements.length > 0 {
541+
mi = 0;
542+
while mi < selectionHook.selectedElements.length {
543+
if selectionHook.selectedElements[mi].index == elementInfo.index {
544+
isInMulti = True;
545+
}
546+
mi = mi + 1;
547+
}
548+
}
549+
550+
if isInMulti {
551+
# Clicking on element already in multi-selection: start group drag
552+
selectionHook.setIsDragging(True);
553+
selectionHook.setDragStartPos(pos);
554+
} else {
555+
# Clicking on new element: single-select it
556+
selectionHook.startDragging(elementInfo, pos);
557+
}
435558
}
436559
} else {
437560
selectionHook.clearSelection();
@@ -581,6 +704,39 @@ def:pub Canvas(props: dict) -> JsxElement {
581704
}
582705

583706
if selectionHook.isDragging and currentTool == "select" {
707+
# Alt+Drag to duplicate: clone originals in place, then drag copies
708+
if e.altKey and not altDragCopiedRef.current {
709+
if selectionHook.selectedElements.length > 1 {
710+
clones = [];
711+
aci = 0;
712+
while aci < selectionHook.selectedElements.length {
713+
clones.push(Object.assign({}, selectionHook.selectedElements[aci].element));
714+
aci = aci + 1;
715+
}
716+
altBase = elementsHook.elements.length;
717+
elementsHook.addMultipleElements(clones);
718+
# Point selection to the new copies (at end of array)
719+
newSel = [];
720+
nsi = 0;
721+
while nsi < clones.length {
722+
newSel.push({"element": clones[nsi], "index": altBase + nsi});
723+
nsi = nsi + 1;
724+
}
725+
selectionHook.setSelectedElements(newSel);
726+
selectionHook.setDragStartPos(pos);
727+
altDragCopiedRef.current = True;
728+
return;
729+
} elif selectionHook.selectedElement {
730+
singleClone = Object.assign({}, selectionHook.selectedElement.element);
731+
altSingleBase = elementsHook.elements.length;
732+
elementsHook.addElement(singleClone);
733+
selectionHook.setSelectedElement({"element": singleClone, "index": altSingleBase});
734+
selectionHook.setSelectedElements([]);
735+
altDragCopiedRef.current = True;
736+
return;
737+
}
738+
}
739+
584740
if selectionHook.selectedElements.length > 1 and selectionHook.dragStartPos {
585741
multiDx = pos.x - selectionHook.dragStartPos.x;
586742
multiDy = pos.y - selectionHook.dragStartPos.y;
@@ -650,6 +806,10 @@ def:pub Canvas(props: dict) -> JsxElement {
650806
canvas = canvasRef.current;
651807
ctx = canvas.getContext("2d");
652808

809+
# Redraw everything first (clears canvas)
810+
canvasHook.redrawCanvas(elementsHook.elements, viewportHook.zoom, viewportHook.panOffset, selectionHook.selectedElement, selectionHook.selectedElements, selectionHook.isBoxSelecting, selectionHook.boxStart, selectionHook.boxEnd);
811+
812+
# Draw the in-progress freehand path on top
653813
ctx.save();
654814
ctx.translate(viewportHook.panOffset.x, viewportHook.panOffset.y);
655815
ctx.scale(viewportHook.zoom, viewportHook.zoom);
@@ -658,21 +818,22 @@ def:pub Canvas(props: dict) -> JsxElement {
658818
ctx.lineWidth = currentStrokeWidth;
659819
ctx.lineCap = "round";
660820
ctx.lineJoin = "round";
821+
ctx.setLineDash([]);
661822
ctx.beginPath();
662823

663-
pathLen = drawingState.currentPath.length;
664-
if pathLen >= 2 {
665-
prev = drawingState.currentPath[pathLen - 2];
666-
last = drawingState.currentPath[pathLen - 1];
667-
midX = (last.x + pos.x) / 2;
668-
midY = (last.y + pos.y) / 2;
669-
ctx.moveTo(last.x, last.y);
670-
ctx.quadraticCurveTo(last.x, last.y, midX, midY);
671-
ctx.stroke();
672-
} elif pathLen == 1 {
673-
ctx.moveTo(drawingState.currentPath[0].x, drawingState.currentPath[0].y);
674-
ctx.lineTo(pos.x, pos.y);
824+
if newPath.length >= 2 {
825+
ctx.moveTo(newPath[0].x, newPath[0].y);
826+
fpi = 1;
827+
while fpi < newPath.length {
828+
fpMidX = (newPath[fpi - 1].x + newPath[fpi].x) / 2;
829+
fpMidY = (newPath[fpi - 1].y + newPath[fpi].y) / 2;
830+
ctx.quadraticCurveTo(newPath[fpi - 1].x, newPath[fpi - 1].y, fpMidX, fpMidY);
831+
fpi = fpi + 1;
832+
}
675833
ctx.stroke();
834+
} elif newPath.length == 1 {
835+
ctx.arc(newPath[0].x, newPath[0].y, currentStrokeWidth / 2, 0, Math.PI * 2);
836+
ctx.fill();
676837
}
677838

678839
ctx.restore();

components/layout/Sidebar.cl.jac

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ def:pub Sidebar(props: dict) -> JsxElement {
6767
showColor = True;
6868
showStroke = True;
6969
showOpacity = True;
70-
showLineStyle = True;
70+
if elType != "freehand" {
71+
showLineStyle = True;
72+
}
7173
activeColor = sel.color;
7274
activeStrokeWidth = sel.strokeWidth;
7375
activeOpacity = sel.opacity or 1.0;

hooks/useElements.cl.jac

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ def:pub useElements() -> dict {
2727
setElements(newElements);
2828
}
2929

30+
def addMultipleElements(newItems: list) -> None {
31+
newElements = elements.concat(newItems);
32+
setElements(newElements);
33+
}
34+
3035
def updateElement(index: int, newElement: dict) -> None {
3136
newElements = elements.slice();
3237
newElements[index] = newElement;
@@ -111,6 +116,7 @@ def:pub useElements() -> dict {
111116
"elements": elements,
112117
"setElements": setElements,
113118
"addElement": addElement,
119+
"addMultipleElements": addMultipleElements,
114120
"updateElement": updateElement,
115121
"updateElementSilent": updateElementSilent,
116122
"updateMultipleElements": updateMultipleElements,

0 commit comments

Comments
 (0)