feat: initial commit web-cad-neu with docker-compose, frontend and backend

This commit is contained in:
2026-06-26 10:50:24 +02:00
commit 4ec76fe406
102 changed files with 25722 additions and 0 deletions
+402
View File
@@ -0,0 +1,402 @@
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];
}
}