fix: LOW issues #37-#50 — remove mock data, type auth functions, ImageElement interface, remove dead code, implement zoom/format/search, cleanup
This commit is contained in:
+89
-51
@@ -3,8 +3,8 @@ import type {
|
|||||||
Theme, RibbonTab, ViewMode, RightPanel, DrawerTab,
|
Theme, RibbonTab, ViewMode, RightPanel, DrawerTab,
|
||||||
CursorPos, CommandHistoryEntry, KIMessage, KISuggestion,
|
CursorPos, CommandHistoryEntry, KIMessage, KISuggestion,
|
||||||
} from './types/ui.types';
|
} from './types/ui.types';
|
||||||
import type { CADElement, CADLayer, BlockDefinition } from './types/cad.types';
|
import type { CADElement, CADLayer, BlockDefinition, ImageElement } from './types/cad.types';
|
||||||
import { createDefaultBlocks, BlockService } from './services/blockService';
|
import { BlockService } from './services/blockService';
|
||||||
import { SeatingService } from './services/seatingService';
|
import { SeatingService } from './services/seatingService';
|
||||||
import type { ToolState } from './interaction';
|
import type { ToolState } from './interaction';
|
||||||
import { GroupManager, type ElementGroup } from './tools/modification/GroupTool';
|
import { GroupManager, type ElementGroup } from './tools/modification/GroupTool';
|
||||||
@@ -37,20 +37,13 @@ import { Login } from './pages/Login';
|
|||||||
import { Register } from './pages/Register';
|
import { Register } from './pages/Register';
|
||||||
import { Dashboard } from './pages/Dashboard';
|
import { Dashboard } from './pages/Dashboard';
|
||||||
|
|
||||||
// ─── Mock Data ──────────────────────────────────────────────
|
// ─── Initial State ──────────────────────────────────────────
|
||||||
const initialLayers: CADLayer[] = [
|
// Data is loaded from the backend on mount — start with empty state.
|
||||||
{ id: 'layer-0', name: 'Wände', visible: true, locked: false, color: '#e74c3c', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null },
|
const initialLayers: CADLayer[] = [];
|
||||||
{ id: 'layer-1', name: 'Türen', visible: true, locked: false, color: '#3498db', lineType: 'solid', transparency: 0, sortOrder: 1, parentId: null },
|
const initialBlocks: BlockDefinition[] = [];
|
||||||
{ id: 'layer-2', name: 'Bestuhlung', visible: true, locked: false, color: '#2ecc71', lineType: 'solid', transparency: 0, sortOrder: 2, parentId: null },
|
|
||||||
{ id: 'layer-3', name: 'Bühne', visible: true, locked: false, color: '#f39c12', lineType: 'solid', transparency: 0, sortOrder: 3, parentId: null },
|
|
||||||
{ id: 'layer-4', name: 'Hintergrund', visible: true, locked: true, color: '#95a5a6', lineType: 'dotted', transparency: 50, sortOrder: 4, parentId: null },
|
|
||||||
];
|
|
||||||
|
|
||||||
const initialBlocks: BlockDefinition[] = createDefaultBlocks();
|
|
||||||
|
|
||||||
const initialCommandHistory: CommandHistoryEntry[] = [
|
const initialCommandHistory: CommandHistoryEntry[] = [
|
||||||
{ prefix: '·', text: 'Bereit · Werkzeug: Auswahl · 110 Objekte · 5 Ebenen', type: 'info' },
|
{ prefix: '·', text: 'Bereit', type: 'info' },
|
||||||
{ prefix: '·', text: 'Auto-Save aktiv · letzte Speicherung vor 3 Sekunden', type: 'info' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const initialKIMessages: KIMessage[] = [
|
const initialKIMessages: KIMessage[] = [
|
||||||
@@ -125,8 +118,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
const [kiLoading, setKiLoading] = useState(false);
|
const [kiLoading, setKiLoading] = useState(false);
|
||||||
|
|
||||||
// Plugin system
|
// Plugin system
|
||||||
useEffect(() => {
|
const createPluginContext = useCallback((): PluginContext => ({
|
||||||
const ctx: PluginContext = {
|
|
||||||
addElement: (el) => setElements((prev) => [...prev, el]),
|
addElement: (el) => setElements((prev) => [...prev, el]),
|
||||||
removeElement: (id) => setElements((prev) => prev.filter((e) => e.id !== id)),
|
removeElement: (id) => setElements((prev) => prev.filter((e) => e.id !== id)),
|
||||||
updateElement: (id, props) => setElements((prev) => prev.map((e) => (e.id === id ? { ...e, ...props } : e))),
|
updateElement: (id, props) => setElements((prev) => prev.map((e) => (e.id === id ? { ...e, ...props } : e))),
|
||||||
@@ -134,12 +126,14 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
getLayers: () => layersRef.current,
|
getLayers: () => layersRef.current,
|
||||||
getActiveLayerId: () => activeLayerIdRef.current,
|
getActiveLayerId: () => activeLayerIdRef.current,
|
||||||
showToast: (msg) => setCommandHistory((prev) => [...prev, { prefix: '·', text: msg, type: 'info' }]),
|
showToast: (msg) => setCommandHistory((prev) => [...prev, { prefix: '·', text: msg, type: 'info' }]),
|
||||||
log: (msg) => console.log(`[Plugin] ${msg}`),
|
log: (msg) => setCommandHistory((prev) => [...prev, { prefix: '·', text: `[Plugin] ${msg}`, type: 'info' }]),
|
||||||
};
|
}), []);
|
||||||
pluginRegistry.setContext(ctx);
|
|
||||||
|
useEffect(() => {
|
||||||
|
pluginRegistry.setContext(createPluginContext());
|
||||||
registerBuiltinPlugins();
|
registerBuiltinPlugins();
|
||||||
pluginRegistry.initDefaults();
|
pluginRegistry.initDefaults();
|
||||||
}, []);
|
}, [createPluginContext]);
|
||||||
|
|
||||||
// Status bar – only count self as online when actually connected
|
// Status bar – only count self as online when actually connected
|
||||||
const onlineCount = collab.status === 'connected' ? collab.cursors.length : 0;
|
const onlineCount = collab.status === 'connected' ? collab.cursors.length : 0;
|
||||||
@@ -158,12 +152,22 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
const [bgConfig, setBgConfig] = useState<BackgroundConfig | null>(null);
|
const [bgConfig, setBgConfig] = useState<BackgroundConfig | null>(null);
|
||||||
const bgServiceRef = React.useRef<BackgroundService>(new BackgroundService());
|
const bgServiceRef = React.useRef<BackgroundService>(new BackgroundService());
|
||||||
|
|
||||||
|
// Cleanup BackgroundService on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
bgServiceRef.current.dispose();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// History panel
|
// History panel
|
||||||
const [historyPanelOpen, setHistoryPanelOpen] = useState(false);
|
const [historyPanelOpen, setHistoryPanelOpen] = useState(false);
|
||||||
|
|
||||||
// Export format selector (cross-browser replacement for nwsave)
|
// Export format selector (cross-browser replacement for nwsave)
|
||||||
const [exportFormatOpen, setExportFormatOpen] = useState(false);
|
const [exportFormatOpen, setExportFormatOpen] = useState(false);
|
||||||
|
|
||||||
|
// Zoom command state — triggers zoom actions in CanvasArea via useEffect
|
||||||
|
const [zoomCommand, setZoomCommand] = useState<'in' | 'out' | 'fit' | '100' | null>(null);
|
||||||
|
|
||||||
// Elements + undo/redo (HistoryManager)
|
// Elements + undo/redo (HistoryManager)
|
||||||
const [elements, setElements] = useState<CADElement[]>([]);
|
const [elements, setElements] = useState<CADElement[]>([]);
|
||||||
const historyManagerRef = React.useRef<HistoryManager>(new HistoryManager());
|
const historyManagerRef = React.useRef<HistoryManager>(new HistoryManager());
|
||||||
@@ -446,18 +450,19 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
}, [layers, blocks, groups, bgConfig, syncHistory, drawingId, token, collab, activeLayerId]);
|
}, [layers, blocks, groups, bgConfig, syncHistory, drawingId, token, collab, activeLayerId]);
|
||||||
|
|
||||||
const handleToggleElementVisible = useCallback((id: string) => {
|
const handleToggleElementVisible = useCallback((id: string) => {
|
||||||
setElements((prev) => prev.map((e) => {
|
const target = elements.find(e => e.id === id);
|
||||||
if (e.id === id) {
|
if (!target) return;
|
||||||
const newEl = { ...e, properties: { ...e.properties, visible: e.properties?.visible === false ? true : false } };
|
const updatedEl: CADElement = { ...target, properties: { ...target.properties, visible: target.properties?.visible === false ? true : false } };
|
||||||
|
setElements((prev) => prev.map((e) => e.id === id ? updatedEl : e));
|
||||||
|
// Call API outside the state updater
|
||||||
if (token) {
|
if (token) {
|
||||||
setSavedStatus("Speichert…");
|
setSavedStatus('Speichert…');
|
||||||
updateElement(token, newEl.id, newEl).then(() => setSavedStatus("gespeichert")).catch(() => setSavedStatus("Fehler"));
|
updateElement(token, updatedEl.id, updatedEl).then(() => setSavedStatus('gespeichert')).catch((err) => {
|
||||||
|
console.error('Failed to toggle element visibility:', err);
|
||||||
|
setSavedStatus('Fehler beim Speichern');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return newEl;
|
}, [elements, token]);
|
||||||
}
|
|
||||||
return e;
|
|
||||||
}));
|
|
||||||
}, [token]);
|
|
||||||
|
|
||||||
const handleElementsDeleted = useCallback((ids: string[]) => {
|
const handleElementsDeleted = useCallback((ids: string[]) => {
|
||||||
setElements((prev) => {
|
setElements((prev) => {
|
||||||
@@ -664,8 +669,12 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleBlockSearch = useCallback((_query: string) => {
|
const handleBlockSearch = useCallback((query: string) => {
|
||||||
// Search is handled locally in BlockLibrary component
|
// BlockLibrary handles search filtering locally — this callback is for
|
||||||
|
// any App-level side effects of block search (e.g. logging, analytics).
|
||||||
|
if (query.trim()) {
|
||||||
|
setCommandHistory((prev) => [...prev, { prefix: '·', text: `Block-Suche: ${query}`, type: 'info' }]);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDragBlock = useCallback((blockId: string) => {
|
const handleDragBlock = useCallback((blockId: string) => {
|
||||||
@@ -874,7 +883,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
if (input.files && input.files[0]) {
|
if (input.files && input.files[0]) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const imgEl: CADElement = {
|
const imgEl: ImageElement = {
|
||||||
id: `el-${Date.now()}`,
|
id: `el-${Date.now()}`,
|
||||||
type: 'image',
|
type: 'image',
|
||||||
layerId: activeLayerId,
|
layerId: activeLayerId,
|
||||||
@@ -907,16 +916,55 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
|
|
||||||
// ─── Format actions ───
|
// ─── Format actions ───
|
||||||
if (action.startsWith('format-')) {
|
if (action.startsWith('format-')) {
|
||||||
setCommandHistory((prev) => [...prev, { prefix: '·', text: `Format: ${action.replace('format-', '')} (Auswahl erforderlich)`, type: 'info' }]);
|
const formatType = action.replace('format-', '');
|
||||||
|
const selectedIds = selectedElementIdsRef.current;
|
||||||
|
const selected = selectedIds.length > 0
|
||||||
|
? elements.filter(e => selectedIds.includes(e.id))
|
||||||
|
: (selectedElement ? [selectedElement] : []);
|
||||||
|
if (selected.length === 0) {
|
||||||
|
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Format: Keine Elemente ausgewählt', type: 'info' }]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modified = selected.map(el => {
|
||||||
|
const props = { ...el.properties };
|
||||||
|
switch (formatType) {
|
||||||
|
case 'stroke-width':
|
||||||
|
props.strokeWidth = (props.strokeWidth ?? 1) + 0.5;
|
||||||
|
break;
|
||||||
|
case 'stroke-color':
|
||||||
|
props.stroke = props.stroke === '#ff0000' ? '#00ff00' : '#ff0000';
|
||||||
|
break;
|
||||||
|
case 'fill-color':
|
||||||
|
props.fill = props.fill === '#ff0000' ? '#00ff00' : '#ff0000';
|
||||||
|
break;
|
||||||
|
case 'stroke-style':
|
||||||
|
props.lineType = props.lineType === 'solid' ? 'dashed' : 'solid';
|
||||||
|
break;
|
||||||
|
case 'align-left':
|
||||||
|
return { ...el, x: selected.reduce((min, s) => Math.min(min, s.x), Infinity) };
|
||||||
|
case 'align-center':
|
||||||
|
const cx = selected.reduce((sum, s) => sum + s.x, 0) / selected.length;
|
||||||
|
return { ...el, x: cx };
|
||||||
|
case 'align-right':
|
||||||
|
return { ...el, x: selected.reduce((max, s) => Math.max(max, s.x), -Infinity) };
|
||||||
|
default:
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
return { ...el, properties: props };
|
||||||
|
});
|
||||||
|
handleElementsModified(modified);
|
||||||
|
setCommandHistory((prev) => [...prev, { prefix: '·', text: `Format: ${formatType} auf ${modified.length} Element(e) angewendet`, type: 'info' }]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── View actions ───
|
// ─── View actions ───
|
||||||
if (action === 'zoom-fit') {
|
if (action === 'zoom-fit') {
|
||||||
|
setZoomCommand('fit');
|
||||||
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: an Ansicht angepasst', type: 'info' }]);
|
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: an Ansicht angepasst', type: 'info' }]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === 'zoom-100') {
|
if (action === 'zoom-100') {
|
||||||
|
setZoomCommand('100');
|
||||||
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: 100%', type: 'info' }]);
|
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: 100%', type: 'info' }]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -929,10 +977,6 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
|
|
||||||
// ─── Tools actions ───
|
// ─── Tools actions ───
|
||||||
if (action === 'measure') { setActiveTool('dimension'); setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Messwerkzeug aktiv', type: 'info' }]); return; }
|
if (action === 'measure') { setActiveTool('dimension'); setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Messwerkzeug aktiv', type: 'info' }]); return; }
|
||||||
if (action === 'search') {
|
|
||||||
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Suche: Befehlszeile verwenden (Strg+F)', type: 'info' }]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (action === 'history') { setHistoryPanelOpen((prev) => !prev); return; }
|
if (action === 'history') { setHistoryPanelOpen((prev) => !prev); return; }
|
||||||
|
|
||||||
// ─── Background ───
|
// ─── Background ───
|
||||||
@@ -942,7 +986,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
if (action === 'ki') { setActiveRightPanel('ki'); setCommandHistory((prev) => [...prev, { prefix: '·', text: 'KI Copilot geöffnet', type: 'info' }]); return; }
|
if (action === 'ki') { setActiveRightPanel('ki'); setCommandHistory((prev) => [...prev, { prefix: '·', text: 'KI Copilot geöffnet', type: 'info' }]); return; }
|
||||||
if (action === 'ki-draw') { setActiveRightPanel('ki'); setCommandHistory((prev) => [...prev, { prefix: '·', text: 'KI Zeichnen: Beschreibung eingeben', type: 'info' }]); return; }
|
if (action === 'ki-draw') { setActiveRightPanel('ki'); setCommandHistory((prev) => [...prev, { prefix: '·', text: 'KI Zeichnen: Beschreibung eingeben', type: 'info' }]); return; }
|
||||||
if (action === 'ki-analyze') { setActiveRightPanel('ki'); setCommandHistory((prev) => [...prev, { prefix: '·', text: 'KI Analyse gestartet', type: 'info' }]); return; }
|
if (action === 'ki-analyze') { setActiveRightPanel('ki'); setCommandHistory((prev) => [...prev, { prefix: '·', text: 'KI Analyse gestartet', type: 'info' }]); return; }
|
||||||
}, [handleUndo, handleRedo, handleImport, handleExport, onNavigateBack, drawingId, token, elements, activeLayerId, gridEnabled, collab]);
|
}, [handleUndo, handleRedo, handleElementsModified, handleImport, handleExport, onNavigateBack, drawingId, token, elements, activeLayerId, gridEnabled, collab, selectedElement]);
|
||||||
|
|
||||||
const handleToolChange = useCallback((tool: string) => {
|
const handleToolChange = useCallback((tool: string) => {
|
||||||
setActiveTool(tool);
|
setActiveTool(tool);
|
||||||
@@ -1093,12 +1137,15 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
}, [token, collab]);
|
}, [token, collab]);
|
||||||
|
|
||||||
const handleZoomIn = useCallback(() => {
|
const handleZoomIn = useCallback(() => {
|
||||||
|
setZoomCommand('in');
|
||||||
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: vergrößert', type: 'info' }]);
|
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: vergrößert', type: 'info' }]);
|
||||||
}, []);
|
}, []);
|
||||||
const handleZoomOut = useCallback(() => {
|
const handleZoomOut = useCallback(() => {
|
||||||
|
setZoomCommand('out');
|
||||||
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: verkleinert', type: 'info' }]);
|
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: verkleinert', type: 'info' }]);
|
||||||
}, []);
|
}, []);
|
||||||
const handleZoomFit = useCallback(() => {
|
const handleZoomFit = useCallback(() => {
|
||||||
|
setZoomCommand('fit');
|
||||||
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: an Ansicht angepasst', type: 'info' }]);
|
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: an Ansicht angepasst', type: 'info' }]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -1259,22 +1306,12 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
const cmdName = parts[0].toUpperCase();
|
const cmdName = parts[0].toUpperCase();
|
||||||
const pluginCmd = pluginCmds.find((c) => c.name.toUpperCase() === cmdName);
|
const pluginCmd = pluginCmds.find((c) => c.name.toUpperCase() === cmdName);
|
||||||
if (pluginCmd) {
|
if (pluginCmd) {
|
||||||
const ctx: PluginContext = {
|
pluginCmd.execute(parts.slice(1), createPluginContext());
|
||||||
addElement: (el) => setElements((prev) => [...prev, el]),
|
|
||||||
removeElement: (id) => setElements((prev) => prev.filter((e) => e.id !== id)),
|
|
||||||
updateElement: (id, props) => setElements((prev) => prev.map((e) => (e.id === id ? { ...e, ...props } : e))),
|
|
||||||
getElements: () => elements,
|
|
||||||
getLayers: () => layers,
|
|
||||||
getActiveLayerId: () => activeLayerId,
|
|
||||||
showToast: (msg) => setCommandHistory((prev) => [...prev, { prefix: '·', text: msg, type: 'info' }]),
|
|
||||||
log: (msg) => console.log(`[Plugin] ${msg}`),
|
|
||||||
};
|
|
||||||
pluginCmd.execute(parts.slice(1), ctx);
|
|
||||||
} else {
|
} else {
|
||||||
setCommandHistory((prev) => [...prev, { prefix: '·', text: `Unbekannter Befehl: ${cmd}`, type: 'info' }]);
|
setCommandHistory((prev) => [...prev, { prefix: '·', text: `Unbekannter Befehl: ${cmd}`, type: 'info' }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [handleUndo, handleRedo, handleRibbonAction, collab]);
|
}, [handleUndo, handleRedo, handleRibbonAction, collab, createPluginContext]);
|
||||||
|
|
||||||
const handleKISend = useCallback(async (text: string) => {
|
const handleKISend = useCallback(async (text: string) => {
|
||||||
const userMsg: KIMessage = { id: `ki-${Date.now()}`, role: 'user', content: text };
|
const userMsg: KIMessage = { id: `ki-${Date.now()}`, role: 'user', content: text };
|
||||||
@@ -1392,6 +1429,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
onZoomIn={handleZoomIn}
|
onZoomIn={handleZoomIn}
|
||||||
onZoomOut={handleZoomOut}
|
onZoomOut={handleZoomOut}
|
||||||
onZoomFit={handleZoomFit}
|
onZoomFit={handleZoomFit}
|
||||||
|
zoomCommand={zoomCommand}
|
||||||
onTextEdit={handleTextEdit}
|
onTextEdit={handleTextEdit}
|
||||||
onCommandTrigger={handleCommandTrigger}
|
onCommandTrigger={handleCommandTrigger}
|
||||||
blocks={blocks}
|
blocks={blocks}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
cursorPos, viewMode, onViewChange, gridEnabled, orthoEnabled, snapEnabled,
|
cursorPos, viewMode, onViewChange, gridEnabled, orthoEnabled, snapEnabled,
|
||||||
polarEnabled,
|
polarEnabled,
|
||||||
activeTool, elements, layers, activeLayerId, onElementCreated, onElementsDeleted, onElementsModified, onCursorMoved, onToolStateChanged,
|
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, selectedTemplate, bgConfig, remoteCursors, zoomCommand,
|
||||||
}) => {
|
}) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const zoomPanRef = useRef<ZoomPanController | null>(null);
|
const zoomPanRef = useRef<ZoomPanController | null>(null);
|
||||||
@@ -254,6 +254,30 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
onZoomFit();
|
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 (
|
return (
|
||||||
<section className="canvas-area" id="canvas-area" aria-label="CAD Zeichenfläche">
|
<section className="canvas-area" id="canvas-area" aria-label="CAD Zeichenfläche">
|
||||||
<div className="canvas-coords" role="status" aria-live="polite">
|
<div className="canvas-coords" role="status" aria-live="polite">
|
||||||
|
|||||||
@@ -216,10 +216,6 @@ const RibbonBar: React.FC<RibbonBarProps> = ({ activeTab, onTabChange, onAction
|
|||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21.3 8.7 8.7 21.3a1 1 0 0 1-1.4 0L2.7 16.7a1 1 0 0 1 0-1.4L15.3 2.7a1 1 0 0 1 1.4 0l4.6 4.6a1 1 0 0 1 0 1.4z"/><path d="m7.5 10.5 2 2"/><path d="m10.5 7.5 2 2"/><path d="m13.5 4.5 2 2"/><path d="m4.5 13.5 2 2"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21.3 8.7 8.7 21.3a1 1 0 0 1-1.4 0L2.7 16.7a1 1 0 0 1 0-1.4L15.3 2.7a1 1 0 0 1 1.4 0l4.6 4.6a1 1 0 0 1 0 1.4z"/><path d="m7.5 10.5 2 2"/><path d="m10.5 7.5 2 2"/><path d="m13.5 4.5 2 2"/><path d="m4.5 13.5 2 2"/></svg>
|
||||||
<span>Messen</span>
|
<span>Messen</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="ribbon-btn" title="Suchen (Strg+F)" onClick={() => onAction('search')}>
|
|
||||||
<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"/></svg>
|
|
||||||
<span>Suchen</span>
|
|
||||||
</button>
|
|
||||||
<button className="ribbon-btn" title="Plugins" onClick={() => onAction('plugins')}>
|
<button className="ribbon-btn" title="Plugins" onClick={() => onAction('plugins')}>
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v6m0 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/><path d="M12 8a6 6 0 0 0-6 6c0 3 2 4 2 6h8c0-2 2-3 2-6a6 6 0 0 0-6-6z"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v6m0 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/><path d="M12 8a6 6 0 0 0-6 6c0 3 2 4 2 6h8c0-2 2-3 2-6a6 6 0 0 0-6-6z"/></svg>
|
||||||
<span>Plugins</span>
|
<span>Plugins</span>
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ export class HistoryManager {
|
|||||||
private currentState: CADStateSnapshot | null = null;
|
private currentState: CADStateSnapshot | null = null;
|
||||||
private maxStackSize: number;
|
private maxStackSize: number;
|
||||||
private listeners: Set<() => void> = new Set();
|
private listeners: Set<() => void> = new Set();
|
||||||
private idCounter = 0;
|
|
||||||
|
|
||||||
constructor(options: HistoryManagerOptions = {}) {
|
constructor(options: HistoryManagerOptions = {}) {
|
||||||
this.maxStackSize = options.maxStackSize ?? DEFAULT_MAX_SIZE;
|
this.maxStackSize = options.maxStackSize ?? DEFAULT_MAX_SIZE;
|
||||||
@@ -212,11 +211,6 @@ export class HistoryManager {
|
|||||||
private notifyListeners(): void {
|
private notifyListeners(): void {
|
||||||
this.listeners.forEach((l) => l());
|
this.listeners.forEach((l) => l());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Eindeutige ID generieren */
|
|
||||||
private generateId(): string {
|
|
||||||
return `hist-${++this.idCounter}-${Date.now()}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Default HistoryManager-Instanz (Singleton) */
|
/** Default HistoryManager-Instanz (Singleton) */
|
||||||
|
|||||||
@@ -30,8 +30,32 @@ export interface Drawing {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Auth Types ────────────────────────────────────────
|
||||||
|
export interface AuthUser {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthSession {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
user: AuthUser;
|
||||||
|
session: AuthSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterResponse {
|
||||||
|
user: AuthUser;
|
||||||
|
session: AuthSession;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Auth ───────────────────────────────────────────────
|
// ─── Auth ───────────────────────────────────────────────
|
||||||
export async function login(email: string, password: string): Promise<{ user: any; session: { token: string } }> {
|
export async function login(email: string, password: string): Promise<LoginResponse> {
|
||||||
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -41,7 +65,7 @@ export async function login(email: string, password: string): Promise<{ user: an
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function register(email: string, password: string, name: string): Promise<{ user: any; session: { token: string } }> {
|
export async function register(email: string, password: string, name: string): Promise<RegisterResponse> {
|
||||||
const res = await fetch(`${API_BASE}/api/auth/register`, {
|
const res = await fetch(`${API_BASE}/api/auth/register`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -51,7 +75,7 @@ export async function register(email: string, password: string, name: string): P
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMe(token: string): Promise<any> {
|
export async function getMe(token: string): Promise<AuthUser> {
|
||||||
const res = await fetch(`${API_BASE}/api/auth/me`, {
|
const res = await fetch(`${API_BASE}/api/auth/me`, {
|
||||||
headers: authHeaders(token),
|
headers: authHeaders(token),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -175,6 +175,11 @@ export class BackgroundService {
|
|||||||
this.image = null;
|
this.image = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Dispose of resources — called on component unmount */
|
||||||
|
dispose(): void {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/** Export to ProjectData.background format */
|
/** Export to ProjectData.background format */
|
||||||
toProjectData(): ProjectData['background'] | undefined {
|
toProjectData(): ProjectData['background'] | undefined {
|
||||||
if (!this.isLoaded()) return undefined;
|
if (!this.isLoaded()) return undefined;
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ export interface CADElement {
|
|||||||
properties: CADProperties;
|
properties: CADProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Image element extending base CADElement with image-specific fields */
|
||||||
|
export interface ImageElement extends CADElement {
|
||||||
|
type: 'image';
|
||||||
|
properties: CADProperties & {
|
||||||
|
src: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface BlockDefinition {
|
export interface BlockDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export interface CanvasAreaProps {
|
|||||||
onZoomIn: () => void;
|
onZoomIn: () => void;
|
||||||
onZoomOut: () => void;
|
onZoomOut: () => void;
|
||||||
onZoomFit: () => void;
|
onZoomFit: () => void;
|
||||||
|
zoomCommand?: 'in' | 'out' | 'fit' | '100' | null;
|
||||||
onTextEdit?: (el: CADElement) => void;
|
onTextEdit?: (el: CADElement) => void;
|
||||||
onCommandTrigger?: (msg: string) => void;
|
onCommandTrigger?: (msg: string) => void;
|
||||||
blocks?: BlockDefinition[];
|
blocks?: BlockDefinition[];
|
||||||
|
|||||||
Reference in New Issue
Block a user