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

403 lines
11 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 { 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];
}
}