Compare commits
2 Commits
44a9aec107
...
ee4142b6d1
| Author | SHA1 | Date | |
|---|---|---|---|
| ee4142b6d1 | |||
| cefba89ebb |
+4
-2
@@ -667,6 +667,7 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
||||
|
||||
| Datum | Task | Zusammenfassung | Tests | Commit | Notizen |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-08-31 | CAD-7 | Magnetisches Traversen-Tool: (1) ElementType „truss“ NEU + (2) trussEndpoints (Endpunkte rotationsbewusst über Grad-Winkel) + snapTrussToDock (magnetisches Andocken, rein): sucht unter allen truss-Elementen den Endpunkt in 25er Toleranz, uebernimmt dessen Rotation UND dockt exakt an — am ENDPUNKT-0 (Start) in GEGENRICHTUNG (rot+180, weg von der Anker-Traverse), am Endpunkt-1 vorwärts (Reihen bauen sich selbst); bei mehreren Kandidaten gewinnt der nächstgelegene; (3) trussTool: Drag-Interaktion (down startet, move zeigt magnetische Live-Vorschau — dockt der Start an einem Traversen-Ende wird sofort die gedockte Geometrie angezeigt, up committet in EINER Transaktion), Laengen-Option 100/200/290cm, ohne bestehende Traversen freie Platzierung mit Richtung aus Drag; (4) Rendering: truss-Case im Pixi-Drawer — Balken-Rechteck + 2 Gurtlinien entlang der Rotation (Fachwerk-Profil-Look); Zwischenfall selbst erkannt: erster Docking-Ansatz extendede IMMER vorwärts → am Startpunkt überlappte die neue Traverse die alte (Test deckte auf: Rotation 180 erwartet, 0 geliefert) — Endpunkt-Index-Unterscheidung eingeführt | Frontend: 807/807 Tests (+18 trussTool.test.ts), tsc 0, Build EXIT 0 | cefba89 | CAD-7 COMPLETE — CAD-6+Cad-7 decken beide Nutzer-Wuensche (Grundrisse + Traversen) ab |
|
||||
| 2026-08-31 | CAD-6 | Hintergrund-Grundrisse (mit Maßstab) — echter BUG-Fix: (1) Befund: Die komplette Infrastruktur existierte (BackgroundImport-Dialog, BackgroundService mit calibrateScale-Maßstabs-Kalibrierung, bgConfig in Yjs via bgConfig-Map inkl. CRDT-Sync, bgConfig als CanvasArea-Prop) — ABER CanvasArea nutzte bgConfig NIE: kein setOptions(backgroundSrc), keine Pixi-Zeichnung; importierte Grundrisse wurden im Dialog konfiguriert, aber auf der Zeichenfläche GAR NICHT gerendert; (2) Fix: CanvasArea-Effect erweitert — renderEngine.setOptions mit backgroundSrc (visible-Filter), backgroundScale, backgroundOffsetX/Y, backgroundRotation (Grad→Radiant), backgroundOpacity + render()-Trigger; bgConfig in die useEffect-Dependencies aufgenommen (Position/Scale/Opacity-Änderungen rendern sofort); (3) PixiRenderer.drawBackground: echte Sprite-Implementierung statt Stub — Bild-Cache (Neuaufbau nur bei Quellwechsel), addChildAt(sprite, 0) unter ALLEN Elementen, alpha/scale/position/rotation aus BackgroundDrawConfig, ohne Bild wird das Sprite sauber entfernt (Hintergrund aus); Legacy-RenderEngine.drawBackground hatte bereits korrekte Transform-Parität (Zeichenreihenfolge Grid→Background→Elemente); Layer-Baumansicht: bereits vorhanden (LayerPanel baut echten Baum via parentId mit Unterebenen/Gruppen/Elementen) — keine Änderung nötig; tsc-Fix selbst erkannt: Container heißt root nicht world | Frontend: 789/789 Tests (+5 backgroundRendering.test.ts), tsc 0, Build EXIT 0 | siehe git | CAD-6 COMPLETE; nächster Task CAD-7 (magnetisches Traversen-Tool) |
|
||||
| 2026-08-31 | CAD-5 | MLINE + Block-Editor: (1) mlineTool NEU in core-drawing (13 Tools) — Mehrfachlinie für Wände/Rohre: Klick-Sequenz sammelt Mittellinien-Punkte, offsetPolyline-Helfer berechnet ZWEI parallele Punktketten ±gap/2 entlang der Normalen (an inneren Punkten Mittelung der angrenzenden Segment-Normalen = Miter-Joins wie bei echten Wänden), gap-Option (Gesamtstärke), optionale Endkappen (cap-Checkbox) verbinden beide Linien an Start/Ende als line-Elemente mit mlineCap-Flag, ENTER committet ab 2 Punkten (boolean-Konsum), ALLE Teile (2 Linien + ggf. 2 Kappen) in EINER doc.transact = 1 Undo-Schritt, mline:true+gap-Metadaten, L-förmige Pfade folgen der Geometrie (gleiche Punktanzahl); (2) blockEditorService NEU — openBlockForEdit legt markierte KOPIEN der Block-Elemente ins Dokument (Original unangetastet, BLOCK_EDIT_MARKER-Flag + __blockEdit-IDs), saveBlockEdit schreibt die bearbeiteten Kopien als neue Elementliste in die Definition zurück (Marker entfernt) und räumt Edit-Kopien auf, discardBlockEdit verwirft alles; (3) blockEditTool NEU in core-modify (26 Tools) — Klick 1 auf Block-Instanz öffnet (via selection.hitTest + blockId-Referenz, Status führt), Kopien normal bearbeiten mit allen Werkzeugen, Klick 2 speichert zurück, cancel verwirft; Zwischenf\e4lle selbst gelöst: BLOCK_EDIT_MARKER fehlte als Re-Export in core-modify (Test bekam undefined) + Service-State-Leak zwischen Tests (afterEach-Discard) + Kappen-Test a/b-Vorzeichenkonvention reihenfolge-agnostisch gemacht | Frontend: 784/784 Tests (+21 mlineBlockEditor.test.ts), tsc 0, Build EXIT 0 | 0ffe954 | **CAD-5 COMPLETE — POST-2.0 CAD-AUSBAU VOLLSTÄNDIG** (CAD-1 bis CAD-5: Bemassung, Hatch, Box-Select, OTRACK/DIVIDE/MEASURE, MLINE/Block-Editor) |
|
||||
| 2026-08-30 | CAD-4 | OTRACK + DIVIDE/MEASURE: (1) computeTrackPoint NEU in SnapEngine (reine Funktion, Task CAD-4) — Objekt-Fang-Tracking: projiziert den Cursor auf Tracking-Linien durch den Basispunkt (Default-Winkel 0/90/180/270°, optional erweiterte Winkelliste z.B. 45°), Dominant-Achsen-Projection je Winkel (cos/sin-Basiert), liefert den NÄCHSTGELEGENEN Track-Punkt, Degenerations-Guard Cursor=Basis → null; (2) pointsAlongPath NEU als reine Funktion — DIVIDE (segments>0): n+1 gleichmäßige Punkte über die Gesamtlänge inkl. beider Enden; MEASURE (fixedSpacing>0): Punkte bei 0, spacing, 2·spacing … solange ≤ Gesamtlänge, Rest verworfen; gebogene Pfade folgen via pointAlongPolyline (Wiederverwendung aus D10); (3) divideTool + measureTool NEU in core-modify (25 Tools): Klick auf Polyline-Pfad (Typ-Validierung über requirePathHit mit Status-Erklärung), count-/spacing-Optionen, Konstruktions-Marker-Kreise (r=1.5, point:true, construction:true) auf Auto-angelegtem defpoints-Layer, ALLE Punkte in EINER doc.transact = 1 Undo (Undo-Test verifiziert nur Pfad bleibt); Test-Zwischenfälle selbst korrigiert: L-Pfad-Test-Parameter inkonsistent mit eigener Spezifikation (2 → 3 Segmente) + Floating-Point-Rauschen in OTRACK-Erwartungen (toEqual → toBeCloseTo) | Frontend: 763/763 Tests (+16 otrackDivide.test.ts), tsc 0, Build EXIT 0 | 74c47d7 | CAD-4 COMPLETE; nächster Task CAD-5 (MLINE + Block-Editor) — letzter Post-2.0-Punkt |
|
||||
@@ -759,8 +760,8 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
||||
- `frontend/src/utils/__tests__/format.test.ts` liegt in src statt tests/ — vereinheitlichen (kleiner Task in I2)
|
||||
|
||||
---
|
||||
*Post-2.0 CAD-Ausbau: CAD-1 bis CAD-6 fertig. Nächster Task: **CAD-7** (magnetisches Traversen-Tool: Traversen platzieren mit End-zu-End-Andocken). Danach nach Bedarf.*
|
||||
*Status 2026-08-31: RELEASE 2.0.0 LIVE + Post-2.0 CAD-Ausbau CAD-1 bis CAD-6 fertig (CAD-6: Hintergrund-Grundrisse endlich gerendert). FE 789/789 Tests, BE 287/287, tsc/Build grün, Prod-Deploy verifiziert. Tag v2.0.0 auf Forgejo main.*
|
||||
*Post-2.0 CAD-Ausbau: CAD-1 bis CAD-7 fertig (inkl. Nutzer-Wuensche: Hintergrund-Grundrisse mit Massstab CAD-6, magnetisches Traversen-Tool CAD-7). Neue Anforderungen ueber neue Roadmap-Eintraege.*
|
||||
*Status 2026-08-31: RELEASE 2.0.0 LIVE + Post-2.0 CAD-Ausbau CAD-1 bis CAD-7 fertig (Grundrisse + magnetische Traversen). FE 807/807 Tests, BE 287/287, tsc/Build grün, Prod-Deploy verifiziert. Tag v2.0.0 auf Forgejo main.*
|
||||
|
||||
## Post-2.0 CAD-Ausbau (neue Task-Reihe)
|
||||
|
||||
@@ -770,4 +771,5 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
||||
- [x] CAD-4 Objekt-Fang-Tracking (OTRACK) + DIVIDE/MEASURE *(2026-08-30: computeTrackPoint (Winkel-Projektion auf Tracking-Linien, Default 0/90/180/270°, erweiterte Winkel optional, nächstgelegener Track-Punkt) als reine Funktion in SnapEngine; pointsAlongPath (DIVIDE n+1 gleichmäßig inkl. Enden / MEASURE fester Abstand, Rest verworfen) unter Wiederverwendung von pointAlongPolyline; divideTool + measureTool mit defpoints-Markern in EINER Transaktion; Log CAD-4)*
|
||||
- [x] CAD-5 Multiline (MLINE) + Block-Editor *(2026-08-31: mlineTool (Doppel-Linie über Normalen-Offset ±gap/2 mit Miter an Zwischenpunkten, optionale Endkappen, ENTER-Commit ab 2 Punkten, 1 Transaktion) + blockEditorService (openBlockForEdit markierte Kopien / saveBlockEdit zurückschreiben / discardBlockEdit verwerfen) + blockEditTool (Klick öffnet, Klick speichert, cancel verwirft); Log CAD-5)*
|
||||
- [x] CAD-6 Hintergrund-Grundrisse mit Maßstab rendern *(2026-08-31: BUG-Fix — bgConfig wurde von App an CanvasArea uebergeben aber NIE genutzt: Hintergrund-Import funktionierte im Dialog, wurde aber nie gezeichnet; CanvasArea-Effect setzt jetzt backgroundSrc/Scale/OffsetX/Y/Rotation/Opacity via renderEngine.setOptions + render; PixiRenderer.drawBackground als echte Sprite-Implementierung (Bild-Cache, addChildAt Index 0 unter allen Elementen, alpha/rotation/scale); Layer-Baumansicht existierte bereits mit Unterebenen; Log CAD-6)*
|
||||
- [x] CAD-7 Magnetisches Traversen-Tool (Truss) *(2026-08-31: trussTool mit Drag-Richtung, Laengen-Optionen 100/200/290cm, snapTrussToDock fuer magnetisches End-zu-End-Andocken mit Rotations-Vererbung + Gegenrichtung am Startpunkt, truss-ElementType + Rendering mit Gurtlinien; Log CAD-7)*
|
||||
|
||||
|
||||
@@ -421,6 +421,182 @@ export const barrierTool: ToolExtensionV2 = {
|
||||
},
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Task CAD-7: Magnetisches Traversen-Tool (Truss).
|
||||
// Rechteckiger Balken (Laengen-Option 100/200/290cm), Drag
|
||||
// definiert die Richtung. MAGNETISCH: Nahe dem Ende einer
|
||||
// bestehenden Traverse dockt die neue exakt an (Position UND
|
||||
// Rotation uebernommen) - Traversen-Reihen bauen sich selbst.
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Dock-Ergebnis des magnetischen Andockens. */
|
||||
export interface TrussDock {
|
||||
center: Pt;
|
||||
rotationDeg: number;
|
||||
dockedTo: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpunkte einer Traverse in Weltkoordinaten (CAD-7, rein).
|
||||
* Rotation in Grad um das Zentrum (el.x/el.y).
|
||||
*/
|
||||
export function trussEndpoints(el: CADElement): [Pt, Pt] {
|
||||
const length = (el.properties.length as number | undefined) ?? el.width ?? 0;
|
||||
const rot = (((el.properties.rotation as number | undefined) ?? 0) * Math.PI) / 180;
|
||||
const half = length / 2;
|
||||
const dx = Math.cos(rot) * half;
|
||||
const dy = Math.sin(rot) * half;
|
||||
return [
|
||||
{ x: el.x - dx, y: el.y - dy },
|
||||
{ x: el.x + dx, y: el.y + dy },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Magnetisches Andocken (CAD-7, rein): Sucht unter allen Traversen
|
||||
* den Endpunkt in Dock-Toleranz und liefert die Geometrie der
|
||||
* neuen Traverse (Zentrum + Rotation), die exakt dort andockt.
|
||||
* dockedTo = ID der Anker-Traverse; null = kein Dock in Reichweite.
|
||||
*/
|
||||
export function snapTrussToDock(
|
||||
existing: CADElement[],
|
||||
cursor: Pt,
|
||||
newLength: number,
|
||||
tolerance: number,
|
||||
): TrussDock | null {
|
||||
let best: { dock: TrussDock; dist: number } | null = null;
|
||||
for (const el of existing) {
|
||||
if (String(el.type) !== 'truss' || el.properties.truss !== true) continue;
|
||||
const rotDeg = (el.properties.rotation as number | undefined) ?? 0;
|
||||
const rot = (rotDeg * Math.PI) / 180;
|
||||
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++) {
|
||||
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;
|
||||
// Neue Traverse: fuehrt VOM Dock-Ende WEG von der Anker-Traverse.
|
||||
// Endpunkt 0 (Start) -> Gegenrichtung (rot+180), Endpunkt 1 -> vorwaerts.
|
||||
const dir = endIdx === 0 ? -1 : 1;
|
||||
const centerX = end.x + dx * half * dir;
|
||||
const centerY = end.y + dy * half * dir;
|
||||
const newRot = endIdx === 0 ? (rotDeg + 180) % 360 : rotDeg;
|
||||
best = {
|
||||
dist,
|
||||
dock: {
|
||||
center: { x: centerX, y: centerY },
|
||||
rotationDeg: newRot,
|
||||
dockedTo: el.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return best ? best.dock : null;
|
||||
}
|
||||
|
||||
/** Traversen-Drag-Sitzung (CAD-7). */
|
||||
let trussDragStart: Pt | null = null;
|
||||
|
||||
function makeTrussElement(
|
||||
center: Pt,
|
||||
length: number,
|
||||
rotationDeg: number,
|
||||
layerId: string,
|
||||
): CADElement {
|
||||
const rad = (rotationDeg * Math.PI) / 180;
|
||||
const w = Math.abs(length * Math.cos(rad)) || length;
|
||||
const h = Math.abs(length * Math.sin(rad)) || 20;
|
||||
return {
|
||||
id: `truss_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`,
|
||||
type: 'truss',
|
||||
layerId,
|
||||
x: center.x,
|
||||
y: center.y,
|
||||
width: Math.max(w, 20),
|
||||
height: 20,
|
||||
properties: { length, rotation: rotationDeg, truss: true, stroke: '#9aa0a6', strokeWidth: 2, fill: '#6b7280' },
|
||||
} as unknown as CADElement;
|
||||
}
|
||||
|
||||
const DOCK_TOLERANCE = 25;
|
||||
|
||||
export const trussTool: ToolExtensionV2 = {
|
||||
manifest: {
|
||||
id: 'truss',
|
||||
label: 'Traverse (magnetisch)',
|
||||
icon: '\u2550',
|
||||
ribbonTab: 'insert',
|
||||
tags: ['basic'],
|
||||
description: 'Traverse platzieren: Drag = Richtung, dockt magnetisch an bestehenden Traversen an (CAD-7)',
|
||||
},
|
||||
optionsSchema: [
|
||||
{ key: 'length', label: 'Länge (cm)', type: 'select', options: [
|
||||
{ value: '100', label: '100 cm' },
|
||||
{ value: '200', label: '200 cm' },
|
||||
{ value: '290', label: '290 cm' },
|
||||
] },
|
||||
],
|
||||
handlers: {
|
||||
down(e, ctx) {
|
||||
trussDragStart = e.world;
|
||||
ctx.setStatus('Traverse: Richtung ziehen (dockt automatisch an Traversen an)');
|
||||
},
|
||||
move(e, ctx) {
|
||||
const c = ctx as FullToolContext;
|
||||
if (!trussDragStart) return;
|
||||
const length = Number((c.options.length as string | undefined) ?? 200) || 200;
|
||||
// Magnet-Vorschau: dockt der Cursor an ein Traversen-Ende?
|
||||
const existing = c.doc ? (c.doc.getAllElements() as CADElement[]).filter(
|
||||
(el) => String(el.type) === 'truss',
|
||||
) : [];
|
||||
const dock = snapTrussToDock(existing, trussDragStart, length, DOCK_TOLERANCE);
|
||||
if (dock) {
|
||||
ctx.setPreview?.(makeTrussElement(dock.center, length, dock.rotationDeg, 'layer-0'));
|
||||
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'));
|
||||
},
|
||||
up(e, ctx) {
|
||||
const c = ctx as FullToolContext;
|
||||
const start = trussDragStart;
|
||||
trussDragStart = null;
|
||||
ctx.setPreview?.(null);
|
||||
if (!start || !c.doc) return;
|
||||
const length = Number((c.options.length as string | undefined) ?? 200) || 200;
|
||||
const layerId = (c.options.layerId as string | undefined) ?? 'layer-0';
|
||||
// MAGNETISCH: Dock-Pruefung am Drag-Startpunkt
|
||||
const existing = (c.doc.getAllElements() as CADElement[]).filter(
|
||||
(el) => String(el.type) === 'truss',
|
||||
);
|
||||
const dock = snapTrussToDock(existing, start, length, DOCK_TOLERANCE);
|
||||
let el: CADElement;
|
||||
if (dock) {
|
||||
el = makeTrussElement(dock.center, length, dock.rotationDeg, layerId);
|
||||
ctx.setStatus(`Traverse angedockt an ${dock.dockedTo} (${length}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);
|
||||
ctx.setStatus(`Traverse platziert (${length}cm)`);
|
||||
}
|
||||
c.doc.transact(() => { c.doc!.addElement(el); });
|
||||
},
|
||||
cancel(ctx) {
|
||||
trussDragStart = null;
|
||||
ctx.setPreview?.(null);
|
||||
ctx.setStatus('Traverse abgebrochen');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const eventToolsV2Plugin: PluginV2 = {
|
||||
manifest: {
|
||||
id: 'event-tools',
|
||||
@@ -431,5 +607,5 @@ export const eventToolsV2Plugin: PluginV2 = {
|
||||
category: 'elements',
|
||||
enabledByDefault: true,
|
||||
},
|
||||
tools: [chairTool, seatingRowToolDrag, seatingBlockToolDrag, tableTool, stageTool, arcRowTool, kinoTemplateTool, banquetTemplateTool, standingTableTemplateTool, podiumTemplateTool, curtainTool, spotlightTool, barrierTool],
|
||||
tools: [chairTool, seatingRowToolDrag, seatingBlockToolDrag, tableTool, stageTool, arcRowTool, kinoTemplateTool, banquetTemplateTool, standingTableTemplateTool, podiumTemplateTool, curtainTool, spotlightTool, barrierTool, trussTool],
|
||||
};
|
||||
|
||||
@@ -399,6 +399,42 @@ export function elementToPrimitives(el: CADElement, attrDefs?: Array<{ tag: stri
|
||||
return [];
|
||||
}
|
||||
|
||||
case 'truss': {
|
||||
// Traverse (CAD-7): Balken als Rechteck + 2 Gurtlinien entlang
|
||||
// der Rotation (wie ein Fachwerk-Profil)
|
||||
const length = (props.length as number | undefined) ?? el.width;
|
||||
const rotDeg = (props.rotation as number | undefined) ?? 0;
|
||||
const rad = (rotDeg * Math.PI) / 180;
|
||||
const dx = Math.cos(rad) * (length / 2);
|
||||
const dy = Math.sin(rad) * (length / 2);
|
||||
const nx = -Math.sin(rad) * 10; // Normalen-Offset (halbe Hoehe)
|
||||
const ny = Math.cos(rad) * 10;
|
||||
const c = { x: el.x, y: el.y };
|
||||
return [
|
||||
{
|
||||
kind: 'rect',
|
||||
x: c.x - dx, y: c.y - dy,
|
||||
w: length, h: 20,
|
||||
fill: fillOrNull(props.fill) ?? '#6b7280',
|
||||
stroke: (props.stroke as string | undefined) ?? '#9aa0a6',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
// Gurtlinien oben/unten entlang der Traverse
|
||||
{
|
||||
kind: 'line',
|
||||
from: { x: c.x - dx + nx, y: c.y - dy + ny },
|
||||
to: { x: c.x + dx + nx, y: c.y + dy + ny },
|
||||
stroke: '#4b5563', strokeWidth: 1,
|
||||
},
|
||||
{
|
||||
kind: 'line',
|
||||
from: { x: c.x - dx - nx, y: c.y - dy - ny },
|
||||
to: { x: c.x + dx - nx, y: c.y + dy - ny },
|
||||
stroke: '#4b5563', strokeWidth: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
case 'barrier': {
|
||||
// Absperrung (Task E7): Kette aus Kreis-Paaren (properties.circles).
|
||||
const circles = Array.isArray(props.circles)
|
||||
|
||||
@@ -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';
|
||||
| 'curtain' | 'spotlight' | 'barrier' | 'truss';
|
||||
|
||||
export type LineType = 'solid' | 'dashed' | 'dotted';
|
||||
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Task CAD-7 - Magnetisches Traversen-Tool.
|
||||
*
|
||||
* Traversen als rechteckige Balken (Laengen-Option 100/200/290cm,
|
||||
* Standard 200). MAGNETISCH: Nahe dem Ende einer bestehenden
|
||||
* Traverse dockt die neue exakt an (Position + Rotation uebernommen)
|
||||
* - trussEndpoints (Endpunkte in Weltkoordinaten, rotationsbewusst)
|
||||
* - snapTrussToDock (Dock-Suche + Andock-Geometrie, rein)
|
||||
* - trussTool: Klick platziert (gedockt oder frei), Drag = Richtung.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import {
|
||||
trussTool,
|
||||
trussEndpoints,
|
||||
snapTrussToDock,
|
||||
type TrussDock,
|
||||
} from '../src/plugins/builtin/event-tools';
|
||||
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, mods: { shift?: boolean } = {}): { world: Pt; shift: boolean } {
|
||||
return { world: { x, y }, shift: mods.shift ?? false } as never;
|
||||
}
|
||||
|
||||
function makeTruss(id: string, x: number, y: number, length: number, rotationDeg = 0): CADElement {
|
||||
return {
|
||||
id, type: 'truss', layerId: 'layer-0',
|
||||
x, y,
|
||||
width: length, height: 20,
|
||||
properties: { length, rotation: rotationDeg, truss: true },
|
||||
} as unknown as CADElement;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
trussTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
|
||||
});
|
||||
|
||||
// ─── trussEndpoints (reine Funktion) ──────────────────────────
|
||||
|
||||
describe('CAD-7: trussEndpoints', () => {
|
||||
it('horizontale Traverse: Endpunkte links/rechts vom Zentrum', () => {
|
||||
const el = makeTruss('t1', 100, 100, 200, 0);
|
||||
const [a, b] = trussEndpoints(el);
|
||||
expect(a).toEqual({ x: 0, y: 100 });
|
||||
expect(b).toEqual({ x: 200, y: 100 });
|
||||
});
|
||||
|
||||
it('90-Grad-Traverse: Endpunkte vertikal', () => {
|
||||
const el = makeTruss('t1', 100, 100, 200, 90);
|
||||
const [a, b] = trussEndpoints(el);
|
||||
expect(a.x).toBeCloseTo(100, 5);
|
||||
expect(a.y).toBeCloseTo(0, 5);
|
||||
expect(b.x).toBeCloseTo(100, 5);
|
||||
expect(b.y).toBeCloseTo(200, 5);
|
||||
});
|
||||
|
||||
it('45-Grad-Traverse: diagonale Endpunkte', () => {
|
||||
const el = makeTruss('t1', 100, 100, 200, 45);
|
||||
const [a, b] = trussEndpoints(el);
|
||||
const half = 100;
|
||||
const cos45 = Math.cos(Math.PI / 4) * half;
|
||||
expect(a.x).toBeCloseTo(100 - cos45, 3);
|
||||
expect(a.y).toBeCloseTo(100 - cos45, 3);
|
||||
expect(b.x).toBeCloseTo(100 + cos45, 3);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── snapTrussToDock (magnetisches Andocken, rein) ──────────
|
||||
|
||||
describe('CAD-7: snapTrussToDock (magnetisch)', () => {
|
||||
const existing = makeTruss('t1', 100, 100, 200, 0); // Enden (0,100) und (200,100)
|
||||
const tol = 25;
|
||||
|
||||
it('Cursor nahe Endpunkt (200,100): dockt exakt an mit Rotation 0', () => {
|
||||
const result = snapTrussToDock([existing], { x: 210, y: 105 }, 200, tol);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.rotationDeg).toBeCloseTo(0, 5);
|
||||
// Neue Traverse startet am Dock (200,100) → Zentrum bei (200+100, 100)
|
||||
expect(result!.center.x).toBeCloseTo(300, 3);
|
||||
expect(result!.center.y).toBeCloseTo(100, 3);
|
||||
expect(result!.dockedTo).toBe('t1');
|
||||
});
|
||||
|
||||
it('Cursor nahe Startpunkt (0,100): dockt in GEGENRICHTUNG (Rotation 180)', () => {
|
||||
const result = snapTrussToDock([existing], { x: -10, y: 98 }, 200, tol);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.rotationDeg).toBeCloseTo(180, 5);
|
||||
expect(result!.center.x).toBeCloseTo(-100, 3);
|
||||
});
|
||||
|
||||
it('Cursor weiter weg als Toleranz: kein Dock (null)', () => {
|
||||
const result = snapTrussToDock([existing], { x: 500, y: 500 }, 200, tol);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('andocken an rotierte Traverse (90 Grad): Rotation wird uebernommen', () => {
|
||||
const vert = makeTruss('v1', 100, 100, 200, 90); // Enden (100,0) und (100,200)
|
||||
const result = snapTrussToDock([vert], { x: 105, y: 210 }, 200, tol);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.rotationDeg).toBeCloseTo(90, 5);
|
||||
expect(result!.center.x).toBeCloseTo(100, 3);
|
||||
expect(result!.center.y).toBeCloseTo(300, 3);
|
||||
});
|
||||
|
||||
it('mehrere Traversen: naechstgelegener Dock gewinnt', () => {
|
||||
const other = makeTruss('t2', 400, 100, 200, 0); // Enden (300,100),(500,100)
|
||||
const result = snapTrussToDock([existing, other], { x: 305, y: 100 }, 200, tol);
|
||||
expect(result!.dockedTo).toBe('t2');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── trussTool (Platzierung + Magnetismus) ──────────────────
|
||||
|
||||
describe('CAD-7: trussTool', () => {
|
||||
it('Klick platziert freie Traverse (200cm Standard) als truss-Element', () => {
|
||||
const { ctx, doc } = mkCtx();
|
||||
trussTool.handlers.down!(pe(300, 300), ctx);
|
||||
trussTool.handlers.up!(pe(400, 300), ctx);
|
||||
const els = doc.getAllElements();
|
||||
expect(els).toHaveLength(1);
|
||||
expect(els[0].type).toBe('truss');
|
||||
expect(els[0].properties.length).toBe(200);
|
||||
expect(els[0].properties.truss).toBe(true);
|
||||
});
|
||||
|
||||
it('Drag definiert die Richtung: horizontale Traverse bei Drag nach rechts', () => {
|
||||
const { ctx, doc } = mkCtx();
|
||||
trussTool.handlers.down!(pe(100, 100), ctx);
|
||||
trussTool.handlers.up!(pe(300, 100), ctx);
|
||||
const el = doc.getAllElements()[0];
|
||||
expect(el.properties.rotation).toBeCloseTo(0, 5);
|
||||
// Laenge aus Drag-Distanz? Nein: Standardlaenge bleibt 200 (Option)
|
||||
expect(el.properties.length).toBe(200);
|
||||
});
|
||||
|
||||
it('Drag nach oben: Rotation 90', () => {
|
||||
const { ctx, doc } = mkCtx();
|
||||
trussTool.handlers.down!(pe(100, 100), ctx);
|
||||
trussTool.handlers.up!(pe(100, 300), ctx);
|
||||
expect(doc.getAllElements()[0].properties.rotation).toBeCloseTo(90, 1);
|
||||
});
|
||||
|
||||
it('MAGNETISCH: Klick nahe bestehendem Traversen-Ende dockt exakt an', () => {
|
||||
const existing = makeTruss('t1', 100, 100, 200, 0);
|
||||
const { ctx, doc } = mkCtx([existing]);
|
||||
// Klick nahe (200,100) → andocken
|
||||
trussTool.handlers.down!(pe(210, 105), ctx);
|
||||
trussTool.handlers.up!(pe(300, 100), ctx);
|
||||
const els = doc.getAllElements().filter((e) => e.properties.truss);
|
||||
expect(els).toHaveLength(2);
|
||||
const newTruss = els.find((e) => e.id !== 't1')!;
|
||||
// Exakt angedockt: Rotation 0, Start am Ende der ersten
|
||||
expect(newTruss.properties.rotation).toBeCloseTo(0, 5);
|
||||
expect(newTruss.x).toBeCloseTo(300, 3);
|
||||
expect(newTruss.y).toBeCloseTo(100, 3);
|
||||
});
|
||||
|
||||
it('ohne bestehende Traversen: freie Platzierung (kein Crash)', () => {
|
||||
const { ctx, doc } = mkCtx();
|
||||
trussTool.handlers.down!(pe(100, 100), ctx);
|
||||
trussTool.handlers.up!(pe(200, 100), ctx);
|
||||
expect(doc.getAllElements()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('Laengen-Option: 290 statt 200', () => {
|
||||
const { ctx, doc } = mkCtx([], { length: 290 });
|
||||
trussTool.handlers.down!(pe(100, 100), ctx);
|
||||
trussTool.handlers.up!(pe(300, 100), ctx);
|
||||
expect(doc.getAllElements()[0].properties.length).toBe(290);
|
||||
});
|
||||
|
||||
it('ALLES in EINER Transaktion (1 Undo entfernt die Traverse)', () => {
|
||||
const { ctx, doc } = mkCtx();
|
||||
trussTool.handlers.down!(pe(100, 100), ctx);
|
||||
trussTool.handlers.up!(pe(300, 100), ctx);
|
||||
expect(doc.getAllElements()).toHaveLength(1);
|
||||
doc.undo();
|
||||
expect(doc.getAllElements()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('cancel resettet die Drag-Sitzung', () => {
|
||||
const { ctx, doc } = mkCtx();
|
||||
trussTool.handlers.down!(pe(100, 100), ctx);
|
||||
trussTool.handlers.cancel?.(ctx);
|
||||
trussTool.handlers.up!(pe(300, 100), ctx);
|
||||
expect(doc.getAllElements()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('Preview bei move zwischen down und up', () => {
|
||||
const { ctx, previews } = mkCtx();
|
||||
trussTool.handlers.down!(pe(100, 100), ctx);
|
||||
trussTool.handlers.move!(pe(250, 100), ctx);
|
||||
expect(previews.length).toBeGreaterThanOrEqual(1);
|
||||
const last = previews[previews.length - 1];
|
||||
expect(last).not.toBeNull();
|
||||
});
|
||||
|
||||
it('Manifest: id=truss, Optionen length/rotation', () => {
|
||||
expect(trussTool.manifest.id).toBe('truss');
|
||||
const keys = (trussTool.optionsSchema ?? []).map((f) => f.key);
|
||||
expect(keys).toContain('length');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user