2026-08-31 22:36:21 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Task CAD-9 – Stueckliste (BOM) fuer Traversen-Systeme.
|
|
|
|
|
|
*
|
|
|
|
|
|
* buildBOM gruppiert Traversen + Eck-Traversen nach Typ + Mass
|
|
|
|
|
|
* (Laenge x Breite), zaehlt Stueckzahlen und summiert die
|
|
|
|
|
|
* Gesamt-Traversemeter (Boxcorner zaehlt BEIDE Arme).
|
|
|
|
|
|
* exportBOMCsv: Semikolon-separierte CSV (Excel-DE-freundlich).
|
|
|
|
|
|
*/
|
|
|
|
|
|
import type { CADElement } from '../types/cad.types';
|
|
|
|
|
|
|
|
|
|
|
|
export interface BOMRow {
|
|
|
|
|
|
pos: number;
|
2026-09-13 01:47:31 +02:00
|
|
|
|
type: 'Traverse' | 'Eck-Traverse' | 'Kreis-Traverse';
|
|
|
|
|
|
/** Laenge in cm (Traverse: length; Eck-Traverse: armLength; Kreis: Durchmesser). */
|
2026-08-31 22:36:21 +02:00
|
|
|
|
length: number;
|
|
|
|
|
|
/** Breite in cm (trussWidth). */
|
|
|
|
|
|
width: number;
|
|
|
|
|
|
count: number;
|
2026-09-13 01:47:31 +02:00
|
|
|
|
/** Gesamtlaenge dieser Position in cm. */
|
2026-08-31 22:36:21 +02:00
|
|
|
|
totalCm: number;
|
2026-09-13 01:47:31 +02:00
|
|
|
|
/** Bezeichnung. */
|
2026-08-31 22:36:21 +02:00
|
|
|
|
label: string;
|
2026-09-13 01:47:31 +02:00
|
|
|
|
/** Gurtrohr-Anzahl (optional, CAD-11). */
|
|
|
|
|
|
chordCount?: number;
|
|
|
|
|
|
/** Eck-Winkel in Grad (optional, CAD-11). */
|
|
|
|
|
|
angle?: number;
|
2026-08-31 22:36:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface BOMResult {
|
|
|
|
|
|
rows: BOMRow[];
|
|
|
|
|
|
/** Gesamte Traversenlaenge in Metern (Ecken zaehlen beide Arme). */
|
|
|
|
|
|
totalLengthM: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface GroupKey {
|
2026-09-13 01:47:31 +02:00
|
|
|
|
type: 'Traverse' | 'Eck-Traverse' | 'Kreis-Traverse';
|
2026-08-31 22:36:21 +02:00
|
|
|
|
length: number;
|
|
|
|
|
|
width: number;
|
2026-09-13 01:47:31 +02:00
|
|
|
|
chordCount?: number;
|
|
|
|
|
|
angle?: number;
|
2026-08-31 22:36:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Baut die Stueckliste aus allen Zeichnungselementen.
|
|
|
|
|
|
* Nur truss- und boxcorner-Elemente fliessen ein; alles andere
|
|
|
|
|
|
* wird still ignoriert.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function buildBOM(elements: CADElement[]): BOMResult {
|
|
|
|
|
|
const groups = new Map<string, { key: GroupKey; count: number }>();
|
|
|
|
|
|
|
|
|
|
|
|
for (const el of elements) {
|
|
|
|
|
|
if (String(el.type) === 'truss' && el.properties.truss === true) {
|
|
|
|
|
|
const length = (el.properties.length as number | undefined) ?? el.width ?? 0;
|
|
|
|
|
|
const width = (el.properties.trussWidth as number | undefined) ?? el.height ?? 29;
|
2026-09-13 01:47:31 +02:00
|
|
|
|
const chordCount = (el.properties.chordCount as number | undefined) ?? 2;
|
|
|
|
|
|
const key: GroupKey = { type: 'Traverse', length, width, chordCount };
|
|
|
|
|
|
const id = `T|${length}|${width}|${chordCount}`;
|
2026-08-31 22:36:21 +02:00
|
|
|
|
const g = groups.get(id) ?? { key, count: 0 };
|
|
|
|
|
|
g.count++;
|
|
|
|
|
|
groups.set(id, g);
|
|
|
|
|
|
} else if (String(el.type) === 'boxcorner' && el.properties.boxcorner === true) {
|
|
|
|
|
|
const length = (el.properties.armLength as number | undefined) ?? 50;
|
|
|
|
|
|
const width = (el.properties.trussWidth as number | undefined) ?? 29;
|
2026-09-13 01:47:31 +02:00
|
|
|
|
const angle = (el.properties.angle as number | undefined) ?? 90;
|
|
|
|
|
|
const key: GroupKey = { type: 'Eck-Traverse', length, width, angle };
|
|
|
|
|
|
const id = `C|${length}|${width}|${angle}`;
|
|
|
|
|
|
const g = groups.get(id) ?? { key, count: 0 };
|
|
|
|
|
|
g.count++;
|
|
|
|
|
|
groups.set(id, g);
|
|
|
|
|
|
} else if (String(el.type) === 'circletruss' && el.properties.circletruss === true) {
|
|
|
|
|
|
const diameter = (el.properties.diameter as number | undefined) ?? el.width;
|
|
|
|
|
|
const width = (el.properties.trussWidth as number | undefined) ?? 29;
|
|
|
|
|
|
const chordCount = (el.properties.chordCount as number | undefined) ?? 2;
|
|
|
|
|
|
const key: GroupKey = { type: 'Kreis-Traverse', length: diameter, width, chordCount };
|
|
|
|
|
|
const id = `K|${diameter}|${width}|${chordCount}`;
|
2026-08-31 22:36:21 +02:00
|
|
|
|
const g = groups.get(id) ?? { key, count: 0 };
|
|
|
|
|
|
g.count++;
|
|
|
|
|
|
groups.set(id, g);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Sortierung: Traversen vor Ecken, innerhalb Typ nach Laenge absteigend
|
|
|
|
|
|
const entries = Array.from(groups.values());
|
|
|
|
|
|
entries.sort((a, b) => {
|
|
|
|
|
|
if (a.key.type !== b.key.type) {
|
|
|
|
|
|
return a.key.type === 'Traverse' ? -1 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return b.key.length - a.key.length;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const rows: BOMRow[] = entries.map((g, i) => {
|
|
|
|
|
|
// Ecken haben 2 Arme: Gesamtlaenge = count * length * 2
|
2026-09-13 01:47:31 +02:00
|
|
|
|
// Kreis-Traversen: Umfang = Pi * Durchmesser
|
|
|
|
|
|
const isCircle = g.key.type === 'Kreis-Traverse';
|
2026-08-31 22:36:21 +02:00
|
|
|
|
const armFactor = g.key.type === 'Eck-Traverse' ? 2 : 1;
|
2026-09-13 01:47:31 +02:00
|
|
|
|
const perPieceCm = isCircle ? Math.PI * g.key.length : g.key.length * armFactor;
|
|
|
|
|
|
const totalCm = g.count * perPieceCm;
|
|
|
|
|
|
const detailParts: string[] = [];
|
|
|
|
|
|
if (g.key.chordCount !== undefined && g.key.chordCount > 0) detailParts.push(`${g.key.chordCount}-rohr`);
|
|
|
|
|
|
if (g.key.angle !== undefined && g.key.angle !== 90) detailParts.push(`${g.key.angle}°`);
|
|
|
|
|
|
const detailSuffix = detailParts.length > 0 ? ` (${detailParts.join(', ')})` : '';
|
2026-08-31 22:36:21 +02:00
|
|
|
|
return {
|
|
|
|
|
|
pos: i + 1,
|
|
|
|
|
|
type: g.key.type,
|
|
|
|
|
|
length: g.key.length,
|
|
|
|
|
|
width: g.key.width,
|
|
|
|
|
|
count: g.count,
|
|
|
|
|
|
totalCm,
|
2026-09-13 01:47:31 +02:00
|
|
|
|
label: `${g.key.type} ${isCircle ? '⌀' : ''}${g.key.length} × ${g.key.width}${detailSuffix}`,
|
|
|
|
|
|
chordCount: g.key.chordCount,
|
|
|
|
|
|
angle: g.key.angle,
|
2026-08-31 22:36:21 +02:00
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const totalCmAll = rows.reduce((s, r) => s + r.totalCm, 0);
|
|
|
|
|
|
return {
|
|
|
|
|
|
rows,
|
|
|
|
|
|
totalLengthM: totalCmAll / 100,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Exportiert die Stueckliste als Semikolon-CSV (Excel-DE oeffnet
|
|
|
|
|
|
* das direkt mehrspaltig). Spalten: Pos;Typ;Laenge_cm;Breite_cm;
|
|
|
|
|
|
* Anzahl;Gesamt_cm;Bezeichnung + Summenzeile am Ende.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function exportBOMCsv(bom: BOMResult): string {
|
|
|
|
|
|
const lines: string[] = ['Pos;Typ;Laenge_cm;Breite_cm;Anzahl;Gesamt_cm;Bezeichnung'];
|
|
|
|
|
|
for (const r of bom.rows) {
|
|
|
|
|
|
lines.push(
|
|
|
|
|
|
`${r.pos};${r.type};${r.length};${r.width};${r.count};${r.totalCm};${r.label}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (bom.rows.length > 0) {
|
|
|
|
|
|
lines.push(`;;;SUMME;;;${bom.totalLengthM.toFixed(2)} m`);
|
|
|
|
|
|
}
|
|
|
|
|
|
return lines.join('\n');
|
|
|
|
|
|
}
|