import type { BlockDefinition, CADElement } from '../types/cad.types'; /** * Block Service — CRUD, SVG-Import, Block-Definition vs Referenz */ export class BlockService { private blocks: Map = new Map(); /** Register or update a block definition */ addBlock(block: BlockDefinition): void { this.blocks.set(block.id, block); } /** Remove a block definition */ removeBlock(id: string): void { this.blocks.delete(id); } /** Get a block definition by ID */ getBlock(id: string): BlockDefinition | undefined { return this.blocks.get(id); } /** Get all block definitions */ getAllBlocks(): BlockDefinition[] { return Array.from(this.blocks.values()); } /** Get blocks by category */ getBlocksByCategory(category: string): BlockDefinition[] { return this.getAllBlocks().filter(b => category === 'Alle' || b.category === category); } /** Search blocks by name */ searchBlocks(query: string): BlockDefinition[] { const q = query.toLowerCase().trim(); if (!q) return this.getAllBlocks(); return this.getAllBlocks().filter(b => b.name.toLowerCase().includes(q) || b.description.toLowerCase().includes(q) ); } /** Rename a block definition */ renameBlock(id: string, name: string): void { const block = this.blocks.get(id); if (block) { this.blocks.set(id, { ...block, name }); } } /** Duplicate a block definition */ duplicateBlock(id: string): BlockDefinition | null { const block = this.blocks.get(id); if (!block) return null; const newId = `blk-${Date.now()}`; const copy: BlockDefinition = { ...block, id: newId, name: `${block.name} (Kopie)`, elements: block.elements.map(el => ({ ...el, id: `el_${Date.now()}_${Math.random().toString(36).slice(2, 7)}` })), }; this.blocks.set(newId, copy); return copy; } /** Create a block instance (reference) from a definition */ createInstance(blockId: string, x: number, y: number, layerId: string, rotation = 0, scale = 1): CADElement | null { const block = this.blocks.get(blockId); if (!block) return null; // Calculate bounding box from elements const bbox = this.getBoundingBox(block.elements); const w = (bbox.maxX - bbox.minX) * scale; const h = (bbox.maxY - bbox.minY) * scale; return { id: `el_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, type: 'block_instance', layerId, x, y, width: w, height: h, properties: { blockId, rotation, scale, offsetX: -bbox.minX * scale, offsetY: -bbox.minY * scale, }, }; } /** Get the elements of a block instance transformed to world coords */ getInstanceElements(instance: CADElement): CADElement[] { const blockId = instance.properties.blockId as string; const block = this.blocks.get(blockId); if (!block) return []; const rotation = (instance.properties.rotation || 0) * Math.PI / 180; const scale = instance.properties.scale || 1; const ox = instance.properties.offsetX || 0; const oy = instance.properties.offsetY || 0; return block.elements.map(el => { // Translate to instance origin, scale, rotate const lx = (el.x + Number(ox)) * scale; const ly = (el.y + Number(oy)) * scale; const rx = lx * Math.cos(rotation) - ly * Math.sin(rotation); const ry = lx * Math.sin(rotation) + ly * Math.cos(rotation); const props = { ...el.properties }; // Transform line endpoints if (props.x1 !== undefined && props.x2 !== undefined) { const x1 = (Number(props.x1) + Number(ox)) * scale; const y1 = (Number(props.y1) + Number(oy)) * scale; const x2 = (Number(props.x2) + Number(ox)) * scale; const y2 = (Number(props.y2) + Number(oy)) * scale; props.x1 = x1 * Math.cos(rotation) - y1 * Math.sin(rotation) + instance.x; props.y1 = x1 * Math.sin(rotation) + y1 * Math.cos(rotation) + instance.y; props.x2 = x2 * Math.cos(rotation) - y2 * Math.sin(rotation) + instance.x; props.y2 = x2 * Math.sin(rotation) + y2 * Math.cos(rotation) + instance.y; } return { ...el, id: `${el.id}_inst_${instance.id}`, x: rx + instance.x, y: ry + instance.y, width: el.width * scale, height: el.height * scale, properties: props, }; }); } /** Calculate bounding box of elements */ getBoundingBox(elements: CADElement[]): { minX: number; minY: number; maxX: number; maxY: number } { if (elements.length === 0) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const el of elements) { const x1 = el.properties.x1 ?? el.x - el.width / 2; const y1 = el.properties.y1 ?? el.y - el.height / 2; const x2 = el.properties.x2 ?? el.x + el.width / 2; const y2 = el.properties.y2 ?? el.y + el.height / 2; minX = Math.min(minX, x1, x2); minY = Math.min(minY, y1, y2); maxX = Math.max(maxX, x1, x2); maxY = Math.max(maxY, y1, y2); } return { minX, minY, maxX, maxY }; } /** Import SVG as block definition (parses basic shapes) */ importSVG(svgContent: string, name: string, category: string): BlockDefinition { const elements: CADElement[] = []; const parser = new DOMParser(); const doc = parser.parseFromString(svgContent, 'image/svg+xml'); const lines = doc.querySelectorAll('line'); lines.forEach((line, i) => { const x1 = parseFloat(line.getAttribute('x1') || '0'); const y1 = parseFloat(line.getAttribute('y1') || '0'); const x2 = parseFloat(line.getAttribute('x2') || '0'); const y2 = parseFloat(line.getAttribute('y2') || '0'); elements.push({ id: `svg_line_${i}`, type: 'line', layerId: '', x: (x1 + x2) / 2, y: (y1 + y2) / 2, width: Math.abs(x2 - x1), height: Math.abs(y2 - y1), properties: { x1, y1, x2, y2 }, }); }); const rects = doc.querySelectorAll('rect'); rects.forEach((rect, i) => { const x = parseFloat(rect.getAttribute('x') || '0'); const y = parseFloat(rect.getAttribute('y') || '0'); const w = parseFloat(rect.getAttribute('width') || '0'); const h = parseFloat(rect.getAttribute('height') || '0'); elements.push({ id: `svg_rect_${i}`, type: 'rect', layerId: '', x: x + w / 2, y: y + h / 2, width: w, height: h, properties: {}, }); }); const circles = doc.querySelectorAll('circle'); circles.forEach((circle, i) => { const cx = parseFloat(circle.getAttribute('cx') || '0'); const cy = parseFloat(circle.getAttribute('cy') || '0'); const r = parseFloat(circle.getAttribute('r') || '0'); elements.push({ id: `svg_circle_${i}`, type: 'circle', layerId: '', x: cx, y: cy, width: r * 2, height: r * 2, properties: { radius: r }, }); }); const blockId = `blk_svg_${Date.now()}`; const block: BlockDefinition = { id: blockId, name, description: `Imported from SVG`, category, elements, thumbnail: svgContent.substring(0, 200), }; this.blocks.set(blockId, block); return block; } /** Create a group block from selected elements */ createGroupBlock(name: string, elements: CADElement[], category: string = 'Custom'): BlockDefinition { const blockId = `blk_grp_${Date.now()}`; const block: BlockDefinition = { id: blockId, name, description: 'Aus Auswahl erstellt', category, elements: elements.map(el => ({ ...el, id: `${el.id}_def` })), }; this.blocks.set(blockId, block); return block; } } /** Default block definitions with real elements */ export function createDefaultBlocks(): BlockDefinition[] { return [ { id: 'blk-chair', name: 'Stuhl-Standard', description: 'Standard Stuhl 0.5×0.5m', category: 'Bestuhlung', elements: [ { id: 'chair-seat', type: 'rect', layerId: '', x: 0, y: 0, width: 50, height: 50, properties: { fill: '#4a90d9' } }, { id: 'chair-back', type: 'rect', layerId: '', x: 0, y: -30, width: 50, height: 10, properties: { fill: '#357abd' } }, ], }, { id: 'blk-chair-vip', name: 'Stuhl-VIP Polster', description: 'VIP Polsterstuhl 0.6×0.6m', category: 'Bestuhlung', elements: [ { id: 'vip-seat', type: 'rect', layerId: '', x: 0, y: 0, width: 60, height: 60, properties: { fill: '#8b5cf6' } }, { id: 'vip-back', type: 'rect', layerId: '', x: 0, y: -35, width: 60, height: 12, properties: { fill: '#7c3aed' } }, ], }, { id: 'blk-table-rect', name: 'Bankett-Tisch 1.8×0.8', description: 'Rechteckiger Bankett-Tisch', category: 'Tische', elements: [ { id: 'table-top', type: 'rect', layerId: '', x: 0, y: 0, width: 180, height: 80, properties: { fill: '#d4a574' } }, ], }, { id: 'blk-table-round', name: 'Runder Tisch 1.5m Ø', description: 'Runder Tisch für 8 Personen', category: 'Tische', elements: [ { id: 'round-top', type: 'circle', layerId: '', x: 0, y: 0, width: 150, height: 150, properties: { radius: 75, fill: '#d4a574' } }, ], }, { id: 'blk-stage', name: 'Hauptbühne 10×3m', description: 'Bühnenmodul', category: 'Bühne', elements: [ { id: 'stage-base', type: 'rect', layerId: '', x: 0, y: 0, width: 1000, height: 300, properties: { fill: '#444' } }, { id: 'stage-edge', type: 'rect', layerId: '', x: 0, y: 150, width: 1000, height: 10, properties: { fill: '#666' } }, ], }, { id: 'blk-door', name: 'Tür 90°', description: 'Drehtür 0.9×0.9m', category: 'Architektur', elements: [ { id: 'door-frame', type: 'rect', layerId: '', x: 0, y: 0, width: 90, height: 90, properties: {} }, { id: 'door-arc', type: 'arc', layerId: '', x: 0, y: 0, width: 90, height: 90, properties: { radius: 90, startAngle: 0, endAngle: 90 } }, ], }, ]; }