task(CAD-8): truss upgrade - length presets 50/100/200/300cm, adjustable width (default 29cm), rotatable rendering as direction polygon, boxcorner corner element with bidirectional magnetic docking
This commit is contained in:
@@ -466,29 +466,34 @@ export function snapTrussToDock(
|
|||||||
): TrussDock | null {
|
): TrussDock | null {
|
||||||
let best: { dock: TrussDock; dist: number } | null = null;
|
let best: { dock: TrussDock; dist: number } | null = null;
|
||||||
for (const el of existing) {
|
for (const el of existing) {
|
||||||
if (String(el.type) !== 'truss' || el.properties.truss !== true) continue;
|
const isTruss = String(el.type) === 'truss' && el.properties.truss === true;
|
||||||
|
const isCorner = String(el.type) === 'boxcorner' && el.properties.boxcorner === true;
|
||||||
|
if (!isTruss && !isCorner) continue;
|
||||||
const rotDeg = (el.properties.rotation as number | undefined) ?? 0;
|
const rotDeg = (el.properties.rotation as number | undefined) ?? 0;
|
||||||
const rot = (rotDeg * Math.PI) / 180;
|
const ends = isTruss ? trussEndpoints(el) : boxcornerEndpoints(el);
|
||||||
const dx = Math.cos(rot);
|
|
||||||
const dy = Math.sin(rot);
|
|
||||||
const half = newLength / 2;
|
|
||||||
const ends = trussEndpoints(el);
|
|
||||||
for (let endIdx = 0; endIdx < ends.length; endIdx++) {
|
for (let endIdx = 0; endIdx < ends.length; endIdx++) {
|
||||||
const end = ends[endIdx];
|
const end = ends[endIdx];
|
||||||
const dist = Math.hypot(cursor.x - end.x, cursor.y - end.y);
|
const dist = Math.hypot(cursor.x - end.x, cursor.y - end.y);
|
||||||
if (dist > tolerance) continue;
|
if (dist > tolerance) continue;
|
||||||
if (best && dist >= best.dist) continue;
|
if (best && dist >= best.dist) continue;
|
||||||
// Neue Traverse: fuehrt VOM Dock-Ende WEG von der Anker-Traverse.
|
// Weg-Richtung VOM Dock-Ende (arm-spezifisch):
|
||||||
// Endpunkt 0 (Start) -> Gegenrichtung (rot+180), Endpunkt 1 -> vorwaerts.
|
// Truss-Start (idx 0) = Gegenrichtung (rot+180), Truss-Ende (idx 1) = rot,
|
||||||
const dir = endIdx === 0 ? -1 : 1;
|
// Boxcorner-Arm1 (idx 0) = rot, Boxcorner-Arm2 (idx 1) = rot+90.
|
||||||
const centerX = end.x + dx * half * dir;
|
let dirDeg: number;
|
||||||
const centerY = end.y + dy * half * dir;
|
if (isTruss) {
|
||||||
const newRot = endIdx === 0 ? (rotDeg + 180) % 360 : rotDeg;
|
dirDeg = endIdx === 0 ? (rotDeg + 180) % 360 : rotDeg;
|
||||||
|
} else {
|
||||||
|
dirDeg = endIdx === 0 ? rotDeg : (rotDeg + 90) % 360;
|
||||||
|
}
|
||||||
|
const dirRad = (dirDeg * Math.PI) / 180;
|
||||||
|
const half = newLength / 2;
|
||||||
|
const centerX = end.x + Math.cos(dirRad) * half;
|
||||||
|
const centerY = end.y + Math.sin(dirRad) * half;
|
||||||
best = {
|
best = {
|
||||||
dist,
|
dist,
|
||||||
dock: {
|
dock: {
|
||||||
center: { x: centerX, y: centerY },
|
center: { x: centerX, y: centerY },
|
||||||
rotationDeg: newRot,
|
rotationDeg: dirDeg,
|
||||||
dockedTo: el.id,
|
dockedTo: el.id,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -505,19 +510,23 @@ function makeTrussElement(
|
|||||||
length: number,
|
length: number,
|
||||||
rotationDeg: number,
|
rotationDeg: number,
|
||||||
layerId: string,
|
layerId: string,
|
||||||
|
trussWidth = 29,
|
||||||
): CADElement {
|
): CADElement {
|
||||||
const rad = (rotationDeg * Math.PI) / 180;
|
const rad = (rotationDeg * Math.PI) / 180;
|
||||||
const w = Math.abs(length * Math.cos(rad)) || length;
|
const w = Math.abs(length * Math.cos(rad)) + Math.abs(trussWidth * Math.sin(rad));
|
||||||
const h = Math.abs(length * Math.sin(rad)) || 20;
|
const h = Math.abs(length * Math.sin(rad)) + Math.abs(trussWidth * Math.cos(rad));
|
||||||
return {
|
return {
|
||||||
id: `truss_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`,
|
id: `truss_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`,
|
||||||
type: 'truss',
|
type: 'truss',
|
||||||
layerId,
|
layerId,
|
||||||
x: center.x,
|
x: center.x,
|
||||||
y: center.y,
|
y: center.y,
|
||||||
width: Math.max(w, 20),
|
width: Math.max(w, trussWidth),
|
||||||
height: 20,
|
height: Math.max(h, trussWidth),
|
||||||
properties: { length, rotation: rotationDeg, truss: true, stroke: '#9aa0a6', strokeWidth: 2, fill: '#6b7280' },
|
properties: {
|
||||||
|
length, rotation: rotationDeg, trussWidth, truss: true,
|
||||||
|
stroke: '#9aa0a6', strokeWidth: 2, fill: '#6b7280',
|
||||||
|
},
|
||||||
} as unknown as CADElement;
|
} as unknown as CADElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,10 +543,12 @@ export const trussTool: ToolExtensionV2 = {
|
|||||||
},
|
},
|
||||||
optionsSchema: [
|
optionsSchema: [
|
||||||
{ key: 'length', label: 'Länge (cm)', type: 'select', options: [
|
{ key: 'length', label: 'Länge (cm)', type: 'select', options: [
|
||||||
|
{ value: '50', label: '50 cm' },
|
||||||
{ value: '100', label: '100 cm' },
|
{ value: '100', label: '100 cm' },
|
||||||
{ value: '200', label: '200 cm' },
|
{ value: '200', label: '200 cm' },
|
||||||
{ value: '290', label: '290 cm' },
|
{ value: '300', label: '300 cm' },
|
||||||
] },
|
] },
|
||||||
|
{ key: 'trussWidth', label: 'Breite (cm)', type: 'number', min: 10, max: 60 },
|
||||||
],
|
],
|
||||||
handlers: {
|
handlers: {
|
||||||
down(e, ctx) {
|
down(e, ctx) {
|
||||||
@@ -548,13 +559,14 @@ export const trussTool: ToolExtensionV2 = {
|
|||||||
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 string | undefined) ?? 200) || 200;
|
||||||
// Magnet-Vorschau: dockt der Cursor an ein Traversen-Ende?
|
const trussWidth = (c.options.trussWidth as number | undefined) ?? 29;
|
||||||
|
// 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',
|
(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'));
|
ctx.setPreview?.(makeTrussElement(dock.center, length, dock.rotationDeg, 'layer-0', trussWidth));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Frei: Richtung aus Drag
|
// Frei: Richtung aus Drag
|
||||||
@@ -570,22 +582,23 @@ export const trussTool: ToolExtensionV2 = {
|
|||||||
ctx.setPreview?.(null);
|
ctx.setPreview?.(null);
|
||||||
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 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
|
// 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(
|
||||||
(el) => String(el.type) === 'truss',
|
(el) => String(el.type) === 'truss' || String(el.type) === 'boxcorner',
|
||||||
);
|
);
|
||||||
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);
|
el = makeTrussElement(dock.center, length, dock.rotationDeg, layerId, trussWidth);
|
||||||
ctx.setStatus(`Traverse angedockt an ${dock.dockedTo} (${length}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);
|
el = makeTrussElement({ x: cx, y: cy }, length, ang, layerId, trussWidth);
|
||||||
ctx.setStatus(`Traverse platziert (${length}cm)`);
|
ctx.setStatus(`Traverse platziert (${length}cm, Breite ${trussWidth}cm)`);
|
||||||
}
|
}
|
||||||
c.doc.transact(() => { c.doc!.addElement(el); });
|
c.doc.transact(() => { c.doc!.addElement(el); });
|
||||||
},
|
},
|
||||||
@@ -597,6 +610,156 @@ export const trussTool: ToolExtensionV2 = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Task CAD-8: Boxcorner (Eck-Traverse) — L-foermiges Element
|
||||||
|
// mit zwei Armen (rot und rot+90). Magnetisch: dockt an
|
||||||
|
// Traversen-Enden und andere Ecken; Traversen docken an die
|
||||||
|
// Armenden. DREHBAR via properties.rotation (PropertiesPanel).
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Armenden einer Boxcorner: Endpunkte beider Arme vom Eckpunkt. */
|
||||||
|
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 rot = (rotDeg * Math.PI) / 180;
|
||||||
|
const rot2 = ((rotDeg + 90) * 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 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CornerDock {
|
||||||
|
vertex: Pt;
|
||||||
|
rotationDeg: number;
|
||||||
|
dockedTo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Magnetisches Andocken einer Ecke (rein): dockt an Traversen-
|
||||||
|
* Enden und Ecken-Armenden; am Traversen-Start-Ende wird die
|
||||||
|
* Gegenrichtung uebernommen (rot+180), damit Reihen nach aussen
|
||||||
|
* wachsen.
|
||||||
|
*/
|
||||||
|
export function cornerDockToDock(
|
||||||
|
existing: CADElement[],
|
||||||
|
cursor: Pt,
|
||||||
|
tolerance: number,
|
||||||
|
): CornerDock | null {
|
||||||
|
let best: { dock: CornerDock; dist: number } | null = null;
|
||||||
|
for (const el of existing) {
|
||||||
|
const isTruss = String(el.type) === 'truss' && el.properties.truss === true;
|
||||||
|
const isCorner = String(el.type) === 'boxcorner' && el.properties.boxcorner === true;
|
||||||
|
if (!isTruss && !isCorner) continue;
|
||||||
|
const rotDeg = (el.properties.rotation as number | undefined) ?? 0;
|
||||||
|
const ends = isTruss ? trussEndpoints(el) : boxcornerEndpoints(el);
|
||||||
|
for (let endIdx = 0; endIdx < ends.length; endIdx++) {
|
||||||
|
const end = ends[endIdx];
|
||||||
|
const dist = Math.hypot(cursor.x - end.x, cursor.y - end.y);
|
||||||
|
if (dist > tolerance) continue;
|
||||||
|
if (best && dist >= best.dist) continue;
|
||||||
|
const dir = isTruss && endIdx === 0 ? -1 : 1;
|
||||||
|
const newRot = isTruss && endIdx === 0 ? (rotDeg + 180) % 360 : rotDeg;
|
||||||
|
void dir;
|
||||||
|
best = {
|
||||||
|
dist,
|
||||||
|
dock: { vertex: { x: end.x, y: end.y }, rotationDeg: newRot, dockedTo: el.id },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best ? best.dock : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeBoxcornerElement(
|
||||||
|
vertex: Pt,
|
||||||
|
armLength: number,
|
||||||
|
rotationDeg: number,
|
||||||
|
trussWidth: number,
|
||||||
|
layerId: string,
|
||||||
|
): CADElement {
|
||||||
|
return {
|
||||||
|
id: `boxcorner_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`,
|
||||||
|
type: 'boxcorner',
|
||||||
|
layerId,
|
||||||
|
x: vertex.x,
|
||||||
|
y: vertex.y,
|
||||||
|
width: armLength + trussWidth,
|
||||||
|
height: armLength + trussWidth,
|
||||||
|
properties: {
|
||||||
|
armLength, trussWidth, rotation: rotationDeg, boxcorner: true,
|
||||||
|
stroke: '#9aa0a6', strokeWidth: 2, fill: '#6b7280',
|
||||||
|
},
|
||||||
|
} as unknown as CADElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cornerDragStart: Pt | null = null;
|
||||||
|
|
||||||
|
export const boxCornerTool: ToolExtensionV2 = {
|
||||||
|
manifest: {
|
||||||
|
id: 'boxcorner',
|
||||||
|
label: 'Eck-Traverse (magnetisch)',
|
||||||
|
icon: '╭',
|
||||||
|
ribbonTab: 'insert',
|
||||||
|
tags: ['basic'],
|
||||||
|
description: 'Eck-Traverse (Boxcorner): Drag = Richtung, dockt magnetisch an (CAD-8)',
|
||||||
|
},
|
||||||
|
optionsSchema: [
|
||||||
|
{ key: 'armLength', label: 'Armlänge (cm)', type: 'number', min: 20, max: 150 },
|
||||||
|
{ key: 'trussWidth', label: 'Breite (cm)', type: 'number', min: 10, max: 60 },
|
||||||
|
],
|
||||||
|
handlers: {
|
||||||
|
down(e, ctx) {
|
||||||
|
cornerDragStart = e.world;
|
||||||
|
ctx.setStatus('Eck-Traverse: Richtung ziehen (dockt automatisch an)');
|
||||||
|
},
|
||||||
|
move(e, ctx) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!cornerDragStart) return;
|
||||||
|
const armLength = (c.options.armLength as number | undefined) ?? 50;
|
||||||
|
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'));
|
||||||
|
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'));
|
||||||
|
},
|
||||||
|
up(e, ctx) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
const start = cornerDragStart;
|
||||||
|
cornerDragStart = null;
|
||||||
|
ctx.setPreview?.(null);
|
||||||
|
if (!start || !c.doc) return;
|
||||||
|
const armLength = (c.options.armLength as number | undefined) ?? 50;
|
||||||
|
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(
|
||||||
|
(el) => String(el.type) === 'truss' || String(el.type) === 'boxcorner',
|
||||||
|
);
|
||||||
|
const dock = cornerDockToDock(existing, start, DOCK_TOLERANCE);
|
||||||
|
let el: CADElement;
|
||||||
|
if (dock) {
|
||||||
|
el = makeBoxcornerElement(dock.vertex, armLength, dock.rotationDeg, 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);
|
||||||
|
ctx.setStatus(`Eck-Traverse platziert (${armLength}cm Arme, Breite ${trussWidth}cm)`);
|
||||||
|
}
|
||||||
|
c.doc.transact(() => { c.doc!.addElement(el); });
|
||||||
|
},
|
||||||
|
cancel(ctx) {
|
||||||
|
cornerDragStart = null;
|
||||||
|
ctx.setPreview?.(null);
|
||||||
|
ctx.setStatus('Eck-Traverse abgebrochen');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const eventToolsV2Plugin: PluginV2 = {
|
export const eventToolsV2Plugin: PluginV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'event-tools',
|
id: 'event-tools',
|
||||||
@@ -607,5 +770,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],
|
tools: [chairTool, seatingRowToolDrag, seatingBlockToolDrag, tableTool, stageTool, arcRowTool, kinoTemplateTool, banquetTemplateTool, standingTableTemplateTool, podiumTemplateTool, curtainTool, spotlightTool, barrierTool, trussTool, boxCornerTool],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -400,39 +400,78 @@ export function elementToPrimitives(el: CADElement, attrDefs?: Array<{ tag: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'truss': {
|
case 'truss': {
|
||||||
// Traverse (CAD-7): Balken als Rechteck + 2 Gurtlinien entlang
|
// Traverse (CAD-7/8): ROTIERTES Rechteck-Polygon + Gurtlinien
|
||||||
// der Rotation (wie ein Fachwerk-Profil)
|
// entlang der Richtung (Fachwerk-Profil), Breite = trussWidth.
|
||||||
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 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 dx = Math.cos(rad) * (length / 2);
|
const ux = Math.cos(rad), uy = Math.sin(rad); // Laengsrichtung
|
||||||
const dy = Math.sin(rad) * (length / 2);
|
const nx = -Math.sin(rad), ny = Math.cos(rad); // Normalenrichtung
|
||||||
const nx = -Math.sin(rad) * 10; // Normalen-Offset (halbe Hoehe)
|
const hl = length / 2, hw = tWidth / 2;
|
||||||
const ny = Math.cos(rad) * 10;
|
|
||||||
const c = { x: el.x, y: el.y };
|
const c = { x: el.x, y: el.y };
|
||||||
return [
|
const corner = (su: number, sv: number): { x: number; y: number } => ({
|
||||||
|
x: c.x + ux * (hl * su) + nx * (hw * sv),
|
||||||
|
y: c.y + uy * (hl * su) + ny * (hw * sv),
|
||||||
|
});
|
||||||
|
const pts = [corner(-1, -1), corner(1, -1), corner(1, 1), corner(-1, 1)];
|
||||||
|
const strokeCol = (props.stroke as string | undefined) ?? '#9aa0a6';
|
||||||
|
const out: ShapePrimitive[] = [
|
||||||
{
|
{
|
||||||
kind: 'rect',
|
kind: 'polyline',
|
||||||
x: c.x - dx, y: c.y - dy,
|
points: pts,
|
||||||
w: length, h: 20,
|
close: true,
|
||||||
fill: fillOrNull(props.fill) ?? '#6b7280',
|
stroke: strokeCol,
|
||||||
stroke: (props.stroke as string | undefined) ?? '#9aa0a6',
|
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
},
|
},
|
||||||
// Gurtlinien oben/unten entlang der Traverse
|
// Gurtlinien oben/unten
|
||||||
{
|
{
|
||||||
kind: 'line',
|
kind: 'line',
|
||||||
from: { x: c.x - dx + nx, y: c.y - dy + ny },
|
from: corner(-1, -1),
|
||||||
to: { x: c.x + dx + nx, y: c.y + dy + ny },
|
to: corner(1, -1),
|
||||||
stroke: '#4b5563', strokeWidth: 1,
|
stroke: '#4b5563', strokeWidth: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: 'line',
|
kind: 'line',
|
||||||
from: { x: c.x - dx - nx, y: c.y - dy - ny },
|
from: corner(-1, 1),
|
||||||
to: { x: c.x + dx - nx, y: c.y + dy - ny },
|
to: corner(1, 1),
|
||||||
stroke: '#4b5563', strokeWidth: 1,
|
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.
|
||||||
|
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 rad = (rotDeg * 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 px = (a: number, b: number): { x: number; y: number } => ({
|
||||||
|
x: v.x + ux * a + nx * b,
|
||||||
|
y: v.y + uy * a + ny * b,
|
||||||
|
});
|
||||||
|
// L-Geometrie: Arm1 entlang +u, Arm2 entlang +n (rot+90)
|
||||||
|
const pts = [
|
||||||
|
px(0, 0),
|
||||||
|
px(armLength, 0),
|
||||||
|
px(armLength, bWidth),
|
||||||
|
px(bWidth, bWidth),
|
||||||
|
px(bWidth, armLength),
|
||||||
|
px(0, armLength),
|
||||||
|
];
|
||||||
|
const strokeCol = (props.stroke as string | undefined) ?? '#9aa0a6';
|
||||||
|
return [{
|
||||||
|
kind: 'polyline',
|
||||||
|
points: pts,
|
||||||
|
close: true,
|
||||||
|
stroke: strokeCol,
|
||||||
|
strokeWidth: 2,
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'barrier': {
|
case 'barrier': {
|
||||||
|
|||||||
@@ -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';
|
| 'curtain' | 'spotlight' | 'barrier' | 'truss' | 'boxcorner';
|
||||||
|
|
||||||
export type LineType = 'solid' | 'dashed' | 'dotted';
|
export type LineType = 'solid' | 'dashed' | 'dotted';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
/**
|
||||||
|
* Task CAD-8 - Traversen-Upgrade:
|
||||||
|
* 1. Laengen-Presets 50/100/200/300 cm
|
||||||
|
* 2. Breite einstellbar (Standard 29 cm)
|
||||||
|
* 3. Rotierbares Rendering (Koerper als ROTIERTES Polygon)
|
||||||
|
* 4. Boxcorner (Eck-Traverse) mit magnetischem Andocken in BEIDE
|
||||||
|
* Richtungen: Ecke dockt an Traversen/Ecken, Traversen docken
|
||||||
|
* an Ecken-Armenden.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import {
|
||||||
|
trussTool,
|
||||||
|
boxCornerTool,
|
||||||
|
trussEndpoints,
|
||||||
|
boxcornerEndpoints,
|
||||||
|
snapTrussToDock,
|
||||||
|
cornerDockToDock,
|
||||||
|
} from '../src/plugins/builtin/event-tools';
|
||||||
|
import { CADDocument } from '../src/kernel/document/CADDocument';
|
||||||
|
import { createInMemoryDoc } from './helpers/inMemoryDoc';
|
||||||
|
import { elementToPrimitives } from '../src/render/pixi/elementDrawers';
|
||||||
|
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, rotationDeg = 0, trussWidth?: number): CADElement {
|
||||||
|
return {
|
||||||
|
id, type: 'truss', layerId: 'layer-0', x, y,
|
||||||
|
width: length, height: trussWidth ?? 29,
|
||||||
|
properties: {
|
||||||
|
length, rotation: rotationDeg, truss: true,
|
||||||
|
...(trussWidth !== undefined ? { trussWidth } : {}),
|
||||||
|
},
|
||||||
|
} as unknown as CADElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCorner(id: string, x: number, y: number, armLength = 50, rotationDeg = 0, width = 29): CADElement {
|
||||||
|
return {
|
||||||
|
id, type: 'boxcorner', layerId: 'layer-0', x, y,
|
||||||
|
width: 80, height: 80,
|
||||||
|
properties: { armLength, rotation: rotationDeg, trussWidth: width, boxcorner: true },
|
||||||
|
} as unknown as CADElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
trussTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
|
||||||
|
boxCornerTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 1: Laengen-Presets + Breite ──────────────────────────────
|
||||||
|
|
||||||
|
describe('CAD-8: Laengen-Presets + Breite', () => {
|
||||||
|
it('Laengen-Select: genau 50/100/200/300 cm', () => {
|
||||||
|
const lenField = trussTool.optionsSchema?.find((f) => f.key === 'length');
|
||||||
|
expect(lenField).toBeDefined();
|
||||||
|
expect(lenField!.type).toBe('select');
|
||||||
|
expect((lenField!.options ?? []).map((o) => o.value)).toEqual(['50', '100', '200', '300']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Breiten-Option trussWidth (number) vorhanden', () => {
|
||||||
|
const wField = trussTool.optionsSchema?.find((f) => f.key === 'trussWidth');
|
||||||
|
expect(wField).toBeDefined();
|
||||||
|
expect(wField!.type).toBe('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('platzierte Traverse: trussWidth=29 Standard, length=200 Standard', () => {
|
||||||
|
const { ctx, doc } = mkCtx();
|
||||||
|
trussTool.handlers.down!(pe(300, 300), ctx);
|
||||||
|
trussTool.handlers.up!(pe(400, 300), ctx);
|
||||||
|
const el = doc.getAllElements()[0];
|
||||||
|
expect(el.properties.trussWidth).toBe(29);
|
||||||
|
expect(el.properties.length).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trussWidth=40 und length=300 aus Optionen uebernommen', () => {
|
||||||
|
const { ctx, doc } = mkCtx([], { trussWidth: 40, length: 300 });
|
||||||
|
trussTool.handlers.down!(pe(100, 100), ctx);
|
||||||
|
trussTool.handlers.up!(pe(300, 100), ctx);
|
||||||
|
const el = doc.getAllElements()[0];
|
||||||
|
expect(el.properties.trussWidth).toBe(40);
|
||||||
|
expect(el.properties.length).toBe(300);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 2: Rotierbares Truss-Rendering ───────────────────────────
|
||||||
|
|
||||||
|
describe('CAD-8: Rotierbares Truss-Rendering (Polygon-Koerper)', () => {
|
||||||
|
it('90-Grad-Traverse: Koerperpolygon vertikal (y-Ausdehnung = Laenge, x = Breite)', () => {
|
||||||
|
const el = makeTruss('t1', 100, 100, 200, 90, 29);
|
||||||
|
const prims = elementToPrimitives(el);
|
||||||
|
const body = prims.find((p) => p.kind === 'polyline') as { points: Pt[]; close: boolean } | undefined;
|
||||||
|
expect(body).toBeDefined();
|
||||||
|
expect(body!.close).toBe(true);
|
||||||
|
expect(body!.points).toHaveLength(4);
|
||||||
|
const ys = body!.points.map((p) => p.y);
|
||||||
|
const xs = body!.points.map((p) => p.x);
|
||||||
|
expect(Math.max(...ys) - Math.min(...ys)).toBeCloseTo(200, 0);
|
||||||
|
expect(Math.max(...xs) - Math.min(...xs)).toBeCloseTo(29, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('0-Grad-Traverse: Koerperpolygon horizontal', () => {
|
||||||
|
const el = makeTruss('t1', 100, 100, 200, 0, 29);
|
||||||
|
const body = elementToPrimitives(el).find((p) => p.kind === 'polyline') as { points: Pt[] };
|
||||||
|
const xs = body.points.map((p) => p.x);
|
||||||
|
const ys = body.points.map((p) => p.y);
|
||||||
|
expect(Math.max(...xs) - Math.min(...xs)).toBeCloseTo(200, 0);
|
||||||
|
expect(Math.max(...ys) - Math.min(...ys)).toBeCloseTo(29, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('DREHBAR: Rotation aendert die Geometrie sichtbar (45 Grad diagonal)', () => {
|
||||||
|
const el = makeTruss('t1', 100, 100, 200, 45, 29);
|
||||||
|
const body = elementToPrimitives(el).find((p) => p.kind === 'polyline') as { points: Pt[] };
|
||||||
|
const xs = body.points.map((p) => p.x);
|
||||||
|
const expected = 200 * Math.cos(Math.PI / 4) + 29 * Math.sin(Math.PI / 4);
|
||||||
|
expect(Math.max(...xs) - Math.min(...xs)).toBeCloseTo(expected, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Breite im Rendering: trussWidth=40 ergibt 40 Einheiten Quer-Ausdehnung', () => {
|
||||||
|
const el = makeTruss('t1', 100, 100, 200, 0, 40);
|
||||||
|
const body = elementToPrimitives(el).find((p) => p.kind === 'polyline') as { points: Pt[] };
|
||||||
|
const ys = body.points.map((p) => p.y);
|
||||||
|
expect(Math.max(...ys) - Math.min(...ys)).toBeCloseTo(40, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 3: Boxcorner-Element ─────────────────────────────────────
|
||||||
|
|
||||||
|
describe('CAD-8: Boxcorner-Element (Eck-Traverse)', () => {
|
||||||
|
it('Manifest: id=boxcorner, Optionen armLength + trussWidth', () => {
|
||||||
|
expect(boxCornerTool.manifest.id).toBe('boxcorner');
|
||||||
|
const keys = (boxCornerTool.optionsSchema ?? []).map((f) => f.key);
|
||||||
|
expect(keys).toContain('armLength');
|
||||||
|
expect(keys).toContain('trussWidth');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Drag-Platzierung: boxcorner mit armLength=50/trussWidth=29 Standard, Rotation aus Drag', () => {
|
||||||
|
const { ctx, doc } = mkCtx();
|
||||||
|
boxCornerTool.handlers.down!(pe(100, 100), ctx);
|
||||||
|
boxCornerTool.handlers.up!(pe(200, 100), ctx);
|
||||||
|
const els = doc.getAllElements();
|
||||||
|
expect(els).toHaveLength(1);
|
||||||
|
const el = els[0];
|
||||||
|
expect(el.type).toBe('boxcorner');
|
||||||
|
expect(el.properties.armLength).toBe(50);
|
||||||
|
expect(el.properties.trussWidth).toBe(29);
|
||||||
|
expect(el.properties.rotation).toBeCloseTo(0, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Drag nach unten: Rotation ~90', () => {
|
||||||
|
const { ctx, doc } = mkCtx();
|
||||||
|
boxCornerTool.handlers.down!(pe(100, 100), ctx);
|
||||||
|
boxCornerTool.handlers.up!(pe(100, 200), ctx);
|
||||||
|
expect(doc.getAllElements()[0].properties.rotation).toBeCloseTo(90, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Optionen uebernommen: armLength=100, trussWidth=40', () => {
|
||||||
|
const { ctx, doc } = mkCtx([], { armLength: 100, trussWidth: 40 });
|
||||||
|
boxCornerTool.handlers.down!(pe(100, 100), ctx);
|
||||||
|
boxCornerTool.handlers.up!(pe(200, 100), ctx);
|
||||||
|
const el = doc.getAllElements()[0];
|
||||||
|
expect(el.properties.armLength).toBe(100);
|
||||||
|
expect(el.properties.trussWidth).toBe(40);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('boxcornerEndpoints: Arme bei rot und rot+90 vom Eckpunkt', () => {
|
||||||
|
const el = makeCorner('c1', 100, 100, 100, 0);
|
||||||
|
const [a1, a2] = boxcornerEndpoints(el);
|
||||||
|
expect(a1.x).toBeCloseTo(200, 5);
|
||||||
|
expect(a1.y).toBeCloseTo(100, 5);
|
||||||
|
expect(a2.x).toBeCloseTo(100, 5);
|
||||||
|
expect(a2.y).toBeCloseTo(200, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('DREHBAR: Rotation 90 dreht die Arm-Endpunkte mit', () => {
|
||||||
|
const el = makeCorner('c1', 100, 100, 100, 90);
|
||||||
|
const [a1, a2] = boxcornerEndpoints(el);
|
||||||
|
expect(a1.x).toBeCloseTo(100, 5);
|
||||||
|
expect(a1.y).toBeCloseTo(200, 5);
|
||||||
|
expect(a2.x).toBeCloseTo(0, 5);
|
||||||
|
expect(a2.y).toBeCloseTo(100, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('1 Undo entfernt die Boxcorner (EINE Transaktion)', () => {
|
||||||
|
const { ctx, doc } = mkCtx();
|
||||||
|
boxCornerTool.handlers.down!(pe(100, 100), ctx);
|
||||||
|
boxCornerTool.handlers.up!(pe(200, 100), ctx);
|
||||||
|
expect(doc.getAllElements()).toHaveLength(1);
|
||||||
|
doc.undo();
|
||||||
|
expect(doc.getAllElements()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancel verwirft die Platzierung', () => {
|
||||||
|
const { ctx, doc } = mkCtx();
|
||||||
|
boxCornerTool.handlers.down!(pe(100, 100), ctx);
|
||||||
|
boxCornerTool.handlers.cancel?.(ctx);
|
||||||
|
boxCornerTool.handlers.up!(pe(200, 100), ctx);
|
||||||
|
expect(doc.getAllElements()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 4: Boxcorner-Magnetismus (beide Richtungen) ─────────────
|
||||||
|
|
||||||
|
describe('CAD-8: Boxcorner-Magnetismus', () => {
|
||||||
|
it('cornerDockToDock: Ecke dockt an Traversen-Ende, Rotation = Weg-Richtung', () => {
|
||||||
|
const t1 = makeTruss('t1', 100, 100, 200, 0);
|
||||||
|
const dock = cornerDockToDock([t1], { x: 210, y: 105 }, 25);
|
||||||
|
expect(dock).not.toBeNull();
|
||||||
|
expect(dock!.vertex.x).toBeCloseTo(200, 5);
|
||||||
|
expect(dock!.vertex.y).toBeCloseTo(100, 5);
|
||||||
|
expect(dock!.rotationDeg).toBeCloseTo(0, 5);
|
||||||
|
expect(dock!.dockedTo).toBe('t1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('am Start-Ende der Traverse: Rotation 180 (weg von der Traverse)', () => {
|
||||||
|
const t1 = makeTruss('t1', 100, 100, 200, 0);
|
||||||
|
const dock = cornerDockToDock([t1], { x: -10, y: 98 }, 25);
|
||||||
|
expect(dock!.vertex.x).toBeCloseTo(0, 5);
|
||||||
|
expect(dock!.vertex.y).toBeCloseTo(100, 5);
|
||||||
|
expect(dock!.rotationDeg).toBeCloseTo(180, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ausserhalb der Toleranz: kein Dock (null)', () => {
|
||||||
|
const t1 = makeTruss('t1', 100, 100, 200, 0);
|
||||||
|
expect(cornerDockToDock([t1], { x: 500, y: 500 }, 25)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Traverse dockt an Boxcorner-Armende (snapTrussToDock kennt Ecken)', () => {
|
||||||
|
const c1 = makeCorner('c1', 100, 100, 50, 0);
|
||||||
|
const dock = snapTrussToDock([c1], { x: 150, y: 100 }, 200, 25);
|
||||||
|
expect(dock).not.toBeNull();
|
||||||
|
expect(dock!.rotationDeg).toBeCloseTo(0, 5);
|
||||||
|
expect(dock!.center.x).toBeCloseTo(250, 3);
|
||||||
|
expect(dock!.center.y).toBeCloseTo(100, 3);
|
||||||
|
expect(dock!.dockedTo).toBe('c1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Ecke dockt an Ecken-Armende', () => {
|
||||||
|
const c1 = makeCorner('c1', 100, 100, 50, 0);
|
||||||
|
const dock = cornerDockToDock([c1], { x: 152, y: 102 }, 25);
|
||||||
|
expect(dock!.vertex.x).toBeCloseTo(150, 5);
|
||||||
|
expect(dock!.vertex.y).toBeCloseTo(100, 5);
|
||||||
|
expect(dock!.rotationDeg).toBeCloseTo(0, 5);
|
||||||
|
expect(dock!.dockedTo).toBe('c1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Integration: platzierte Ecke wird vom trussTool magnetisch gefunden', () => {
|
||||||
|
const { ctx, doc } = mkCtx();
|
||||||
|
boxCornerTool.handlers.down!(pe(100, 100), ctx);
|
||||||
|
boxCornerTool.handlers.up!(pe(200, 100), ctx);
|
||||||
|
trussTool.handlers.down!(pe(150, 100), ctx);
|
||||||
|
trussTool.handlers.up!(pe(250, 100), ctx);
|
||||||
|
const els = doc.getAllElements();
|
||||||
|
expect(els).toHaveLength(2);
|
||||||
|
const truss = els.find((e) => e.type === 'truss')!;
|
||||||
|
expect(truss.properties.rotation).toBeCloseTo(0, 5);
|
||||||
|
expect(truss.x).toBeCloseTo(250, 3);
|
||||||
|
expect(truss.y).toBeCloseTo(100, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Integration: boxCornerTool dockt an bestehender Traverse', () => {
|
||||||
|
const t1 = makeTruss('t1', 100, 100, 200, 0);
|
||||||
|
const { ctx, doc } = mkCtx([t1]);
|
||||||
|
boxCornerTool.handlers.down!(pe(210, 105), ctx);
|
||||||
|
boxCornerTool.handlers.up!(pe(300, 100), ctx);
|
||||||
|
const corner = doc.getAllElements().find((e) => e.type === 'boxcorner')!;
|
||||||
|
expect(corner).toBeDefined();
|
||||||
|
expect(corner.x).toBeCloseTo(200, 3);
|
||||||
|
expect(corner.y).toBeCloseTo(100, 3);
|
||||||
|
expect(corner.properties.rotation).toBeCloseTo(0, 5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 5: Boxcorner-Rendering ─────────────────────────────────
|
||||||
|
|
||||||
|
describe('CAD-8: Boxcorner-Rendering (L-Form)', () => {
|
||||||
|
it('L-foermiges Polygon: 8 Punkte, geschlossen, am Eckpunkt', () => {
|
||||||
|
const el = makeCorner('c1', 100, 100, 50, 0, 29);
|
||||||
|
const prims = elementToPrimitives(el);
|
||||||
|
const poly = prims.find((p) => p.kind === 'polyline') as { points: Pt[]; close: boolean };
|
||||||
|
expect(poly).toBeDefined();
|
||||||
|
expect(poly.close).toBe(true);
|
||||||
|
expect(poly.points).toHaveLength(6); // L-Geometrie: 6 Punkte
|
||||||
|
const minDist = Math.min(...poly.points.map((p) => Math.hypot(p.x - 100, p.y - 100)));
|
||||||
|
expect(minDist).toBeLessThanOrEqual(29);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Rotation 90: L-Form zeigt nach unten-links statt rechts-unten', () => {
|
||||||
|
const el = makeCorner('c1', 100, 100, 50, 90, 29);
|
||||||
|
const poly = elementToPrimitives(el).find((p) => p.kind === 'polyline') as { points: Pt[] };
|
||||||
|
const xs = poly.points.map((p) => p.x);
|
||||||
|
const ys = poly.points.map((p) => p.y);
|
||||||
|
// Bei rot 90: Arme zeigen nach unten (y+) und links (x-)
|
||||||
|
expect(Math.max(...ys)).toBeGreaterThan(140);
|
||||||
|
expect(Math.min(...xs)).toBeLessThan(60);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user