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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user