feat: initial commit web-cad-neu with docker-compose, frontend and backend
This commit is contained in:
@@ -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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user