feat: global block library tree + grouping tool — 19 files, 588 tests green

This commit is contained in:
A0 Orchestrator
2026-07-01 21:02:31 +02:00
parent 405e55e818
commit 7b19a99b24
20 changed files with 2099 additions and 79 deletions
+67 -3
View File
@@ -10,12 +10,14 @@ import { SnapEngine } from '../canvas/SnapEngine';
import { SelectionEngine } from '../canvas/SelectionEngine';
import { SpatialIndex } from '../canvas/SpatialIndex';
import { LayerManager } from '../canvas/LayerManager';
import { GroupManager } from '../tools/modification/GroupTool';
const CanvasArea: React.FC<CanvasAreaProps> = ({
cursorPos, viewMode, onViewChange, gridEnabled, orthoEnabled, snapEnabled,
polarEnabled,
activeTool, elements, layers, activeLayerId, onElementCreated, onElementsDeleted, onElementsModified, onCursorMoved, onToolStateChanged,
onToggleGrid, onToggleOrtho, onToggleSnap, onZoomIn, onZoomOut, onZoomFit, onTextEdit, onCommandTrigger, blocks, onBlockDrop, onSelectionChange, selectedTemplate, bgConfig, remoteCursors, zoomCommand,
groups,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const zoomPanRef = useRef<ZoomPanController | null>(null);
@@ -149,6 +151,24 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [blocks]);
// Sync groups to RenderEngine and InteractionEngine
useEffect(() => {
const renderEngine = renderEngineRef.current;
const interaction = interactionRef.current;
if (!renderEngine) return;
renderEngine.setGroups(groups ?? []);
renderEngine.render();
// InteractionEngine uses GroupManager from App — we pass it via a simple adapter
if (interaction && groups) {
const gm = new GroupManager();
gm.fromJSON(groups);
interaction.setGroupManager(gm);
} else if (interaction) {
interaction.setGroupManager(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [groups]);
// Sync active layer to LayerManager
useEffect(() => {
const layerManager = layerManagerRef.current;
@@ -294,15 +314,59 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }}
onDrop={(e) => {
e.preventDefault();
const blockId = e.dataTransfer.getData('text/block-id');
if (!blockId || !onBlockDrop) return;
const rect = e.currentTarget.getBoundingClientRect();
const sx = e.clientX - rect.left;
const sy = e.clientY - rect.top;
const zoomPan = zoomPanRef.current;
if (!zoomPan) return;
const world = zoomPan.screenToWorld(sx, sy);
onBlockDrop(blockId, world.x, world.y);
// Project block drop
const blockId = e.dataTransfer.getData('text/block-id');
if (blockId && onBlockDrop) {
onBlockDrop(blockId, world.x, world.y);
return;
}
// Global block drop — parse block_data and create element(s)
const globalBlockData = e.dataTransfer.getData('text/global-block-data');
if (globalBlockData) {
try {
const parsed = JSON.parse(globalBlockData);
if (parsed.type === 'svg' && parsed.svg) {
// SVG block — create a block_instance element
const svgEl: CADElement = {
id: `el_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
type: 'block_instance',
layerId: 'layer-0',
x: world.x,
y: world.y,
width: 100,
height: 100,
properties: { blockId: `svg_${Date.now()}`, svgData: parsed.svg, scale: 1 },
};
onElementCreated(svgEl);
} else if (Array.isArray(parsed)) {
// Array of elements — create them with offset
parsed.forEach((el: CADElement, i: number) => {
onElementCreated({
...el,
id: `el_${Date.now()}_${i}`,
x: (el.x ?? 0) + world.x,
y: (el.y ?? 0) + world.y,
});
});
} else if (parsed.type) {
// Single element
onElementCreated({
...parsed,
id: `el_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
x: world.x,
y: world.y,
});
}
} catch {
// Invalid block data — ignore
}
}
}}
/>