Files
web-cad/frontend/src/services/blockService.ts
T

301 lines
10 KiB
TypeScript
Raw Normal View History

import type { BlockDefinition, CADElement } from '../types/cad.types';
/**
* Block Service — CRUD, SVG-Import, Block-Definition vs Referenz
*/
export class BlockService {
private blocks: Map<string, BlockDefinition> = 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 } },
],
},
];
}