403 lines
11 KiB
TypeScript
403 lines
11 KiB
TypeScript
import type { CADElement, CADProperties } from '../types/cad.types';
|
||
|
||
/**
|
||
* Seating Service — creates chairs, rows, blocks, tables, stages with configurable parameters.
|
||
* Also provides seat counting and template presets.
|
||
*/
|
||
|
||
export interface ChairConfig {
|
||
width: number;
|
||
height: number;
|
||
fill: string;
|
||
backrestColor: string;
|
||
outlineColor: string;
|
||
}
|
||
|
||
export const DEFAULT_CHAIR: ChairConfig = {
|
||
width: 40,
|
||
height: 40,
|
||
fill: '#4a90d9',
|
||
backrestColor: '#3a7ac9',
|
||
outlineColor: '#2a5a99',
|
||
};
|
||
|
||
export interface SeatingRowConfig {
|
||
count: number;
|
||
spacing: number;
|
||
rotation: number;
|
||
chairWidth: number;
|
||
chairHeight: number;
|
||
fill: string;
|
||
}
|
||
|
||
export const DEFAULT_ROW: SeatingRowConfig = {
|
||
count: 10,
|
||
spacing: 50,
|
||
rotation: 0,
|
||
chairWidth: 40,
|
||
chairHeight: 40,
|
||
fill: '#4a90d9',
|
||
};
|
||
|
||
export interface SeatingBlockConfig {
|
||
rows: number;
|
||
cols: number;
|
||
rowSpacing: number;
|
||
colSpacing: number;
|
||
rowOffset: number;
|
||
rotation: number;
|
||
chairWidth: number;
|
||
chairHeight: number;
|
||
fill: string;
|
||
}
|
||
|
||
export const DEFAULT_BLOCK: SeatingBlockConfig = {
|
||
rows: 5,
|
||
cols: 10,
|
||
rowSpacing: 50,
|
||
colSpacing: 50,
|
||
rowOffset: 0,
|
||
rotation: 0,
|
||
chairWidth: 40,
|
||
chairHeight: 40,
|
||
fill: '#4a90d9',
|
||
};
|
||
|
||
export interface TableConfig {
|
||
width: number;
|
||
height: number;
|
||
shape: 'rect' | 'round';
|
||
fill: string;
|
||
rotation: number;
|
||
}
|
||
|
||
export const DEFAULT_TABLE: TableConfig = {
|
||
width: 80,
|
||
height: 40,
|
||
shape: 'rect',
|
||
fill: '#8b6f47',
|
||
rotation: 0,
|
||
};
|
||
|
||
export interface StageConfig {
|
||
width: number;
|
||
height: number;
|
||
fill: string;
|
||
rotation: number;
|
||
label: string;
|
||
}
|
||
|
||
export const DEFAULT_STAGE: StageConfig = {
|
||
width: 200,
|
||
height: 60,
|
||
fill: '#2c3e50',
|
||
rotation: 0,
|
||
label: 'Bühne',
|
||
};
|
||
|
||
export interface SeatingTemplate {
|
||
name: string;
|
||
description: string;
|
||
type: 'row' | 'block' | 'mixed';
|
||
config: Record<string, unknown>;
|
||
}
|
||
|
||
export const SEATING_TEMPLATES: SeatingTemplate[] = [
|
||
{
|
||
name: 'Kleine Reihe',
|
||
description: '5 Stühle in einer Reihe',
|
||
type: 'row',
|
||
config: { count: 5, spacing: 50, rotation: 0 },
|
||
},
|
||
{
|
||
name: 'Mittlere Reihe',
|
||
description: '10 Stühle in einer Reihe',
|
||
type: 'row',
|
||
config: { count: 10, spacing: 50, rotation: 0 },
|
||
},
|
||
{
|
||
name: 'Große Reihe',
|
||
description: '20 Stühle in einer Reihe',
|
||
type: 'row',
|
||
config: { count: 20, spacing: 50, rotation: 0 },
|
||
},
|
||
{
|
||
name: 'Kleiner Block',
|
||
description: '3×5 Stühle',
|
||
type: 'block',
|
||
config: { rows: 3, cols: 5, rowSpacing: 50, colSpacing: 50, rowOffset: 0 },
|
||
},
|
||
{
|
||
name: 'Mittlerer Block',
|
||
description: '5×10 Stühle',
|
||
type: 'block',
|
||
config: { rows: 5, cols: 10, rowSpacing: 50, colSpacing: 50, rowOffset: 0 },
|
||
},
|
||
{
|
||
name: 'Großer Block',
|
||
description: '8×15 Stühle',
|
||
type: 'block',
|
||
config: { rows: 8, cols: 15, rowSpacing: 50, colSpacing: 50, rowOffset: 0 },
|
||
},
|
||
{
|
||
name: 'Konzertsaal',
|
||
description: '3 Blöcke mit Gängen: 5×8, 5×12, 5×8',
|
||
type: 'mixed',
|
||
config: {
|
||
blocks: [
|
||
{ rows: 5, cols: 8, offsetX: 0, rowSpacing: 50, colSpacing: 50 },
|
||
{ rows: 5, cols: 12, offsetX: 500, rowSpacing: 50, colSpacing: 50 },
|
||
{ rows: 5, cols: 8, offsetX: 1200, rowSpacing: 50, colSpacing: 50 },
|
||
],
|
||
},
|
||
},
|
||
{
|
||
name: 'Theater',
|
||
description: '10 Reihen mit 15 Stühlen, versetzt',
|
||
type: 'block',
|
||
config: { rows: 10, cols: 15, rowSpacing: 55, colSpacing: 50, rowOffset: 25 },
|
||
},
|
||
];
|
||
|
||
export class SeatingService {
|
||
private generateId(): string {
|
||
return `el_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
||
}
|
||
|
||
/** Create a single chair element */
|
||
createChair(x: number, y: number, layerId: string, config: Partial<ChairConfig> = {}): CADElement {
|
||
const cfg = { ...DEFAULT_CHAIR, ...config };
|
||
return {
|
||
id: this.generateId(),
|
||
type: 'chair',
|
||
layerId,
|
||
x,
|
||
y,
|
||
width: cfg.width,
|
||
height: cfg.height,
|
||
properties: {
|
||
rotation: 0,
|
||
fill: cfg.fill,
|
||
backrestColor: cfg.backrestColor,
|
||
outlineColor: cfg.outlineColor,
|
||
seatType: 'standard',
|
||
},
|
||
};
|
||
}
|
||
|
||
/** Create a seating row (multiple chairs in a line) */
|
||
createSeatingRow(x: number, y: number, layerId: string, config: Partial<SeatingRowConfig> = {}): CADElement[] {
|
||
const cfg = { ...DEFAULT_ROW, ...config };
|
||
const elements: CADElement[] = [];
|
||
const totalWidth = (cfg.count - 1) * cfg.spacing + cfg.chairWidth;
|
||
const startX = x - totalWidth / 2 + cfg.chairWidth / 2;
|
||
|
||
for (let i = 0; i < cfg.count; i++) {
|
||
const cx = startX + i * cfg.spacing;
|
||
const cy = y;
|
||
// Apply rotation around center
|
||
const rot = cfg.rotation * Math.PI / 180;
|
||
const dx = cx - x;
|
||
const dy = cy - y;
|
||
const rx = dx * Math.cos(rot) - dy * Math.sin(rot) + x;
|
||
const ry = dx * Math.sin(rot) + dy * Math.cos(rot) + y;
|
||
|
||
elements.push({
|
||
id: this.generateId(),
|
||
type: 'chair',
|
||
layerId,
|
||
x: rx,
|
||
y: ry,
|
||
width: cfg.chairWidth,
|
||
height: cfg.chairHeight,
|
||
properties: {
|
||
rotation: cfg.rotation,
|
||
fill: cfg.fill,
|
||
backrestColor: '#3a7ac9',
|
||
outlineColor: '#2a5a99',
|
||
seatType: 'standard',
|
||
rowIndex: i,
|
||
rowId: `row_${Date.now()}`,
|
||
},
|
||
});
|
||
}
|
||
return elements;
|
||
}
|
||
|
||
/** Create a seating block (rows × cols of chairs) */
|
||
createSeatingBlock(x: number, y: number, layerId: string, config: Partial<SeatingBlockConfig> = {}): CADElement[] {
|
||
const cfg = { ...DEFAULT_BLOCK, ...config };
|
||
const elements: CADElement[] = [];
|
||
const totalW = (cfg.cols - 1) * cfg.colSpacing + cfg.chairWidth;
|
||
const totalH = (cfg.rows - 1) * cfg.rowSpacing + cfg.chairHeight;
|
||
const startX = x - totalW / 2 + cfg.chairWidth / 2;
|
||
const startY = y - totalH / 2 + cfg.chairHeight / 2;
|
||
const rot = cfg.rotation * Math.PI / 180;
|
||
const blockId = `block_${Date.now()}`;
|
||
|
||
for (let row = 0; row < cfg.rows; row++) {
|
||
const offset = cfg.rowOffset * (row % 2);
|
||
for (let col = 0; col < cfg.cols; col++) {
|
||
const lx = startX + col * cfg.colSpacing + offset;
|
||
const ly = startY + row * cfg.rowSpacing;
|
||
// Rotate around center
|
||
const dx = lx - x;
|
||
const dy = ly - y;
|
||
const rx = dx * Math.cos(rot) - dy * Math.sin(rot) + x;
|
||
const ry = dx * Math.sin(rot) + dy * Math.cos(rot) + y;
|
||
|
||
elements.push({
|
||
id: this.generateId(),
|
||
type: 'chair',
|
||
layerId,
|
||
x: rx,
|
||
y: ry,
|
||
width: cfg.chairWidth,
|
||
height: cfg.chairHeight,
|
||
properties: {
|
||
rotation: cfg.rotation,
|
||
fill: cfg.fill,
|
||
backrestColor: '#3a7ac9',
|
||
outlineColor: '#2a5a99',
|
||
seatType: 'standard',
|
||
rowIndex: row,
|
||
colIndex: col,
|
||
blockId,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
return elements;
|
||
}
|
||
|
||
/** Create a table element */
|
||
createTable(x: number, y: number, layerId: string, config: Partial<TableConfig> = {}): CADElement {
|
||
const cfg = { ...DEFAULT_TABLE, ...config };
|
||
return {
|
||
id: this.generateId(),
|
||
type: 'table',
|
||
layerId,
|
||
x,
|
||
y,
|
||
width: cfg.width,
|
||
height: cfg.height,
|
||
properties: {
|
||
rotation: cfg.rotation,
|
||
fill: cfg.fill,
|
||
shape: cfg.shape,
|
||
stroke: '#5a4a37',
|
||
strokeWidth: 1.5,
|
||
},
|
||
};
|
||
}
|
||
|
||
/** Create a stage element */
|
||
createStage(x: number, y: number, layerId: string, config: Partial<StageConfig> = {}): CADElement {
|
||
const cfg = { ...DEFAULT_STAGE, ...config };
|
||
return {
|
||
id: this.generateId(),
|
||
type: 'stage',
|
||
layerId,
|
||
x,
|
||
y,
|
||
width: cfg.width,
|
||
height: cfg.height,
|
||
properties: {
|
||
rotation: cfg.rotation,
|
||
fill: cfg.fill,
|
||
stroke: '#1a2e3f',
|
||
strokeWidth: 2,
|
||
label: cfg.label,
|
||
},
|
||
};
|
||
}
|
||
|
||
/** Create elements from a template */
|
||
createFromTemplate(templateName: string, x: number, y: number, layerId: string): CADElement[] {
|
||
const template = SEATING_TEMPLATES.find(t => t.name === templateName);
|
||
if (!template) return [];
|
||
|
||
if (template.type === 'row') {
|
||
return this.createSeatingRow(x, y, layerId, template.config as Partial<SeatingRowConfig>);
|
||
}
|
||
if (template.type === 'block') {
|
||
return this.createSeatingBlock(x, y, layerId, template.config as Partial<SeatingBlockConfig>);
|
||
}
|
||
if (template.type === 'mixed') {
|
||
const blocks = (template.config as { blocks: Array<Record<string, number>> }).blocks;
|
||
const elements: CADElement[] = [];
|
||
for (const blk of blocks) {
|
||
const bx = x + (blk.offsetX || 0);
|
||
const cfg: Partial<SeatingBlockConfig> = {
|
||
rows: blk.rows,
|
||
cols: blk.cols,
|
||
rowSpacing: blk.rowSpacing || 50,
|
||
colSpacing: blk.colSpacing || 50,
|
||
rowOffset: 0,
|
||
};
|
||
elements.push(...this.createSeatingBlock(bx, y, layerId, cfg));
|
||
}
|
||
return elements;
|
||
}
|
||
return [];
|
||
}
|
||
|
||
/** Count seats in a list of elements */
|
||
countSeats(elements: CADElement[]): { total: number; byRow: Record<string, number>; byBlock: Record<string, number> } {
|
||
let total = 0;
|
||
const byRow: Record<string, number> = {};
|
||
const byBlock: Record<string, number> = {};
|
||
|
||
for (const el of elements) {
|
||
if (el.type === 'chair') {
|
||
total++;
|
||
const rowId = el.properties.rowId as string | undefined;
|
||
const blockId = el.properties.blockId as string | undefined;
|
||
if (rowId) {
|
||
byRow[rowId] = (byRow[rowId] || 0) + 1;
|
||
}
|
||
if (blockId) {
|
||
byBlock[blockId] = (byBlock[blockId] || 0) + 1;
|
||
}
|
||
}
|
||
}
|
||
return { total, byRow, byBlock };
|
||
}
|
||
|
||
/** Get all chairs belonging to a specific row */
|
||
getRowElements(elements: CADElement[], rowId: string): CADElement[] {
|
||
return elements.filter(el => el.type === 'chair' && el.properties.rowId === rowId);
|
||
}
|
||
|
||
/** Get all chairs belonging to a specific block */
|
||
getBlockElements(elements: CADElement[], blockId: string): CADElement[] {
|
||
return elements.filter(el => el.type === 'chair' && el.properties.blockId === blockId);
|
||
}
|
||
|
||
/** Add a chair to an existing row at the end */
|
||
addChairToRow(elements: CADElement[], rowId: string, layerId: string): CADElement | null {
|
||
const rowChairs = this.getRowElements(elements, rowId);
|
||
if (rowChairs.length === 0) return null;
|
||
const last = rowChairs[rowChairs.length - 1];
|
||
const spacing = 50;
|
||
const rot = (last.properties.rotation || 0) * Math.PI / 180;
|
||
const dx = spacing;
|
||
const dy = 0;
|
||
const rx = dx * Math.cos(rot) - dy * Math.sin(rot) + last.x;
|
||
const ry = dx * Math.sin(rot) + dy * Math.cos(rot) + last.y;
|
||
return this.createChair(rx, ry, layerId, {
|
||
width: last.width,
|
||
height: last.height,
|
||
fill: last.properties.fill as string,
|
||
});
|
||
}
|
||
|
||
/** Remove a chair from a row by index */
|
||
removeChairFromRow(elements: CADElement[], rowId: string, index: number): string[] {
|
||
const rowChairs = this.getRowElements(elements, rowId);
|
||
if (index < 0 || index >= rowChairs.length) return [];
|
||
const sorted = rowChairs.sort((a, b) => (a.properties.rowIndex as number) - (b.properties.rowIndex as number));
|
||
return [sorted[index].id];
|
||
}
|
||
}
|