301 lines
10 KiB
TypeScript
301 lines
10 KiB
TypeScript
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 } },
|
||
],
|
||
},
|
||
];
|
||
}
|