@@ -3,8 +3,8 @@ import type {
Theme , RibbonTab , ViewMode , RightPanel , DrawerTab ,
CursorPos , CommandHistoryEntry , KIMessage , KISuggestion ,
} from './types/ui.types' ;
import type { CADElement , CADLayer , BlockDefinition } from './types/cad.types' ;
import { createDefaultBlocks , BlockService } from './services/blockService' ;
import type { CADElement , CADLayer , BlockDefinition , ImageElement } from './types/cad.types' ;
import { BlockService } from './services/blockService' ;
import { SeatingService } from './services/seatingService' ;
import type { ToolState } from './interaction' ;
import { GroupManager , type ElementGroup } from './tools/modification/GroupTool' ;
@@ -37,20 +37,13 @@ import { Login } from './pages/Login';
import { Register } from './pages/Register' ;
import { Dashboard } from './pages/Dashboard' ;
// ─── Mock Data ──── ──────────────────────────────────────────
const initialLayers : CADLayer [ ] = [
{ id : 'layer-0' , name : 'Wände' , visible : true , locked : false , color : '#e74c3c' , lineType : 'solid' , transparency : 0 , sortOrder : 0 , parentId : null } ,
{ id : 'layer-1' , name : 'Türen' , visible : true , locked : false , color : '#3498db' , lineType : 'solid' , transparency : 0 , sortOrder : 1 , parentId : null } ,
{ 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 ( ) ;
// ─── Initial State ──────────────────────────────────────────
// Data is loaded from the backend on mount — start with empty state.
const initialLayers : CADLayer [ ] = [ ] ;
const initialBlocks : BlockDefinition [ ] = [ ] ;
const initialCommandHistory : CommandHistoryEntry [ ] = [
{ prefix : '·' , text : 'Bereit · Werkzeug: Auswahl · 110 Objekte · 5 Ebenen ' , type : 'info' } ,
{ prefix : '·' , text : 'Auto-Save aktiv · letzte Speicherung vor 3 Sekunden' , type : 'info' } ,
{ prefix : '·' , text : 'Bereit' , type : 'info' } ,
] ;
const initialKIMessages : KIMessage [ ] = [
@@ -125,21 +118,22 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
const [ kiLoading , setKiLoading ] = useState ( false ) ;
// Plugin system
const createPluginContext = useCallback ( ( ) : PluginContext = > ( {
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 : ( ) = > elementsRef . current ,
getLayers : ( ) = > layersRef . current ,
getActiveLayerId : ( ) = > activeLayerIdRef . current ,
showToast : ( msg ) = > setCommandHistory ( ( prev ) = > [ . . . prev , { prefix : '·' , text : msg , type : 'info' } ] ) ,
log : ( msg ) = > setCommandHistory ( ( prev ) = > [ . . . prev , { prefix : '·' , text : ` [Plugin] ${ msg } ` , type : 'info' } ] ) ,
} ) , [ ] ) ;
useEffect ( ( ) = > {
const ctx : PluginContext = {
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 : ( ) = > elementsRef . current ,
getLayers : ( ) = > layersRef . current ,
getActiveLayerId : ( ) = > activeLayerIdRef . current ,
showToast : ( msg ) = > setCommandHistory ( ( prev ) = > [ . . . prev , { prefix : '·' , text : msg , type : 'info' } ] ) ,
log : ( msg ) = > console . log ( ` [Plugin] ${ msg } ` ) ,
} ;
pluginRegistry . setContext ( ctx ) ;
pluginRegistry . setContext ( createPluginContext ( ) ) ;
registerBuiltinPlugins ( ) ;
pluginRegistry . initDefaults ( ) ;
} , [ ] ) ;
} , [ createPluginContext ] ) ;
// Status bar – only count self as online when actually connected
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 bgServiceRef = React . useRef < BackgroundService > ( new BackgroundService ( ) ) ;
// Cleanup BackgroundService on unmount
useEffect ( ( ) = > {
return ( ) = > {
bgServiceRef . current . dispose ( ) ;
} ;
} , [ ] ) ;
// History panel
const [ historyPanelOpen , setHistoryPanelOpen ] = useState ( false ) ;
// Export format selector (cross-browser replacement for nwsave)
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)
const [ elements , setElements ] = useState < CADElement [ ] > ( [ ] ) ;
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 ] ) ;
const handleToggleElementVisible = useCallback ( ( id : string ) = > {
setElements ( ( prev ) = > prev . map ( ( e ) = > {
if ( e . id === id ) {
const newEl = { . . . e , properties : { . . . e . properties , visible : e .properties?.visible === false ? true : false } } ;
if ( token ) {
setSavedStatus ( "Speichert…" ) ;
updateElement ( token , newEl . id , newEl ) . then ( ( ) = > setSavedStatus ( "gespeichert" ) ) . catch ( ( ) = > setSavedStatus ( "Fehler" ) ) ;
}
return newEl ;
}
return e ;
} ) ) ;
} , [ token ] ) ;
const target = elements . find ( e = > e . id === id ) ;
if ( ! target ) return ;
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 ) {
setSavedStatus ( 'Speichert…' ) ;
updateElement ( token , updatedEl . id , updatedEl ) . then ( ( ) = > setSavedStatus ( 'gespeichert' ) ) . catch ( ( err ) = > {
console . error ( 'Failed to toggle element visibility:' , err ) ;
setSavedStatus ( 'Fehler beim Speichern' ) ;
} ) ;
}
} , [ elements , token ] ) ;
const handleElementsDeleted = useCallback ( ( ids : string [ ] ) = > {
setElements ( ( prev ) = > {
@@ -664,8 +669,12 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
}
} , [ ] ) ;
const handleBlockSearch = useCallback ( ( _query : string ) = > {
// Search is handled locally in BlockLibrary component
const handleBlockSearch = useCallback ( ( query : string ) = > {
// 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 ) = > {
@@ -874,7 +883,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
if ( input . files && input . files [ 0 ] ) {
const reader = new FileReader ( ) ;
reader . onload = ( ) = > {
const imgEl : CADElement = {
const imgEl : ImageElement = {
id : ` el- ${ Date . now ( ) } ` ,
type : 'image' ,
layerId : activeLayerId ,
@@ -907,16 +916,55 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
// ─── Format actions ───
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 ;
}
// ─── View actions ───
if ( action === 'zoom-fit' ) {
setZoomCommand ( 'fit' ) ;
setCommandHistory ( ( prev ) = > [ . . . prev , { prefix : '·' , text : 'Zoom: an Ansicht angepasst' , type : 'info' } ] ) ;
return ;
}
if ( action === 'zoom-100' ) {
setZoomCommand ( '100' ) ;
setCommandHistory ( ( prev ) = > [ . . . prev , { prefix : '·' , text : 'Zoom: 100%' , type : 'info' } ] ) ;
return ;
}
@@ -929,10 +977,6 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
// ─── Tools actions ───
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 ; }
// ─── 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-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 ; }
} , [ 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 ) = > {
setActiveTool ( tool ) ;
@@ -1093,12 +1137,15 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
} , [ token , collab ] ) ;
const handleZoomIn = useCallback ( ( ) = > {
setZoomCommand ( 'in' ) ;
setCommandHistory ( ( prev ) = > [ . . . prev , { prefix : '·' , text : 'Zoom: vergrößert' , type : 'info' } ] ) ;
} , [ ] ) ;
const handleZoomOut = useCallback ( ( ) = > {
setZoomCommand ( 'out' ) ;
setCommandHistory ( ( prev ) = > [ . . . prev , { prefix : '·' , text : 'Zoom: verkleinert' , type : 'info' } ] ) ;
} , [ ] ) ;
const handleZoomFit = useCallback ( ( ) = > {
setZoomCommand ( 'fit' ) ;
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 pluginCmd = pluginCmds . find ( ( c ) = > c . name . toUpperCase ( ) === cmdName ) ;
if ( pluginCmd ) {
const ctx : PluginContext = {
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 ) ;
pluginCmd . execute ( parts . slice ( 1 ) , createPluginContext ( ) ) ;
} else {
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 userMsg : KIMessage = { id : ` ki- ${ Date . now ( ) } ` , role : 'user' , content : text } ;
@@ -1392,6 +1429,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
onZoomIn = { handleZoomIn }
onZoomOut = { handleZoomOut }
onZoomFit = { handleZoomFit }
zoomCommand = { zoomCommand }
onTextEdit = { handleTextEdit }
onCommandTrigger = { handleCommandTrigger }
blocks = { blocks }