/** * Task D11 - Snap-Erweiterungen: tangent, perpendicular, quadrant, fromOffset. * * quadrant: Kreis-Quadrantenpunkte (0/90/180/270 auf dem Radius). * perpendicular: Fuspunkt auf Linie senkrecht zum Referenzpunkt. * tangent: Tangentialpunkte am Kreis vom Referenzpunkt. * fromOffset: Basispunkt + dx/dy-Eingabefeld. */ import { describe, it, expect } from 'vitest'; import { SnapEngine, fromOffset, type SnapConfig } from '../src/canvas/SnapEngine'; import type { CADElement } from '../src/types/cad.types'; function cfg(modes: string[], extra: Partial = {}): Partial { return { enabled: true, modes: new Set(modes as never), tolerance: 10, ...extra, }; } function circle(id: string, cx: number, cy: number, r: number): CADElement { return { id, type: 'circle', layerId: 'l', x: cx, y: cy, width: r * 2, height: r * 2, properties: { radius: r }, } as unknown as CADElement; } function line(id: string, x1: number, y1: number, x2: number, y2: number): CADElement { return { id, type: 'line', layerId: 'l', x: (x1 + x2) / 2, y: (y1 + y2) / 2, width: Math.abs(x2 - x1), height: Math.abs(y2 - y1), properties: { x1, y1, x2, y2 }, } as unknown as CADElement; } // ─── Quadrant ────────────────────────────────────────────── describe('D11: quadrant snap', () => { it('Kreis r=50: alle 4 Quadrantenpunkte werden geliefert', () => { const eng = new SnapEngine(cfg(['quadrant'])); eng.setElements([circle('c1', 100, 100, 50)]); // Cursor in der Naehe des 0-Quadranten (150,100) const res = eng.snap(148, 100); expect(res.point).not.toBeNull(); expect(res.point!.type).toBe('quadrant'); expect(res.point!.x).toBe(150); expect(res.point!.y).toBe(100); }); it('90-Quadrant (100,50) wird gefunden', () => { const eng = new SnapEngine(cfg(['quadrant'])); eng.setElements([circle('c1', 100, 100, 50)]); const res = eng.snap(100, 52); expect(res.point!.x).toBe(100); expect(res.point!.y).toBe(50); expect(res.point!.type).toBe('quadrant'); }); it('270-Quadrant (unten) wird gefunden', () => { const eng = new SnapEngine(cfg(['quadrant'])); eng.setElements([circle('c1', 100, 100, 50)]); const res = eng.snap(100, 148); expect(res.point!.y).toBe(150); expect(res.point!.type).toBe('quadrant'); }); it('Modus nicht aktiviert -> kein quadrant snap', () => { const eng = new SnapEngine(cfg(['endpoint'])); eng.setElements([circle('c1', 100, 100, 50)]); const res = eng.snap(148, 100); expect(res.point?.type ?? null).not.toBe('quadrant'); }); }); // ─── Perpendicular ───────────────────────────────────────── describe('D11: perpendicular snap', () => { it('Fuspunkt senkrecht vom refPoint auf horizontale Linie', () => { const eng = new SnapEngine(cfg(['perpendicular'])); // Horizontale Linie y=100, refPoint (50,50): Fuspunkt (50,100) eng.setElements([line('l1', 0, 100, 200, 100)]); const res = eng.snap(52, 98, { x: 50, y: 50 }); expect(res.point).not.toBeNull(); expect(res.point!.type).toBe('perpendicular'); expect(res.point!.x).toBe(50); expect(res.point!.y).toBe(100); }); it('Schrge Linie: Fuspunkt berechnet', () => { const eng = new SnapEngine(cfg(['perpendicular'])); // Linie (0,0)-(100,100), refPoint (100,0): Fuspunkt (50,50) eng.setElements([line('l1', 0, 0, 100, 100)]); const res = eng.snap(51, 49, { x: 100, y: 0 }); expect(res.point!.x).toBeCloseTo(50, 0); expect(res.point!.y).toBeCloseTo(50, 0); }); it('ohne refPoint kein perpendicular snap (Modus still)', () => { const eng = new SnapEngine(cfg(['perpendicular'])); eng.setElements([line('l1', 0, 100, 200, 100)]); const res = eng.snap(50, 98); expect(res.point?.type ?? null).not.toBe('perpendicular'); }); }); // ─── Tangent ─────────────────────────────────────────────── describe('D11: tangent snap', () => { it('Tangentialpunkt am Kreis von refPoint (rechts vom Kreis)', () => { const eng = new SnapEngine(cfg(['tangent'])); // Kreis (100,100) r=50, refPoint (200,100), d=100: // halfAng = acos(r/d) = 60° → Tangentenpunkte bei (125, 143.3)/(125, 56.7) eng.setElements([circle('c1', 100, 100, 50)]); const res = eng.snap(127, 145, { x: 200, y: 100 }); expect(res.point).not.toBeNull(); expect(res.point!.type).toBe('tangent'); expect(res.point!.x).toBeCloseTo(125, 0); expect(res.point!.y).toBeCloseTo(143.3, 0); }); it('refPoint AUF dem Kreiszentrum -> kein Tangentenpunkt (degeneriert)', () => { const eng = new SnapEngine(cfg(['tangent'])); eng.setElements([circle('c1', 100, 100, 50)]); const res = eng.snap(148, 100, { x: 100, y: 100 }); expect(res.point?.type ?? null).not.toBe('tangent'); }); }); // ─── fromOffset ──────────────────────────────────────────── describe('D11: fromOffset', () => { it('Basispunkt + dx/dy ergibt Zielpunkt', () => { const p = fromOffset({ x: 100, y: 200 }, 50, -30); expect(p).toEqual({ x: 150, y: 170 }); }); it('dx=dy=0 bleibt am Basispunkt', () => { const p = fromOffset({ x: 10, y: 20 }, 0, 0); expect(p).toEqual({ x: 10, y: 20 }); }); it('negative Offsets funktionieren', () => { const p = fromOffset({ x: 0, y: 0 }, -100, -50); expect(p).toEqual({ x: -100, y: -50 }); }); });