task(CAD-11): truss professional upgrade - chordCount (1-4 belt tubes) with adjustable rendering, free length input (10-1000cm), variable angle corners (15-165), circle truss tool with diameter option, BOM with chordCount grouping + Pi*d circumference

This commit is contained in:
Agent Zero
2026-09-13 01:47:31 +02:00
parent 8141be4b80
commit 875dbd6d22
6 changed files with 441 additions and 60 deletions
@@ -511,6 +511,7 @@ function makeTrussElement(
rotationDeg: number,
layerId: string,
trussWidth = 29,
chordCount = 2,
): CADElement {
const rad = (rotationDeg * Math.PI) / 180;
const w = Math.abs(length * Math.cos(rad)) + Math.abs(trussWidth * Math.sin(rad));
@@ -524,7 +525,7 @@ function makeTrussElement(
width: Math.max(w, trussWidth),
height: Math.max(h, trussWidth),
properties: {
length, rotation: rotationDeg, trussWidth, truss: true,
length, rotation: rotationDeg, trussWidth, chordCount, truss: true,
stroke: '#9aa0a6', strokeWidth: 2, fill: '#6b7280',
},
} as unknown as CADElement;
@@ -542,13 +543,9 @@ export const trussTool: ToolExtensionV2 = {
description: 'Traverse platzieren: Drag = Richtung, dockt magnetisch an bestehenden Traversen an (CAD-7)',
},
optionsSchema: [
{ key: 'length', label: 'Länge (cm)', type: 'select', options: [
{ value: '50', label: '50 cm' },
{ value: '100', label: '100 cm' },
{ value: '200', label: '200 cm' },
{ value: '300', label: '300 cm' },
] },
{ key: 'length', label: 'Länge (cm)', type: 'number', min: 10, max: 1000 },
{ key: 'trussWidth', label: 'Breite (cm)', type: 'number', min: 10, max: 60 },
{ key: 'chordCount', label: 'Gurtrohre', type: 'number', min: 1, max: 4 },
],
handlers: {
down(e, ctx) {
@@ -558,22 +555,23 @@ export const trussTool: ToolExtensionV2 = {
move(e, ctx) {
const c = ctx as FullToolContext;
if (!trussDragStart) return;
const length = Number((c.options.length as string | undefined) ?? 200) || 200;
const length = Number((c.options.length as number | string | undefined) ?? 200) || 200;
const trussWidth = (c.options.trussWidth as number | undefined) ?? 29;
const chordCount = Math.max(1, Math.min(4, (c.options.chordCount as number | undefined) ?? 2));
// Magnet-Vorschau: dockt der Cursor an ein Traversen-/Ecken-Ende?
const existing = c.doc ? (c.doc.getAllElements() as CADElement[]).filter(
(el) => String(el.type) === 'truss' || String(el.type) === 'boxcorner',
) : [];
const dock = snapTrussToDock(existing, trussDragStart, length, DOCK_TOLERANCE);
if (dock) {
ctx.setPreview?.(makeTrussElement(dock.center, length, dock.rotationDeg, 'layer-0', trussWidth));
ctx.setPreview?.(makeTrussElement(dock.center, length, dock.rotationDeg, 'layer-0', trussWidth, chordCount));
return;
}
// Frei: Richtung aus Drag
const ang = (Math.atan2(e.world.y - trussDragStart.y, e.world.x - trussDragStart.x) * 180) / Math.PI;
const cx = (trussDragStart.x + e.world.x) / 2;
const cy = (trussDragStart.y + e.world.y) / 2;
ctx.setPreview?.(makeTrussElement({ x: cx, y: cy }, length, ang, 'layer-0'));
ctx.setPreview?.(makeTrussElement({ x: cx, y: cy }, length, ang, 'layer-0', trussWidth, chordCount));
},
up(e, ctx) {
const c = ctx as FullToolContext;
@@ -583,6 +581,7 @@ export const trussTool: ToolExtensionV2 = {
if (!start || !c.doc) return;
const length = Number((c.options.length as string | undefined) ?? 200) || 200;
const trussWidth = (c.options.trussWidth as number | undefined) ?? 29;
const chordCount = Math.max(1, Math.min(4, (c.options.chordCount as number | undefined) ?? 2));
const layerId = (c.options.layerId as string | undefined) ?? 'layer-0';
// MAGNETISCH: Dock-Pruefung am Drag-Startpunkt (Traversen UND Ecken)
const existing = (c.doc.getAllElements() as CADElement[]).filter(
@@ -591,13 +590,13 @@ export const trussTool: ToolExtensionV2 = {
const dock = snapTrussToDock(existing, start, length, DOCK_TOLERANCE);
let el: CADElement;
if (dock) {
el = makeTrussElement(dock.center, length, dock.rotationDeg, layerId, trussWidth);
el = makeTrussElement(dock.center, length, dock.rotationDeg, layerId, trussWidth, chordCount);
ctx.setStatus(`Traverse angedockt an ${dock.dockedTo} (${length}cm, Breite ${trussWidth}cm)`);
} else {
const ang = (Math.atan2(e.world.y - start.y, e.world.x - start.x) * 180) / Math.PI;
const cx = (start.x + e.world.x) / 2;
const cy = (start.y + e.world.y) / 2;
el = makeTrussElement({ x: cx, y: cy }, length, ang, layerId, trussWidth);
el = makeTrussElement({ x: cx, y: cy }, length, ang, layerId, trussWidth, chordCount);
ctx.setStatus(`Traverse platziert (${length}cm, Breite ${trussWidth}cm)`);
}
c.doc.transact(() => { c.doc!.addElement(el); });
@@ -621,8 +620,9 @@ export const trussTool: ToolExtensionV2 = {
export function boxcornerEndpoints(el: CADElement): [Pt, Pt] {
const armLength = (el.properties.armLength as number | undefined) ?? 50;
const rotDeg = (el.properties.rotation as number | undefined) ?? 0;
const angle = (el.properties.angle as number | undefined) ?? 90;
const rot = (rotDeg * Math.PI) / 180;
const rot2 = ((rotDeg + 90) * Math.PI) / 180;
const rot2 = ((rotDeg + angle) * Math.PI) / 180;
return [
{ x: el.x + Math.cos(rot) * armLength, y: el.y + Math.sin(rot) * armLength },
{ x: el.x + Math.cos(rot2) * armLength, y: el.y + Math.sin(rot2) * armLength },
@@ -674,6 +674,7 @@ function makeBoxcornerElement(
vertex: Pt,
armLength: number,
rotationDeg: number,
angleDeg: number,
trussWidth: number,
layerId: string,
): CADElement {
@@ -686,7 +687,7 @@ function makeBoxcornerElement(
width: armLength + trussWidth,
height: armLength + trussWidth,
properties: {
armLength, trussWidth, rotation: rotationDeg, boxcorner: true,
armLength, trussWidth, rotation: rotationDeg, angle: angleDeg, boxcorner: true,
stroke: '#9aa0a6', strokeWidth: 2, fill: '#6b7280',
},
} as unknown as CADElement;
@@ -705,6 +706,7 @@ export const boxCornerTool: ToolExtensionV2 = {
},
optionsSchema: [
{ key: 'armLength', label: 'Armlänge (cm)', type: 'number', min: 20, max: 150 },
{ key: 'angle', label: 'Winkel (°)', type: 'number', min: 15, max: 165 },
{ key: 'trussWidth', label: 'Breite (cm)', type: 'number', min: 10, max: 60 },
],
handlers: {
@@ -716,17 +718,18 @@ export const boxCornerTool: ToolExtensionV2 = {
const c = ctx as FullToolContext;
if (!cornerDragStart) return;
const armLength = (c.options.armLength as number | undefined) ?? 50;
const cornerAngle = (c.options.angle as number | undefined) ?? 90;
const trussWidth = (c.options.trussWidth as number | undefined) ?? 29;
const existing = c.doc ? (c.doc.getAllElements() as CADElement[]).filter(
(el) => String(el.type) === 'truss' || String(el.type) === 'boxcorner',
) : [];
const dock = cornerDockToDock(existing, cornerDragStart, DOCK_TOLERANCE);
if (dock) {
ctx.setPreview?.(makeBoxcornerElement(dock.vertex, armLength, dock.rotationDeg, trussWidth, 'layer-0'));
ctx.setPreview?.(makeBoxcornerElement(dock.vertex, armLength, dock.rotationDeg, cornerAngle, trussWidth, 'layer-0'));
return;
}
const ang = (Math.atan2(e.world.y - cornerDragStart.y, e.world.x - cornerDragStart.x) * 180) / Math.PI;
ctx.setPreview?.(makeBoxcornerElement(cornerDragStart, armLength, ang, trussWidth, 'layer-0'));
ctx.setPreview?.(makeBoxcornerElement(cornerDragStart, armLength, ang, cornerAngle, trussWidth, 'layer-0'));
},
up(e, ctx) {
const c = ctx as FullToolContext;
@@ -735,6 +738,7 @@ export const boxCornerTool: ToolExtensionV2 = {
ctx.setPreview?.(null);
if (!start || !c.doc) return;
const armLength = (c.options.armLength as number | undefined) ?? 50;
const cornerAngle = (c.options.angle as number | undefined) ?? 90;
const trussWidth = (c.options.trussWidth as number | undefined) ?? 29;
const layerId = (c.options.layerId as string | undefined) ?? 'layer-0';
const existing = (c.doc.getAllElements() as CADElement[]).filter(
@@ -743,11 +747,11 @@ export const boxCornerTool: ToolExtensionV2 = {
const dock = cornerDockToDock(existing, start, DOCK_TOLERANCE);
let el: CADElement;
if (dock) {
el = makeBoxcornerElement(dock.vertex, armLength, dock.rotationDeg, trussWidth, layerId);
el = makeBoxcornerElement(dock.vertex, armLength, dock.rotationDeg, cornerAngle, trussWidth, layerId);
ctx.setStatus(`Eck-Traverse angedockt an ${dock.dockedTo}`);
} else {
const ang = (Math.atan2(e.world.y - start.y, e.world.x - start.x) * 180) / Math.PI;
el = makeBoxcornerElement(start, armLength, ang, trussWidth, layerId);
el = makeBoxcornerElement(start, armLength, ang, cornerAngle, trussWidth, layerId);
ctx.setStatus(`Eck-Traverse platziert (${armLength}cm Arme, Breite ${trussWidth}cm)`);
}
c.doc.transact(() => { c.doc!.addElement(el); });
@@ -760,6 +764,58 @@ export const boxCornerTool: ToolExtensionV2 = {
},
};
// Task CAD-11: Kreis-Traverse
function makeCircleTrussElement(
center: Pt,
diameter: number,
trussWidth: number,
chordCount: number,
layerId: string,
): CADElement {
return {
id: `circletruss_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`,
type: 'circletruss',
layerId,
x: center.x,
y: center.y,
width: diameter,
height: diameter,
properties: {
diameter, trussWidth, chordCount, circletruss: true,
stroke: '#9aa0a6', strokeWidth: 2, fill: 'none',
},
} as unknown as CADElement;
}
export const circleTrussTool: ToolExtensionV2 = {
manifest: {
id: 'circle-truss',
label: 'Kreis-Traverse',
icon: '○',
ribbonTab: 'insert',
tags: ['basic'],
description: 'Kreis-Traverse platzieren (CAD-11)',
},
optionsSchema: [
{ key: 'diameter', label: 'Durchmesser (cm)', type: 'number', min: 50, max: 1500 },
{ key: 'trussWidth', label: 'Breite (cm)', type: 'number', min: 10, max: 60 },
{ key: 'chordCount', label: 'Gurtrohre', type: 'number', min: 1, max: 4 },
],
handlers: {
down(e, ctx) {
const c = ctx as FullToolContext;
if (!c.doc) return;
const diameter = Math.max(50, (c.options.diameter as number | undefined) ?? 200);
const trussWidth = (c.options.trussWidth as number | undefined) ?? 29;
const chordCount = Math.max(1, Math.min(4, (c.options.chordCount as number | undefined) ?? 2));
const layerId = (c.options.layerId as string | undefined) ?? 'layer-0';
const el = makeCircleTrussElement(e.world, diameter, trussWidth, chordCount, layerId);
c.doc.transact(() => { c.doc!.addElement(el); });
ctx.setStatus('Kreis-Traverse platziert');
},
},
};
export const eventToolsV2Plugin: PluginV2 = {
manifest: {
id: 'event-tools',
@@ -770,5 +826,5 @@ export const eventToolsV2Plugin: PluginV2 = {
category: 'elements',
enabledByDefault: true,
},
tools: [chairTool, seatingRowToolDrag, seatingBlockToolDrag, tableTool, stageTool, arcRowTool, kinoTemplateTool, banquetTemplateTool, standingTableTemplateTool, podiumTemplateTool, curtainTool, spotlightTool, barrierTool, trussTool, boxCornerTool],
tools: [chairTool, seatingRowToolDrag, seatingBlockToolDrag, tableTool, stageTool, arcRowTool, kinoTemplateTool, banquetTemplateTool, standingTableTemplateTool, podiumTemplateTool, curtainTool, spotlightTool, barrierTool, trussTool, boxCornerTool, circleTrussTool],
};
+69 -25
View File
@@ -400,14 +400,16 @@ export function elementToPrimitives(el: CADElement, attrDefs?: Array<{ tag: stri
}
case 'truss': {
// Traverse (CAD-7/8): ROTIERTES Rechteck-Polygon + Gurtlinien
// entlang der Richtung (Fachwerk-Profil), Breite = trussWidth.
// Traverse (CAD-7/8/11): ROTIERTES Rechteck-Polygon + Gurtlinien
// entlang der Richtung (Fachwerk-Profil), Breite = trussWidth,
// chordCount = Anzahl Gurtrohre (CAD-11).
const length = (props.length as number | undefined) ?? el.width;
const tWidth = (props.trussWidth as number | undefined) ?? el.height ?? 29;
const chordCount = Math.max(1, Math.min(4, (props.chordCount as number | undefined) ?? 2));
const rotDeg = (props.rotation as number | undefined) ?? 0;
const rad = (rotDeg * Math.PI) / 180;
const ux = Math.cos(rad), uy = Math.sin(rad); // Laengsrichtung
const nx = -Math.sin(rad), ny = Math.cos(rad); // Normalenrichtung
const ux = Math.cos(rad), uy = Math.sin(rad);
const nx = -Math.sin(rad), ny = Math.cos(rad);
const hl = length / 2, hw = tWidth / 2;
const c = { x: el.x, y: el.y };
const corner = (su: number, sv: number): { x: number; y: number } => ({
@@ -424,45 +426,58 @@ export function elementToPrimitives(el: CADElement, attrDefs?: Array<{ tag: stri
stroke: strokeCol,
strokeWidth: 2,
},
// Gurtlinien oben/unten
{
kind: 'line',
from: corner(-1, -1),
to: corner(1, -1),
stroke: '#4b5563', strokeWidth: 1,
},
{
kind: 'line',
from: corner(-1, 1),
to: corner(1, 1),
stroke: '#4b5563', strokeWidth: 1,
},
];
// Gurtrohre: chordCount Linien gleichmaessig ueber die Breite verteilt
// chordCount=1: Mittellinie, chordCount=2: bei ±hw/2, chordCount=4: bei ±hw/2 und ±hw/6
for (let ci = 0; ci < chordCount; ci++) {
let sv: number;
if (chordCount === 1) sv = 0;
else if (chordCount === 2) sv = ci === 0 ? -0.5 : 0.5;
else if (chordCount === 3) sv = ci === 0 ? -0.67 : ci === 1 ? 0 : 0.67;
else sv = -0.75 + ci * 0.5; // chordCount=4: -0.75, -0.25, 0.25, 0.75
const chordPt = (su: number): { x: number; y: number } => ({
x: c.x + ux * (hl * su) + nx * (hw * sv),
y: c.y + uy * (hl * su) + ny * (hw * sv),
});
const from = chordPt(-1);
const to = chordPt(1);
out.push({ kind: 'line', from, to, stroke: '#4b5563', strokeWidth: 1 });
}
return out;
}
case 'boxcorner': {
// Eck-Traverse (CAD-8): L-foermiges Polygon (6 Punkte) aus zwei
// Armen (rot und rot+90) um den Eckpunkt, Breite = trussWidth.
// Eck-Traverse (CAD-8/11): L-foermiges Polygon (6 Punkte) aus zwei
// Armen (rot und rot+angle) um den Eckpunkt, Breite = trussWidth,
// Winkel = angle (variabel, CAD-11, Default 90).
const armLength = (props.armLength as number | undefined) ?? 50;
const bWidth = (props.trussWidth as number | undefined) ?? 29;
const rotDeg = (props.rotation as number | undefined) ?? 0;
const angleDeg = (props.angle as number | undefined) ?? 90;
const rad = (rotDeg * Math.PI) / 180;
const rad2 = ((rotDeg + angleDeg) * Math.PI) / 180;
const ux = Math.cos(rad), uy = Math.sin(rad);
const nx = -Math.sin(rad), ny = Math.cos(rad);
const v = { x: el.x, y: el.y };
const vx = { x: el.x, y: el.y };
const px = (a: number, b: number): { x: number; y: number } => ({
x: v.x + ux * a + nx * b,
y: v.y + uy * a + ny * b,
x: vx.x + ux * a + nx * b,
y: vx.y + uy * a + ny * b,
});
// L-Geometrie: Arm1 entlang +u, Arm2 entlang +n (rot+90)
const ax2 = Math.cos(rad2), ay2 = Math.sin(rad2);
const pxa = (a: number, b: number): { x: number; y: number } => ({
x: vx.x + ax2 * a - ay2 * b,
y: vx.y + ay2 * a + ax2 * b,
});
// L-Geometrie: Arm1 entlang +u (rot), Arm2 entlang rot+angle
const pts = [
px(0, 0),
px(armLength, 0),
px(armLength, bWidth),
px(bWidth, bWidth),
px(bWidth, armLength),
px(0, armLength),
pxa(bWidth, 0),
pxa(armLength, 0),
pxa(0, 0),
px(0, bWidth),
];
const strokeCol = (props.stroke as string | undefined) ?? '#9aa0a6';
return [{
@@ -474,6 +489,35 @@ export function elementToPrimitives(el: CADElement, attrDefs?: Array<{ tag: stri
}];
}
case 'circletruss': {
// Kreis-Traverse (CAD-11): Aussenkreis + Innenkreis +
// chordCount Gurtrohr-Kreise gleichmaessig verteilt.
const diameter = (props.diameter as number | undefined) ?? el.width;
const tWidth = (props.trussWidth as number | undefined) ?? 29;
const chordCount = Math.max(1, Math.min(4, (props.chordCount as number | undefined) ?? 2));
const rOuter = diameter / 2;
const rInner = rOuter - tWidth;
const strokeCol = (props.stroke as string | undefined) ?? '#9aa0a6';
const out: ShapePrimitive[] = [
{ kind: 'circle', center: { x: el.x, y: el.y }, radius: rOuter, fill: null, stroke: strokeCol, strokeWidth: 2 },
{ kind: 'circle', center: { x: el.x, y: el.y }, radius: rInner, fill: null, stroke: strokeCol, strokeWidth: 1 },
];
// Gurtrohr-Kreise: gleichmaessig zwischen Aussen- und Innenkreis
const rMid = (rOuter + rInner) / 2;
for (let ci = 0; ci < chordCount; ci++) {
const ang = (2 * Math.PI * ci) / chordCount;
out.push({
kind: 'circle',
center: { x: el.x + Math.cos(ang) * rMid, y: el.y + Math.sin(ang) * rMid },
radius: tWidth * 0.15,
fill: null,
stroke: '#4b5563',
strokeWidth: 1,
});
}
return out;
}
case 'barrier': {
// Absperrung (Task E7): Kette aus Kreis-Paaren (properties.circles).
const circles = Array.isArray(props.circles)
+37 -11
View File
@@ -10,16 +10,20 @@ import type { CADElement } from '../types/cad.types';
export interface BOMRow {
pos: number;
type: 'Traverse' | 'Eck-Traverse';
/** Laenge in cm (Traverse: length; Eck-Traverse: armLength). */
type: 'Traverse' | 'Eck-Traverse' | 'Kreis-Traverse';
/** Laenge in cm (Traverse: length; Eck-Traverse: armLength; Kreis: Durchmesser). */
length: number;
/** Breite in cm (trussWidth). */
width: number;
count: number;
/** Gesamtlaenge dieser Position in cm (count * length, Ecken * 2). */
/** Gesamtlaenge dieser Position in cm. */
totalCm: number;
/** Bezeichnung, z.B. "Traverse 200 x 29". */
/** Bezeichnung. */
label: string;
/** Gurtrohr-Anzahl (optional, CAD-11). */
chordCount?: number;
/** Eck-Winkel in Grad (optional, CAD-11). */
angle?: number;
}
export interface BOMResult {
@@ -29,9 +33,11 @@ export interface BOMResult {
}
interface GroupKey {
type: 'Traverse' | 'Eck-Traverse';
type: 'Traverse' | 'Eck-Traverse' | 'Kreis-Traverse';
length: number;
width: number;
chordCount?: number;
angle?: number;
}
/**
@@ -46,16 +52,27 @@ export function buildBOM(elements: CADElement[]): BOMResult {
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;
const key: GroupKey = { type: 'Traverse', length, width };
const id = `T|${length}|${width}`;
const chordCount = (el.properties.chordCount as number | undefined) ?? 2;
const key: GroupKey = { type: 'Traverse', length, width, chordCount };
const id = `T|${length}|${width}|${chordCount}`;
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;
const key: GroupKey = { type: 'Eck-Traverse', length, width };
const id = `C|${length}|${width}`;
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}`;
const g = groups.get(id) ?? { key, count: 0 };
g.count++;
groups.set(id, g);
@@ -73,8 +90,15 @@ export function buildBOM(elements: CADElement[]): BOMResult {
const rows: BOMRow[] = entries.map((g, i) => {
// Ecken haben 2 Arme: Gesamtlaenge = count * length * 2
// Kreis-Traversen: Umfang = Pi * Durchmesser
const isCircle = g.key.type === 'Kreis-Traverse';
const armFactor = g.key.type === 'Eck-Traverse' ? 2 : 1;
const totalCm = g.count * g.key.length * armFactor;
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(', ')})` : '';
return {
pos: i + 1,
type: g.key.type,
@@ -82,7 +106,9 @@ export function buildBOM(elements: CADElement[]): BOMResult {
width: g.key.width,
count: g.count,
totalCm,
label: `${g.key.type} ${g.key.length} \u00d7 ${g.key.width}`,
label: `${g.key.type} ${isCircle ? '⌀' : ''}${g.key.length} × ${g.key.width}${detailSuffix}`,
chordCount: g.key.chordCount,
angle: g.key.angle,
};
});
+1 -1
View File
@@ -7,7 +7,7 @@ export type ElementType =
| 'polyline' | 'text' | 'dimension' | 'block_instance' | 'chair'
| 'seating-row' | 'seating-block' | 'table' | 'stage'
| 'leader' | 'revcloud' | 'image'
| 'curtain' | 'spotlight' | 'barrier' | 'truss' | 'boxcorner';
| 'curtain' | 'spotlight' | 'barrier' | 'truss' | 'boxcorner' | 'circletruss';
export type LineType = 'solid' | 'dashed' | 'dotted';