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:
@@ -511,6 +511,7 @@ function makeTrussElement(
|
|||||||
rotationDeg: number,
|
rotationDeg: number,
|
||||||
layerId: string,
|
layerId: string,
|
||||||
trussWidth = 29,
|
trussWidth = 29,
|
||||||
|
chordCount = 2,
|
||||||
): CADElement {
|
): CADElement {
|
||||||
const rad = (rotationDeg * Math.PI) / 180;
|
const rad = (rotationDeg * Math.PI) / 180;
|
||||||
const w = Math.abs(length * Math.cos(rad)) + Math.abs(trussWidth * Math.sin(rad));
|
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),
|
width: Math.max(w, trussWidth),
|
||||||
height: Math.max(h, trussWidth),
|
height: Math.max(h, trussWidth),
|
||||||
properties: {
|
properties: {
|
||||||
length, rotation: rotationDeg, trussWidth, truss: true,
|
length, rotation: rotationDeg, trussWidth, chordCount, truss: true,
|
||||||
stroke: '#9aa0a6', strokeWidth: 2, fill: '#6b7280',
|
stroke: '#9aa0a6', strokeWidth: 2, fill: '#6b7280',
|
||||||
},
|
},
|
||||||
} as unknown as CADElement;
|
} 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)',
|
description: 'Traverse platzieren: Drag = Richtung, dockt magnetisch an bestehenden Traversen an (CAD-7)',
|
||||||
},
|
},
|
||||||
optionsSchema: [
|
optionsSchema: [
|
||||||
{ key: 'length', label: 'Länge (cm)', type: 'select', options: [
|
{ key: 'length', label: 'Länge (cm)', type: 'number', min: 10, max: 1000 },
|
||||||
{ value: '50', label: '50 cm' },
|
|
||||||
{ value: '100', label: '100 cm' },
|
|
||||||
{ value: '200', label: '200 cm' },
|
|
||||||
{ value: '300', label: '300 cm' },
|
|
||||||
] },
|
|
||||||
{ key: 'trussWidth', label: 'Breite (cm)', type: 'number', min: 10, max: 60 },
|
{ key: 'trussWidth', label: 'Breite (cm)', type: 'number', min: 10, max: 60 },
|
||||||
|
{ key: 'chordCount', label: 'Gurtrohre', type: 'number', min: 1, max: 4 },
|
||||||
],
|
],
|
||||||
handlers: {
|
handlers: {
|
||||||
down(e, ctx) {
|
down(e, ctx) {
|
||||||
@@ -558,22 +555,23 @@ export const trussTool: ToolExtensionV2 = {
|
|||||||
move(e, ctx) {
|
move(e, ctx) {
|
||||||
const c = ctx as FullToolContext;
|
const c = ctx as FullToolContext;
|
||||||
if (!trussDragStart) return;
|
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 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?
|
// Magnet-Vorschau: dockt der Cursor an ein Traversen-/Ecken-Ende?
|
||||||
const existing = c.doc ? (c.doc.getAllElements() as CADElement[]).filter(
|
const existing = c.doc ? (c.doc.getAllElements() as CADElement[]).filter(
|
||||||
(el) => String(el.type) === 'truss' || String(el.type) === 'boxcorner',
|
(el) => String(el.type) === 'truss' || String(el.type) === 'boxcorner',
|
||||||
) : [];
|
) : [];
|
||||||
const dock = snapTrussToDock(existing, trussDragStart, length, DOCK_TOLERANCE);
|
const dock = snapTrussToDock(existing, trussDragStart, length, DOCK_TOLERANCE);
|
||||||
if (dock) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// Frei: Richtung aus Drag
|
// Frei: Richtung aus Drag
|
||||||
const ang = (Math.atan2(e.world.y - trussDragStart.y, e.world.x - trussDragStart.x) * 180) / Math.PI;
|
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 cx = (trussDragStart.x + e.world.x) / 2;
|
||||||
const cy = (trussDragStart.y + e.world.y) / 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) {
|
up(e, ctx) {
|
||||||
const c = ctx as FullToolContext;
|
const c = ctx as FullToolContext;
|
||||||
@@ -583,6 +581,7 @@ export const trussTool: ToolExtensionV2 = {
|
|||||||
if (!start || !c.doc) return;
|
if (!start || !c.doc) return;
|
||||||
const length = Number((c.options.length as string | undefined) ?? 200) || 200;
|
const length = Number((c.options.length as string | undefined) ?? 200) || 200;
|
||||||
const trussWidth = (c.options.trussWidth as number | undefined) ?? 29;
|
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 layerId = (c.options.layerId as string | undefined) ?? 'layer-0';
|
||||||
// MAGNETISCH: Dock-Pruefung am Drag-Startpunkt (Traversen UND Ecken)
|
// MAGNETISCH: Dock-Pruefung am Drag-Startpunkt (Traversen UND Ecken)
|
||||||
const existing = (c.doc.getAllElements() as CADElement[]).filter(
|
const existing = (c.doc.getAllElements() as CADElement[]).filter(
|
||||||
@@ -591,13 +590,13 @@ export const trussTool: ToolExtensionV2 = {
|
|||||||
const dock = snapTrussToDock(existing, start, length, DOCK_TOLERANCE);
|
const dock = snapTrussToDock(existing, start, length, DOCK_TOLERANCE);
|
||||||
let el: CADElement;
|
let el: CADElement;
|
||||||
if (dock) {
|
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)`);
|
ctx.setStatus(`Traverse angedockt an ${dock.dockedTo} (${length}cm, Breite ${trussWidth}cm)`);
|
||||||
} else {
|
} else {
|
||||||
const ang = (Math.atan2(e.world.y - start.y, e.world.x - start.x) * 180) / Math.PI;
|
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 cx = (start.x + e.world.x) / 2;
|
||||||
const cy = (start.y + e.world.y) / 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)`);
|
ctx.setStatus(`Traverse platziert (${length}cm, Breite ${trussWidth}cm)`);
|
||||||
}
|
}
|
||||||
c.doc.transact(() => { c.doc!.addElement(el); });
|
c.doc.transact(() => { c.doc!.addElement(el); });
|
||||||
@@ -621,8 +620,9 @@ export const trussTool: ToolExtensionV2 = {
|
|||||||
export function boxcornerEndpoints(el: CADElement): [Pt, Pt] {
|
export function boxcornerEndpoints(el: CADElement): [Pt, Pt] {
|
||||||
const armLength = (el.properties.armLength as number | undefined) ?? 50;
|
const armLength = (el.properties.armLength as number | undefined) ?? 50;
|
||||||
const rotDeg = (el.properties.rotation as number | undefined) ?? 0;
|
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 rot = (rotDeg * Math.PI) / 180;
|
||||||
const rot2 = ((rotDeg + 90) * Math.PI) / 180;
|
const rot2 = ((rotDeg + angle) * Math.PI) / 180;
|
||||||
return [
|
return [
|
||||||
{ x: el.x + Math.cos(rot) * armLength, y: el.y + Math.sin(rot) * armLength },
|
{ 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 },
|
{ x: el.x + Math.cos(rot2) * armLength, y: el.y + Math.sin(rot2) * armLength },
|
||||||
@@ -674,6 +674,7 @@ function makeBoxcornerElement(
|
|||||||
vertex: Pt,
|
vertex: Pt,
|
||||||
armLength: number,
|
armLength: number,
|
||||||
rotationDeg: number,
|
rotationDeg: number,
|
||||||
|
angleDeg: number,
|
||||||
trussWidth: number,
|
trussWidth: number,
|
||||||
layerId: string,
|
layerId: string,
|
||||||
): CADElement {
|
): CADElement {
|
||||||
@@ -686,7 +687,7 @@ function makeBoxcornerElement(
|
|||||||
width: armLength + trussWidth,
|
width: armLength + trussWidth,
|
||||||
height: armLength + trussWidth,
|
height: armLength + trussWidth,
|
||||||
properties: {
|
properties: {
|
||||||
armLength, trussWidth, rotation: rotationDeg, boxcorner: true,
|
armLength, trussWidth, rotation: rotationDeg, angle: angleDeg, boxcorner: true,
|
||||||
stroke: '#9aa0a6', strokeWidth: 2, fill: '#6b7280',
|
stroke: '#9aa0a6', strokeWidth: 2, fill: '#6b7280',
|
||||||
},
|
},
|
||||||
} as unknown as CADElement;
|
} as unknown as CADElement;
|
||||||
@@ -705,6 +706,7 @@ export const boxCornerTool: ToolExtensionV2 = {
|
|||||||
},
|
},
|
||||||
optionsSchema: [
|
optionsSchema: [
|
||||||
{ key: 'armLength', label: 'Armlänge (cm)', type: 'number', min: 20, max: 150 },
|
{ 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 },
|
{ key: 'trussWidth', label: 'Breite (cm)', type: 'number', min: 10, max: 60 },
|
||||||
],
|
],
|
||||||
handlers: {
|
handlers: {
|
||||||
@@ -716,17 +718,18 @@ export const boxCornerTool: ToolExtensionV2 = {
|
|||||||
const c = ctx as FullToolContext;
|
const c = ctx as FullToolContext;
|
||||||
if (!cornerDragStart) return;
|
if (!cornerDragStart) return;
|
||||||
const armLength = (c.options.armLength as number | undefined) ?? 50;
|
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 trussWidth = (c.options.trussWidth as number | undefined) ?? 29;
|
||||||
const existing = c.doc ? (c.doc.getAllElements() as CADElement[]).filter(
|
const existing = c.doc ? (c.doc.getAllElements() as CADElement[]).filter(
|
||||||
(el) => String(el.type) === 'truss' || String(el.type) === 'boxcorner',
|
(el) => String(el.type) === 'truss' || String(el.type) === 'boxcorner',
|
||||||
) : [];
|
) : [];
|
||||||
const dock = cornerDockToDock(existing, cornerDragStart, DOCK_TOLERANCE);
|
const dock = cornerDockToDock(existing, cornerDragStart, DOCK_TOLERANCE);
|
||||||
if (dock) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const ang = (Math.atan2(e.world.y - cornerDragStart.y, e.world.x - cornerDragStart.x) * 180) / Math.PI;
|
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) {
|
up(e, ctx) {
|
||||||
const c = ctx as FullToolContext;
|
const c = ctx as FullToolContext;
|
||||||
@@ -735,6 +738,7 @@ export const boxCornerTool: ToolExtensionV2 = {
|
|||||||
ctx.setPreview?.(null);
|
ctx.setPreview?.(null);
|
||||||
if (!start || !c.doc) return;
|
if (!start || !c.doc) return;
|
||||||
const armLength = (c.options.armLength as number | undefined) ?? 50;
|
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 trussWidth = (c.options.trussWidth as number | undefined) ?? 29;
|
||||||
const layerId = (c.options.layerId as string | undefined) ?? 'layer-0';
|
const layerId = (c.options.layerId as string | undefined) ?? 'layer-0';
|
||||||
const existing = (c.doc.getAllElements() as CADElement[]).filter(
|
const existing = (c.doc.getAllElements() as CADElement[]).filter(
|
||||||
@@ -743,11 +747,11 @@ export const boxCornerTool: ToolExtensionV2 = {
|
|||||||
const dock = cornerDockToDock(existing, start, DOCK_TOLERANCE);
|
const dock = cornerDockToDock(existing, start, DOCK_TOLERANCE);
|
||||||
let el: CADElement;
|
let el: CADElement;
|
||||||
if (dock) {
|
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}`);
|
ctx.setStatus(`Eck-Traverse angedockt an ${dock.dockedTo}`);
|
||||||
} else {
|
} else {
|
||||||
const ang = (Math.atan2(e.world.y - start.y, e.world.x - start.x) * 180) / Math.PI;
|
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)`);
|
ctx.setStatus(`Eck-Traverse platziert (${armLength}cm Arme, Breite ${trussWidth}cm)`);
|
||||||
}
|
}
|
||||||
c.doc.transact(() => { c.doc!.addElement(el); });
|
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 = {
|
export const eventToolsV2Plugin: PluginV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'event-tools',
|
id: 'event-tools',
|
||||||
@@ -770,5 +826,5 @@ export const eventToolsV2Plugin: PluginV2 = {
|
|||||||
category: 'elements',
|
category: 'elements',
|
||||||
enabledByDefault: true,
|
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],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -400,14 +400,16 @@ export function elementToPrimitives(el: CADElement, attrDefs?: Array<{ tag: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'truss': {
|
case 'truss': {
|
||||||
// Traverse (CAD-7/8): ROTIERTES Rechteck-Polygon + Gurtlinien
|
// Traverse (CAD-7/8/11): ROTIERTES Rechteck-Polygon + Gurtlinien
|
||||||
// entlang der Richtung (Fachwerk-Profil), Breite = trussWidth.
|
// entlang der Richtung (Fachwerk-Profil), Breite = trussWidth,
|
||||||
|
// chordCount = Anzahl Gurtrohre (CAD-11).
|
||||||
const length = (props.length as number | undefined) ?? el.width;
|
const length = (props.length as number | undefined) ?? el.width;
|
||||||
const tWidth = (props.trussWidth as number | undefined) ?? el.height ?? 29;
|
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 rotDeg = (props.rotation as number | undefined) ?? 0;
|
||||||
const rad = (rotDeg * Math.PI) / 180;
|
const rad = (rotDeg * Math.PI) / 180;
|
||||||
const ux = Math.cos(rad), uy = Math.sin(rad); // Laengsrichtung
|
const ux = Math.cos(rad), uy = Math.sin(rad);
|
||||||
const nx = -Math.sin(rad), ny = Math.cos(rad); // Normalenrichtung
|
const nx = -Math.sin(rad), ny = Math.cos(rad);
|
||||||
const hl = length / 2, hw = tWidth / 2;
|
const hl = length / 2, hw = tWidth / 2;
|
||||||
const c = { x: el.x, y: el.y };
|
const c = { x: el.x, y: el.y };
|
||||||
const corner = (su: number, sv: number): { x: number; y: number } => ({
|
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,
|
stroke: strokeCol,
|
||||||
strokeWidth: 2,
|
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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'boxcorner': {
|
case 'boxcorner': {
|
||||||
// Eck-Traverse (CAD-8): L-foermiges Polygon (6 Punkte) aus zwei
|
// Eck-Traverse (CAD-8/11): L-foermiges Polygon (6 Punkte) aus zwei
|
||||||
// Armen (rot und rot+90) um den Eckpunkt, Breite = trussWidth.
|
// 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 armLength = (props.armLength as number | undefined) ?? 50;
|
||||||
const bWidth = (props.trussWidth as number | undefined) ?? 29;
|
const bWidth = (props.trussWidth as number | undefined) ?? 29;
|
||||||
const rotDeg = (props.rotation as number | undefined) ?? 0;
|
const rotDeg = (props.rotation as number | undefined) ?? 0;
|
||||||
|
const angleDeg = (props.angle as number | undefined) ?? 90;
|
||||||
const rad = (rotDeg * Math.PI) / 180;
|
const rad = (rotDeg * Math.PI) / 180;
|
||||||
|
const rad2 = ((rotDeg + angleDeg) * Math.PI) / 180;
|
||||||
const ux = Math.cos(rad), uy = Math.sin(rad);
|
const ux = Math.cos(rad), uy = Math.sin(rad);
|
||||||
const nx = -Math.sin(rad), ny = Math.cos(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 } => ({
|
const px = (a: number, b: number): { x: number; y: number } => ({
|
||||||
x: v.x + ux * a + nx * b,
|
x: vx.x + ux * a + nx * b,
|
||||||
y: v.y + uy * a + ny * 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 = [
|
const pts = [
|
||||||
px(0, 0),
|
px(0, 0),
|
||||||
px(armLength, 0),
|
px(armLength, 0),
|
||||||
px(armLength, bWidth),
|
px(armLength, bWidth),
|
||||||
px(bWidth, bWidth),
|
px(bWidth, bWidth),
|
||||||
px(bWidth, armLength),
|
pxa(bWidth, 0),
|
||||||
px(0, armLength),
|
pxa(armLength, 0),
|
||||||
|
pxa(0, 0),
|
||||||
|
px(0, bWidth),
|
||||||
];
|
];
|
||||||
const strokeCol = (props.stroke as string | undefined) ?? '#9aa0a6';
|
const strokeCol = (props.stroke as string | undefined) ?? '#9aa0a6';
|
||||||
return [{
|
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': {
|
case 'barrier': {
|
||||||
// Absperrung (Task E7): Kette aus Kreis-Paaren (properties.circles).
|
// Absperrung (Task E7): Kette aus Kreis-Paaren (properties.circles).
|
||||||
const circles = Array.isArray(props.circles)
|
const circles = Array.isArray(props.circles)
|
||||||
|
|||||||
@@ -10,16 +10,20 @@ import type { CADElement } from '../types/cad.types';
|
|||||||
|
|
||||||
export interface BOMRow {
|
export interface BOMRow {
|
||||||
pos: number;
|
pos: number;
|
||||||
type: 'Traverse' | 'Eck-Traverse';
|
type: 'Traverse' | 'Eck-Traverse' | 'Kreis-Traverse';
|
||||||
/** Laenge in cm (Traverse: length; Eck-Traverse: armLength). */
|
/** Laenge in cm (Traverse: length; Eck-Traverse: armLength; Kreis: Durchmesser). */
|
||||||
length: number;
|
length: number;
|
||||||
/** Breite in cm (trussWidth). */
|
/** Breite in cm (trussWidth). */
|
||||||
width: number;
|
width: number;
|
||||||
count: number;
|
count: number;
|
||||||
/** Gesamtlaenge dieser Position in cm (count * length, Ecken * 2). */
|
/** Gesamtlaenge dieser Position in cm. */
|
||||||
totalCm: number;
|
totalCm: number;
|
||||||
/** Bezeichnung, z.B. "Traverse 200 x 29". */
|
/** Bezeichnung. */
|
||||||
label: string;
|
label: string;
|
||||||
|
/** Gurtrohr-Anzahl (optional, CAD-11). */
|
||||||
|
chordCount?: number;
|
||||||
|
/** Eck-Winkel in Grad (optional, CAD-11). */
|
||||||
|
angle?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BOMResult {
|
export interface BOMResult {
|
||||||
@@ -29,9 +33,11 @@ export interface BOMResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface GroupKey {
|
interface GroupKey {
|
||||||
type: 'Traverse' | 'Eck-Traverse';
|
type: 'Traverse' | 'Eck-Traverse' | 'Kreis-Traverse';
|
||||||
length: number;
|
length: number;
|
||||||
width: 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) {
|
if (String(el.type) === 'truss' && el.properties.truss === true) {
|
||||||
const length = (el.properties.length as number | undefined) ?? el.width ?? 0;
|
const length = (el.properties.length as number | undefined) ?? el.width ?? 0;
|
||||||
const width = (el.properties.trussWidth as number | undefined) ?? el.height ?? 29;
|
const width = (el.properties.trussWidth as number | undefined) ?? el.height ?? 29;
|
||||||
const key: GroupKey = { type: 'Traverse', length, width };
|
const chordCount = (el.properties.chordCount as number | undefined) ?? 2;
|
||||||
const id = `T|${length}|${width}`;
|
const key: GroupKey = { type: 'Traverse', length, width, chordCount };
|
||||||
|
const id = `T|${length}|${width}|${chordCount}`;
|
||||||
const g = groups.get(id) ?? { key, count: 0 };
|
const g = groups.get(id) ?? { key, count: 0 };
|
||||||
g.count++;
|
g.count++;
|
||||||
groups.set(id, g);
|
groups.set(id, g);
|
||||||
} else if (String(el.type) === 'boxcorner' && el.properties.boxcorner === true) {
|
} else if (String(el.type) === 'boxcorner' && el.properties.boxcorner === true) {
|
||||||
const length = (el.properties.armLength as number | undefined) ?? 50;
|
const length = (el.properties.armLength as number | undefined) ?? 50;
|
||||||
const width = (el.properties.trussWidth as number | undefined) ?? 29;
|
const width = (el.properties.trussWidth as number | undefined) ?? 29;
|
||||||
const key: GroupKey = { type: 'Eck-Traverse', length, width };
|
const angle = (el.properties.angle as number | undefined) ?? 90;
|
||||||
const id = `C|${length}|${width}`;
|
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 };
|
const g = groups.get(id) ?? { key, count: 0 };
|
||||||
g.count++;
|
g.count++;
|
||||||
groups.set(id, g);
|
groups.set(id, g);
|
||||||
@@ -73,8 +90,15 @@ export function buildBOM(elements: CADElement[]): BOMResult {
|
|||||||
|
|
||||||
const rows: BOMRow[] = entries.map((g, i) => {
|
const rows: BOMRow[] = entries.map((g, i) => {
|
||||||
// Ecken haben 2 Arme: Gesamtlaenge = count * length * 2
|
// 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 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 {
|
return {
|
||||||
pos: i + 1,
|
pos: i + 1,
|
||||||
type: g.key.type,
|
type: g.key.type,
|
||||||
@@ -82,7 +106,9 @@ export function buildBOM(elements: CADElement[]): BOMResult {
|
|||||||
width: g.key.width,
|
width: g.key.width,
|
||||||
count: g.count,
|
count: g.count,
|
||||||
totalCm,
|
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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export type ElementType =
|
|||||||
| 'polyline' | 'text' | 'dimension' | 'block_instance' | 'chair'
|
| 'polyline' | 'text' | 'dimension' | 'block_instance' | 'chair'
|
||||||
| 'seating-row' | 'seating-block' | 'table' | 'stage'
|
| 'seating-row' | 'seating-block' | 'table' | 'stage'
|
||||||
| 'leader' | 'revcloud' | 'image'
|
| 'leader' | 'revcloud' | 'image'
|
||||||
| 'curtain' | 'spotlight' | 'barrier' | 'truss' | 'boxcorner';
|
| 'curtain' | 'spotlight' | 'barrier' | 'truss' | 'boxcorner' | 'circletruss';
|
||||||
|
|
||||||
export type LineType = 'solid' | 'dashed' | 'dotted';
|
export type LineType = 'solid' | 'dashed' | 'dotted';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
/**
|
||||||
|
* Task CAD-11 - Truss Professional Upgrade:
|
||||||
|
* 1. chordCount (Anzahl Gurtrohre, 1-4) einstellbar
|
||||||
|
* 2. Laenge als freier Zahleneintrag (nicht nur Presets)
|
||||||
|
* 3. Corner mit variablem Winkel (nicht nur 90 Grad)
|
||||||
|
* 4. Kreis-Traverse als eigenes Tool
|
||||||
|
* 5. BOM beruecksichtigt chordCount und Kreistraversen
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import {
|
||||||
|
trussTool,
|
||||||
|
boxCornerTool,
|
||||||
|
circleTrussTool,
|
||||||
|
trussEndpoints,
|
||||||
|
boxcornerEndpoints,
|
||||||
|
} from '../src/plugins/builtin/event-tools';
|
||||||
|
import { buildBOM } from '../src/services/bomService';
|
||||||
|
import { elementToPrimitives } from '../src/render/pixi/elementDrawers';
|
||||||
|
import { CADDocument } from '../src/kernel/document/CADDocument';
|
||||||
|
import { createInMemoryDoc } from './helpers/inMemoryDoc';
|
||||||
|
import type { CADElement, Pt } from '../src/types/cad.types';
|
||||||
|
|
||||||
|
function mkCtx(elements: CADElement[] = [], options: Record<string, unknown> = {}) {
|
||||||
|
const { ydoc } = createInMemoryDoc();
|
||||||
|
const doc = new CADDocument(ydoc);
|
||||||
|
for (const el of elements) doc.addElement(el);
|
||||||
|
const statuses: string[] = [];
|
||||||
|
const previews: (CADElement | null)[] = [];
|
||||||
|
const ctx: any = {
|
||||||
|
doc,
|
||||||
|
options,
|
||||||
|
setStatus: (m: string) => statuses.push(m),
|
||||||
|
setPreview: (el: CADElement | null) => previews.push(el),
|
||||||
|
};
|
||||||
|
return { ctx, doc, statuses, previews };
|
||||||
|
}
|
||||||
|
|
||||||
|
function pe(x: number, y: number): { world: Pt } {
|
||||||
|
return { world: { x, y } } as never;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTruss(id: string, x: number, y: number, length: number, rotation: number, width = 29, chordCount = 2): CADElement {
|
||||||
|
return {
|
||||||
|
id, type: 'truss', layerId: 'l', x, y, width: length, height: width,
|
||||||
|
properties: { length, rotation, trussWidth: width, chordCount, truss: true },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCorner(id: string, x: number, y: number, armLength = 50, rotation = 0, angle = 90, width = 29): CADElement {
|
||||||
|
return {
|
||||||
|
id, type: 'boxcorner', layerId: 'l', x, y, width: 80, height: 80,
|
||||||
|
properties: { armLength, rotation, angle, trussWidth: width, boxcorner: true },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCircleTruss(id: string, x: number, y: number, diameter: number, width = 29, chordCount = 2): CADElement {
|
||||||
|
return {
|
||||||
|
id, type: 'circletruss', layerId: 'l', x, y, width: diameter, height: diameter,
|
||||||
|
properties: { diameter, trussWidth: width, chordCount, circletruss: true },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
for (const t of [trussTool, boxCornerTool, circleTrussTool]) {
|
||||||
|
t.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 1: chordCount (Gurtrohre) ──────────────────────────────
|
||||||
|
|
||||||
|
describe('CAD-11: chordCount', () => {
|
||||||
|
it('trussTool hat chordCount-Option (number, 1-4, Default 2)', () => {
|
||||||
|
const field = trussTool.optionsSchema?.find((f) => f.key === 'chordCount');
|
||||||
|
expect(field).toBeDefined();
|
||||||
|
expect(field!.type).toBe('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('platzierte Traverse: chordCount=2 Standard', () => {
|
||||||
|
const { ctx, doc } = mkCtx();
|
||||||
|
trussTool.handlers.down!(pe(100, 100), ctx);
|
||||||
|
trussTool.handlers.up!(pe(300, 100), ctx);
|
||||||
|
expect(doc.getAllElements()[0].properties.chordCount).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('chordCount=3 wird uebernommen', () => {
|
||||||
|
const { ctx, doc } = mkCtx([], { chordCount: 3 });
|
||||||
|
trussTool.handlers.down!(pe(100, 100), ctx);
|
||||||
|
trussTool.handlers.up!(pe(300, 100), ctx);
|
||||||
|
expect(doc.getAllElements()[0].properties.chordCount).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Rendering: chordCount=2 zeigt 2 Gurtlinien, chordCount=4 zeigt 4', () => {
|
||||||
|
const t2 = makeTruss('t2', 100, 100, 200, 0, 29, 2);
|
||||||
|
const t4 = makeTruss('t4', 100, 100, 200, 0, 29, 4);
|
||||||
|
const prims2 = elementToPrimitives(t2).filter((p) => p.kind === 'line' && p.stroke === '#4b5563');
|
||||||
|
const prims4 = elementToPrimitives(t4).filter((p) => p.kind === 'line' && p.stroke === '#4b5563');
|
||||||
|
expect(prims2.length).toBe(2);
|
||||||
|
expect(prims4.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('chordCount=1: eine Mittellinie', () => {
|
||||||
|
const t1 = makeTruss('t1', 100, 100, 200, 0, 29, 1);
|
||||||
|
const lines = elementToPrimitives(t1).filter((p) => p.kind === 'line' && p.stroke === '#4b5563');
|
||||||
|
expect(lines.length).toBe(1);
|
||||||
|
const line = lines[0] as { from: Pt; to: Pt };
|
||||||
|
expect(line.from.y).toBeCloseTo(100, 0); // Mittellinie auf y=100
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 2: Freie Laengeneingabe ─────────────────────────────────
|
||||||
|
|
||||||
|
describe('CAD-11: Laenge als freier Zahleneintrag', () => {
|
||||||
|
it('length ist number (nicht select) — Presets als separate Quick-Buttons', () => {
|
||||||
|
const field = trussTool.optionsSchema?.find((f) => f.key === 'length');
|
||||||
|
expect(field).toBeDefined();
|
||||||
|
expect(field!.type).toBe('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Custom-Laenge 175 wird uebernommen', () => {
|
||||||
|
const { ctx, doc } = mkCtx([], { length: 175 });
|
||||||
|
trussTool.handlers.down!(pe(100, 100), ctx);
|
||||||
|
trussTool.handlers.up!(pe(300, 100), ctx);
|
||||||
|
expect(doc.getAllElements()[0].properties.length).toBe(175);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 3: Corner mit variablem Winkel ─────────────────────────
|
||||||
|
|
||||||
|
describe('CAD-11: Corner variablem Winkel', () => {
|
||||||
|
it('boxCornerTool hat angle-Option (number, Default 90)', () => {
|
||||||
|
const field = boxCornerTool.optionsSchema?.find((f) => f.key === 'angle');
|
||||||
|
expect(field).toBeDefined();
|
||||||
|
expect(field!.type).toBe('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('boxcornerEndpoints: 90-Grad-Ecke = Arm2 bei rot+90', () => {
|
||||||
|
const c = makeCorner('c1', 100, 100, 50, 0, 90);
|
||||||
|
const [a1, a2] = boxcornerEndpoints(c);
|
||||||
|
expect(a1.x).toBeCloseTo(150, 0);
|
||||||
|
expect(a1.y).toBeCloseTo(100, 0);
|
||||||
|
expect(a2.x).toBeCloseTo(100, 0);
|
||||||
|
expect(a2.y).toBeCloseTo(150, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('boxcornerEndpoints: 45-Grad-Ecke = Arm2 bei rot+45', () => {
|
||||||
|
const c = makeCorner('c1', 100, 100, 50, 0, 45);
|
||||||
|
const [a1, a2] = boxcornerEndpoints(c);
|
||||||
|
expect(a1.x).toBeCloseTo(150, 0);
|
||||||
|
expect(a1.y).toBeCloseTo(100, 0);
|
||||||
|
const halfDiag = 50 * Math.cos(Math.PI / 4);
|
||||||
|
expect(a2.x).toBeCloseTo(100 + halfDiag, 0);
|
||||||
|
expect(a2.y).toBeCloseTo(100 + halfDiag, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('platzierte 30-Grad-Ecke: angle=30 in properties', () => {
|
||||||
|
const { ctx, doc } = mkCtx([], { angle: 30 });
|
||||||
|
boxCornerTool.handlers.down!(pe(100, 100), ctx);
|
||||||
|
boxCornerTool.handlers.up!(pe(200, 100), ctx);
|
||||||
|
expect(doc.getAllElements()[0].properties.angle).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Default-Winkel ist 90 (abwaertskompatibel)', () => {
|
||||||
|
const { ctx, doc } = mkCtx();
|
||||||
|
boxCornerTool.handlers.down!(pe(100, 100), ctx);
|
||||||
|
boxCornerTool.handlers.up!(pe(200, 100), ctx);
|
||||||
|
expect(doc.getAllElements()[0].properties.angle).toBe(90);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Rendering: 90-Grad = L-Form, 45-Grad = spitzeres V', () => {
|
||||||
|
const c90 = makeCorner('c90', 100, 100, 50, 0, 90, 29);
|
||||||
|
const c45 = makeCorner('c45', 100, 100, 50, 0, 45, 29);
|
||||||
|
const p90 = elementToPrimitives(c90).find((p) => p.kind === 'polyline') as { points: Pt[] };
|
||||||
|
const p45 = elementToPrimitives(c45).find((p) => p.kind === 'polyline') as { points: Pt[] };
|
||||||
|
// Beide sind Polygone mit >= 6 Punkten
|
||||||
|
expect(p90.points.length).toBeGreaterThanOrEqual(6);
|
||||||
|
expect(p45.points.length).toBeGreaterThanOrEqual(6);
|
||||||
|
// 45-Grad-Ecke: zweiter Arm diagonal → Y-Ausdehnung kleiner als bei 90°
|
||||||
|
const maxY90 = Math.max(...p90.points.map((p) => p.y));
|
||||||
|
const maxY45 = Math.max(...p45.points.map((p) => p.y));
|
||||||
|
expect(maxY45).toBeLessThan(maxY90);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 4: Kreis-Traverse ──────────────────────────────────────
|
||||||
|
|
||||||
|
describe('CAD-11: Kreis-Traverse', () => {
|
||||||
|
it('circleTrussTool existiert mit Manifest', () => {
|
||||||
|
expect(circleTrussTool.manifest.id).toBe('circle-truss');
|
||||||
|
const keys = (circleTrussTool.optionsSchema ?? []).map((f) => f.key);
|
||||||
|
expect(keys).toContain('diameter');
|
||||||
|
expect(keys).toContain('trussWidth');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Klick platziert Kreistraverse mit diameter/trussWidth/chordCount', () => {
|
||||||
|
const { ctx, doc } = mkCtx([], { diameter: 300, trussWidth: 29, chordCount: 2 });
|
||||||
|
circleTrussTool.handlers.down!(pe(200, 200), ctx);
|
||||||
|
const els = doc.getAllElements();
|
||||||
|
expect(els).toHaveLength(1);
|
||||||
|
expect(els[0].type).toBe('circletruss');
|
||||||
|
expect(els[0].properties.diameter).toBe(300);
|
||||||
|
expect(els[0].properties.trussWidth).toBe(29);
|
||||||
|
expect(els[0].properties.chordCount).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Kreis-Traverse: Rendering zeigt Aussen- und Innenkreis + Gurtrohr-Kreise', () => {
|
||||||
|
const ct = makeCircleTruss('ct1', 200, 200, 300, 29, 2);
|
||||||
|
const prims = elementToPrimitives(ct);
|
||||||
|
const circles = prims.filter((p) => p.kind === 'circle');
|
||||||
|
// Aussen + Innen + chordCount-Kreise = mindestens 4
|
||||||
|
expect(circles.length).toBeGreaterThanOrEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('1 Undo entfernt die Kreistraverse', () => {
|
||||||
|
const { ctx, doc } = mkCtx();
|
||||||
|
circleTrussTool.handlers.down!(pe(200, 200), ctx);
|
||||||
|
expect(doc.getAllElements()).toHaveLength(1);
|
||||||
|
doc.undo();
|
||||||
|
expect(doc.getAllElements()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('diameter-Default ist 200', () => {
|
||||||
|
const { ctx, doc } = mkCtx();
|
||||||
|
circleTrussTool.handlers.down!(pe(200, 200), ctx);
|
||||||
|
expect(doc.getAllElements()[0].properties.diameter).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 5: BOM-Integration ─────────────────────────────────────
|
||||||
|
|
||||||
|
describe('CAD-11: BOM mit chordCount + Kreistraversen', () => {
|
||||||
|
it('BOM gruppiert nach chordCount (2-rohr vs 4-rohr getrennt)', () => {
|
||||||
|
const els = [
|
||||||
|
makeTruss('t1', 0, 0, 200, 0, 29, 2),
|
||||||
|
makeTruss('t2', 0, 0, 200, 0, 29, 4),
|
||||||
|
];
|
||||||
|
const bom = buildBOM(els);
|
||||||
|
expect(bom.rows.filter((r) => r.type === 'Traverse')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('BOM enthaelt Kreistraversen mit Durchmesser', () => {
|
||||||
|
const els = [makeCircleTruss('ct1', 200, 200, 300, 29, 2)];
|
||||||
|
const bom = buildBOM(els);
|
||||||
|
expect(bom.rows).toHaveLength(1);
|
||||||
|
expect(bom.rows[0].label).toContain('Kreis');
|
||||||
|
expect(bom.rows[0].label).toContain('300');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Kreistraverse-Umfang zaehlt in Gesamtlaenge (Pi * d)', () => {
|
||||||
|
const els = [makeCircleTruss('ct1', 200, 200, 300, 29, 2)];
|
||||||
|
const bom = buildBOM(els);
|
||||||
|
const expected = (Math.PI * 300) / 100; // ~9.42m
|
||||||
|
expect(bom.totalLengthM).toBeCloseTo(expected, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -67,11 +67,12 @@ beforeEach(() => {
|
|||||||
// ─── 1: Laengen-Presets + Breite ──────────────────────────────
|
// ─── 1: Laengen-Presets + Breite ──────────────────────────────
|
||||||
|
|
||||||
describe('CAD-8: Laengen-Presets + Breite', () => {
|
describe('CAD-8: Laengen-Presets + Breite', () => {
|
||||||
it('Laengen-Select: genau 50/100/200/300 cm', () => {
|
it('Laengen-Select: freie Eingabe mit 50-300 Bereich (CAD-11: number)', () => {
|
||||||
const lenField = trussTool.optionsSchema?.find((f) => f.key === 'length');
|
const lenField = trussTool.optionsSchema?.find((f) => f.key === 'length');
|
||||||
expect(lenField).toBeDefined();
|
expect(lenField).toBeDefined();
|
||||||
expect(lenField!.type).toBe('select');
|
expect(lenField!.type).toBe('number');
|
||||||
expect((lenField!.options ?? []).map((o) => o.value)).toEqual(['50', '100', '200', '300']);
|
expect(lenField!.min).toBe(10);
|
||||||
|
expect(lenField!.max).toBe(1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Breiten-Option trussWidth (number) vorhanden', () => {
|
it('Breiten-Option trussWidth (number) vorhanden', () => {
|
||||||
@@ -296,7 +297,7 @@ describe('CAD-8: Boxcorner-Rendering (L-Form)', () => {
|
|||||||
const poly = prims.find((p) => p.kind === 'polyline') as { points: Pt[]; close: boolean };
|
const poly = prims.find((p) => p.kind === 'polyline') as { points: Pt[]; close: boolean };
|
||||||
expect(poly).toBeDefined();
|
expect(poly).toBeDefined();
|
||||||
expect(poly.close).toBe(true);
|
expect(poly.close).toBe(true);
|
||||||
expect(poly.points).toHaveLength(6); // L-Geometrie: 6 Punkte
|
expect(poly.points).toHaveLength(8); // CAD-11: variable-angle Geometrie hat 8 Punkte
|
||||||
const minDist = Math.min(...poly.points.map((p) => Math.hypot(p.x - 100, p.y - 100)));
|
const minDist = Math.min(...poly.points.map((p) => Math.hypot(p.x - 100, p.y - 100)));
|
||||||
expect(minDist).toBeLessThanOrEqual(29);
|
expect(minDist).toBeLessThanOrEqual(29);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user