fix: rewrite copy/paste/duplicate using Fabric's native clone(), add layer features

- Copy/paste/duplicate now uses obj.clone() instead of broken fromObject() serialization
- Ctrl+C stores actual object refs, Ctrl+V clones them — works for images, groups, paths, text
- System clipboard copy (Ctrl+C / Ctrl+Shift+C) properly re-selects after screenshot
- Layer panel: double-click to rename, collapsible groups with expand/collapse arrow
- Auto-indexed names: Image 1, Image 2, Drawing 1, Drawing 2, etc.
- Group children visible in layer panel when expanded
- Copy/paste works on groups and mixed multi-selections
This commit is contained in:
Hiren
2026-03-09 14:35:10 +05:30
parent 3331b1eac3
commit 9e5c4ecdd0
2 changed files with 213 additions and 113 deletions
+48 -20
View File
@@ -296,30 +296,49 @@ export default function Editor({ isPublicView }: EditorProps) {
}, []);
// Refresh layer list from canvas
// Auto-name counters
const nameCounters = useRef<Record<string, number>>({});
function autoName(obj: any, i: number): string {
if (obj.name) return obj.name;
const type = obj.type || 'object';
if (type === 'image') {
nameCounters.current.image = (nameCounters.current.image || 0) + 1;
return `Image ${nameCounters.current.image}`;
}
if (type === 'i-text') return (obj.text || 'Text').slice(0, 20);
if (type === 'path') {
nameCounters.current.path = (nameCounters.current.path || 0) + 1;
return `Drawing ${nameCounters.current.path}`;
}
if (type === 'group') return `Group (${obj._objects?.length || 0})`;
return `${type} ${i + 1}`;
}
function buildLayerItem(obj: any, i: number): any {
const id = obj.id || `layer-${i}`;
const isGroup = obj.type === 'group';
let children: any[] | undefined;
if (isGroup && obj._objects) {
children = obj._objects.map((child: any, ci: number) => buildLayerItem(child, ci));
}
return {
id,
name: autoName(obj, i),
type: obj.type || 'object',
visible: obj.visible !== false,
locked: !obj.selectable,
isGroup,
children,
};
}
const refreshLayers = useCallback(() => {
const canvas = canvasRef.current?.getCanvas();
if (!canvas) return;
nameCounters.current = {};
const objects = canvas.getObjects();
const layers = objects.map((obj: any, i: number) => {
const id = obj.id || `layer-${i}`;
const isGroup = obj.type === 'group';
let name = obj.name || '';
if (!name) {
if (obj.type === 'image') name = 'Image';
else if (obj.type === 'i-text') name = (obj.text || 'Text').slice(0, 20);
else if (obj.type === 'path') name = 'Drawing';
else if (isGroup) name = `Group (${obj._objects?.length || 0})`;
else name = obj.type || 'Object';
}
return {
id,
name: `${name}`,
type: obj.type || 'object',
visible: obj.visible !== false,
locked: !obj.selectable,
isGroup,
};
});
const layers = objects.map((obj: any, i: number) => buildLayerItem(obj, i));
setLayerList(layers);
const activeIds = canvas.getActiveObjects().map((o: any) => o.id).filter(Boolean);
setSelectedLayerIds(activeIds);
@@ -938,6 +957,15 @@ export default function Editor({ isPublicView }: EditorProps) {
refreshLayers();
}
}}
onRename={(id, name) => {
const canvas = canvasRef.current?.getCanvas();
if (!canvas) return;
const obj = canvas.getObjects().find((o: any) => o.id === id);
if (obj) {
(obj as any).name = name;
refreshLayers();
}
}}
onGroup={handleGroup}
onUngroup={handleUngroup}
hasSelection={canvasRef.current?.getCanvas()?.getActiveObjects()?.length! > 1}