merge: combine all features from both codebases - mobile dashboard + layer panel + notifications + shares + all fixes
This commit is contained in:
@@ -10,12 +10,17 @@ 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';
|
||||
import { formatCoordinate } from '../utils/format';
|
||||
|
||||
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,
|
||||
onToggleGrid, onToggleOrtho, onToggleSnap, onZoomIn, onZoomOut, onZoomFit, onTextEdit, onCommandTrigger, blocks, onBlockDrop, onSelectionChange, externalSelectionIds, selectedTemplate, bgConfig, remoteCursors, zoomCommand,
|
||||
groups,
|
||||
unit = 'mm', gridSize = 20, scaleFactor = 1,
|
||||
canvasBgColor = '#1e1e2e', gridColor = '#3a3a4a', rulerEnabled = true,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const zoomPanRef = useRef<ZoomPanController | null>(null);
|
||||
@@ -23,6 +28,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
const interactionRef = useRef<InteractionEngine | null>(null);
|
||||
const spatialIndexRef = useRef<SpatialIndex | null>(null);
|
||||
const layerManagerRef = useRef<LayerManager | null>(null);
|
||||
const selectionEngineRef = useRef<SelectionEngine | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
@@ -43,6 +49,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
interactionRef.current = interaction;
|
||||
spatialIndexRef.current = spatialIndex;
|
||||
layerManagerRef.current = layerManager;
|
||||
selectionEngineRef.current = selectionEngine;
|
||||
|
||||
interaction.setCallbacks({
|
||||
onElementCreated,
|
||||
@@ -64,6 +71,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
interactionRef.current = null;
|
||||
spatialIndexRef.current = null;
|
||||
layerManagerRef.current = null;
|
||||
selectionEngineRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -149,6 +157,46 @@ 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 external selection (from tree panel) to SelectionEngine
|
||||
const prevExternalSelectionRef = useRef<string[]>([]);
|
||||
useEffect(() => {
|
||||
const selectionEngine = selectionEngineRef.current;
|
||||
const renderEngine = renderEngineRef.current;
|
||||
if (!selectionEngine || !renderEngine) return;
|
||||
if (!externalSelectionIds || externalSelectionIds.length === 0) return;
|
||||
// Avoid feedback loop: skip if selection is already identical
|
||||
const currentIds = Array.from(selectionEngine.getSelectedIds()).sort();
|
||||
const incomingIds = [...externalSelectionIds].sort();
|
||||
const same = currentIds.length === incomingIds.length && currentIds.every((id, i) => id === incomingIds[i]);
|
||||
if (same) return;
|
||||
// Avoid re-processing the same external selection we just applied
|
||||
const prevIds = prevExternalSelectionRef.current;
|
||||
const sameAsPrev = prevIds.length === incomingIds.length && prevIds.every((id, i) => id === incomingIds[i]);
|
||||
if (sameAsPrev) return;
|
||||
prevExternalSelectionRef.current = incomingIds;
|
||||
selectionEngine.selectByIds(externalSelectionIds, false);
|
||||
renderEngine.render();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [externalSelectionIds]);
|
||||
|
||||
// Sync active layer to LayerManager
|
||||
useEffect(() => {
|
||||
const layerManager = layerManagerRef.current;
|
||||
@@ -165,12 +213,16 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
renderEngine.setOptions({ showGrid: gridEnabled });
|
||||
renderEngine.setOptions({ showSnapPoints: snapEnabled });
|
||||
renderEngine.setOptions({ showOrtho: orthoEnabled });
|
||||
renderEngine.setOptions({ gridSize, unit, scaleFactor, showGridLabels: true });
|
||||
renderEngine.setOptions({ canvasBgColor, gridColor, rulerEnabled });
|
||||
interaction.setSnapEnabled(snapEnabled);
|
||||
interaction.setOrthoEnabled(orthoEnabled);
|
||||
interaction.setPolarEnabled(polarEnabled);
|
||||
interaction.setUnits(unit, scaleFactor);
|
||||
interaction.setGridSize(gridSize);
|
||||
renderEngine.render();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [gridEnabled, snapEnabled, orthoEnabled, polarEnabled]);
|
||||
}, [gridEnabled, snapEnabled, orthoEnabled, polarEnabled, unit, gridSize, scaleFactor, canvasBgColor, gridColor, rulerEnabled]);
|
||||
|
||||
// Sync active tool
|
||||
useEffect(() => {
|
||||
@@ -254,12 +306,36 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
onZoomFit();
|
||||
};
|
||||
|
||||
// Handle zoom commands from App (ribbon/menu actions)
|
||||
useEffect(() => {
|
||||
if (!zoomCommand) return;
|
||||
const zoomPan = zoomPanRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
if (!zoomPan || !canvas) return;
|
||||
switch (zoomCommand) {
|
||||
case 'in':
|
||||
zoomPan.zoomAt(canvas.width / 2, canvas.height / 2, 1.2);
|
||||
break;
|
||||
case 'out':
|
||||
zoomPan.zoomAt(canvas.width / 2, canvas.height / 2, 0.8);
|
||||
break;
|
||||
case 'fit':
|
||||
zoomPan.zoomFit(elements);
|
||||
break;
|
||||
case '100':
|
||||
zoomPan.reset();
|
||||
break;
|
||||
}
|
||||
renderEngineRef.current?.render();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [zoomCommand]);
|
||||
|
||||
return (
|
||||
<section className="canvas-area" id="canvas-area" aria-label="CAD Zeichenfläche">
|
||||
<div className="canvas-coords" role="status" aria-live="polite">
|
||||
<span><span className="canvas-coords-label">X</span><span className="canvas-coords-val" id="coord-x">{cursorPos.x.toFixed(3)}</span></span>
|
||||
<span><span className="canvas-coords-label">Y</span><span className="canvas-coords-val" id="coord-y">{cursorPos.y.toFixed(3)}</span></span>
|
||||
<span style={{ color: 'var(--color-text-faint)' }}>m</span>
|
||||
<span><span className="canvas-coords-label">X</span><span className="canvas-coords-val" id="coord-x">{formatCoordinate(cursorPos.x, unit, scaleFactor)}</span></span>
|
||||
<span><span className="canvas-coords-label">Y</span><span className="canvas-coords-val" id="coord-y">{formatCoordinate(cursorPos.y, unit, scaleFactor)}</span></span>
|
||||
<span style={{ color: 'var(--color-text-faint)' }}>{unit}</span>
|
||||
</div>
|
||||
|
||||
<canvas
|
||||
@@ -270,15 +346,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
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -326,28 +446,28 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
|
||||
<div className="canvas-toolbar" role="toolbar" aria-label="Canvas-Werkzeuge">
|
||||
<button className="canvas-toolbar-btn" title="Herauszoomen (-)" aria-label="Herauszoomen" onClick={handleZoomOut}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<span className="canvas-zoom-display" id="zoom-display">100%</span>
|
||||
<button className="canvas-toolbar-btn" title="Hineinzoomen (+)" aria-label="Hineinzoomen" onClick={handleZoomIn}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<button className="canvas-toolbar-btn" title="Zoom anpassen (F)" aria-label="Zoom anpassen" onClick={handleZoomFit}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><line x1="7" y1="12" x2="17" y2="12"/></svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><line x1="7" y1="12" x2="17" y2="12"/></svg>
|
||||
</button>
|
||||
<div className="canvas-toolbar-divider"></div>
|
||||
<button className={`canvas-toolbar-btn${gridEnabled ? ' active' : ''}`} title="Grid ein/aus (F7)" aria-label="Grid umschalten" aria-pressed={gridEnabled} onClick={onToggleGrid}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>
|
||||
</button>
|
||||
<button className={`canvas-toolbar-btn${orthoEnabled ? ' active' : ''}`} title="Ortho-Modus (F8)" aria-label="Ortho-Modus umschalten" aria-pressed={orthoEnabled} onClick={onToggleOrtho}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3l18 18M3 21l18-18"/></svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3l18 18M3 21l18-18"/></svg>
|
||||
</button>
|
||||
<button className={`canvas-toolbar-btn${snapEnabled ? ' active' : ''}`} title="Snap ein/aus (F3)" aria-label="Snap umschalten" aria-pressed={snapEnabled} onClick={onToggleSnap}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M2 12h4M18 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M5.6 18.4l2.8-2.8M15.6 8.4l2.8-2.8"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v4M12 18v4M2 12h4M18 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M5.6 18.4l2.8-2.8M15.6 8.4l2.8-2.8"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</button>
|
||||
<div className="canvas-toolbar-divider"></div>
|
||||
<button className="canvas-toolbar-btn" title="Vorherige Ansicht" aria-label="Vorherige Ansicht">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user