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

301 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 } },
],
},
];
}