diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ca2447d..c62a215 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -105,6 +105,11 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack const [gridSize, setGridSize] = useState(20); const [scaleFactor, setScaleFactor] = useState(1); + // Canvas appearance + const [canvasBgColor, setCanvasBgColor] = useState('#1e1e2e'); + const [gridColor, setGridColor] = useState('#3a3a4a'); + const [rulerEnabled, setRulerEnabled] = useState(true); + // Right sidebar const [activeRightPanel, setActiveRightPanel] = useState('tool'); const [selectedElement, setSelectedElement] = useState(null); @@ -1527,11 +1532,18 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack onOpenRightDrawer={() => setMobileRightOpen(true)} unit={unit} onUnitChange={handleUnitChange} + onNavigateBack={onNavigateBack} /> setRulerEnabled((p) => !p)} />
= ({ projectId, token, onNavigateBack unit={unit} gridSize={gridSize} scaleFactor={scaleFactor} + canvasBgColor={canvasBgColor} + gridColor={gridColor} + rulerEnabled={rulerEnabled} /> = 20) { const majOffX = this.zoomPan.getTransform().e % majorSize; const majOffY = this.zoomPan.getTransform().f % majorSize; - this.ctx.strokeStyle = '#33334a'; + // Major lines use a brighter variant of gridColor + this.ctx.strokeStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 15); this.ctx.beginPath(); for (let x = majOffX; x < w; x += majorSize) { this.ctx.moveTo(x, 0); @@ -272,6 +279,105 @@ export class RenderEngine { } } + private lightenColor(hex: string, percent: number): string { + const num = parseInt(hex.replace('#', ''), 16); + if (isNaN(num)) return hex; + const r = Math.min(255, (num >> 16) + Math.round(255 * percent / 100)); + const g = Math.min(255, ((num >> 8) & 0x00FF) + Math.round(255 * percent / 100)); + const b = Math.min(255, (num & 0x0000FF) + Math.round(255 * percent / 100)); + return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`; + } + + private drawRuler(w: number, h: number): void { + if (!this.options.rulerEnabled) return; + const scale = this.zoomPan.getScale(); + const transform = this.zoomPan.getTransform(); + const unit = this.options.unit || 'mm'; + const scaleFactor = this.options.scaleFactor || 1; + const rulerSize = 20; + + this.ctx.save(); + this.ctx.fillStyle = this.options.canvasBgColor || '#1e1e2e'; + this.ctx.fillRect(0, 0, w, rulerSize); + this.ctx.fillRect(0, 0, rulerSize, h); + + this.ctx.strokeStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 30); + this.ctx.lineWidth = 1; + this.ctx.beginPath(); + this.ctx.moveTo(0, rulerSize); + this.ctx.lineTo(w, rulerSize); + this.ctx.moveTo(rulerSize, 0); + this.ctx.lineTo(rulerSize, h); + this.ctx.stroke(); + + // Determine tick spacing in world units (aim for ~50px between major ticks) + const targetPx = 50; + const worldPerPx = 1 / scale; + const rawTickSpacing = targetPx * worldPerPx; + const niceSpacings = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000]; + let tickSpacing = niceSpacings[0]; + for (const ns of niceSpacings) { + if (ns >= rawTickSpacing) { tickSpacing = ns; break; } + tickSpacing = ns; + } + const tickPx = tickSpacing * scale; + + this.ctx.fillStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 50); + this.ctx.font = '9px sans-serif'; + this.ctx.textBaseline = 'middle'; + + // X-axis ruler (top) + this.ctx.textAlign = 'center'; + const xStartWorld = Math.floor((-transform.e / scale) / tickSpacing) * tickSpacing; + for (let i = 0; ; i++) { + const screenX = transform.e + (xStartWorld + i * tickSpacing) * scale; + if (screenX < rulerSize) continue; + if (screenX > w) break; + // Tick mark + this.ctx.strokeStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 30); + this.ctx.beginPath(); + this.ctx.moveTo(screenX, rulerSize - 6); + this.ctx.lineTo(screenX, rulerSize); + this.ctx.stroke(); + // Label + const worldVal = (xStartWorld + i * tickSpacing) * scaleFactor; + let label: string; + switch (unit) { + case 'mm': label = `${worldVal.toFixed(0)}`; break; + case 'cm': label = `${(worldVal / 10).toFixed(0)}`; break; + case 'm': label = `${(worldVal / 1000).toFixed(2)}`; break; + } + this.ctx.fillText(label, screenX, rulerSize / 2); + } + + // Y-axis ruler (left) + this.ctx.textAlign = 'right'; + this.ctx.textBaseline = 'middle'; + const yStartWorld = Math.floor((-transform.f / scale) / tickSpacing) * tickSpacing; + for (let i = 0; ; i++) { + const screenY = transform.f + (yStartWorld + i * tickSpacing) * scale; + if (screenY < rulerSize) continue; + if (screenY > h) break; + this.ctx.strokeStyle = this.lightenColor(this.options.gridColor || '#3a3a4a', 30); + this.ctx.beginPath(); + this.ctx.moveTo(rulerSize - 6, screenY); + this.ctx.lineTo(rulerSize, screenY); + this.ctx.stroke(); + const worldVal = (yStartWorld + i * tickSpacing) * scaleFactor; + let label: string; + switch (unit) { + case 'mm': label = `${worldVal.toFixed(0)}`; break; + case 'cm': label = `${(worldVal / 10).toFixed(0)}`; break; + case 'm': label = `${(worldVal / 1000).toFixed(2)}`; break; + } + this.ctx.fillText(label, rulerSize - 2, screenY); + } + + this.ctx.restore(); + this.ctx.textAlign = 'start'; + this.ctx.textBaseline = 'alphabetic'; + } + private drawBackground(): void { // Background image rendering with transform const img = new Image(); diff --git a/frontend/src/components/CanvasArea.tsx b/frontend/src/components/CanvasArea.tsx index ae0c252..774bcda 100644 --- a/frontend/src/components/CanvasArea.tsx +++ b/frontend/src/components/CanvasArea.tsx @@ -20,6 +20,7 @@ const CanvasArea: React.FC = ({ 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(null); const zoomPanRef = useRef(null); @@ -213,6 +214,7 @@ const CanvasArea: React.FC = ({ 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); @@ -220,7 +222,7 @@ const CanvasArea: React.FC = ({ interaction.setGridSize(gridSize); renderEngine.render(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [gridEnabled, snapEnabled, orthoEnabled, polarEnabled, unit, gridSize, scaleFactor]); + }, [gridEnabled, snapEnabled, orthoEnabled, polarEnabled, unit, gridSize, scaleFactor, canvasBgColor, gridColor, rulerEnabled]); // Sync active tool useEffect(() => { diff --git a/frontend/src/components/RibbonBar.tsx b/frontend/src/components/RibbonBar.tsx index 0738595..e67f4d9 100644 --- a/frontend/src/components/RibbonBar.tsx +++ b/frontend/src/components/RibbonBar.tsx @@ -6,11 +6,16 @@ const tabs: Array<{ id: RibbonTab; label: string; svg: React.ReactNode }> = [ { id: 'insert', label: 'Einfügen', svg: }, { id: 'format', label: 'Format', svg: }, { id: 'view', label: 'Ansicht', svg: }, + { id: 'canvas', label: 'Zeichenfläche', svg: }, { id: 'tools', label: 'Extras', svg: }, { id: 'ki', label: 'KI', svg: }, ]; -const RibbonBar: React.FC = ({ activeTab, onTabChange, onAction }) => { +const RibbonBar: React.FC = ({ + activeTab, onTabChange, onAction, + canvasBgColor = '#1e1e2e', gridColor = '#3a3a4a', rulerEnabled = true, + onCanvasBgColorChange, onGridColorChange, onToggleRuler, +}) => { return (