task(B2): line/circle/arc intersections (fixes BUG-2)
This commit is contained in:
@@ -10,6 +10,9 @@ import {
|
||||
offsetElement,
|
||||
getElementBBox,
|
||||
distance,
|
||||
lineCircleIntersections,
|
||||
circleCircleIntersections,
|
||||
lineArcIntersection,
|
||||
angleBetween,
|
||||
} from '../src/tools/modification/geometry';
|
||||
import type { CADElement } from '../src/types/cad.types';
|
||||
@@ -293,3 +296,90 @@ describe('geometry – angleBetween', () => {
|
||||
expect(angleBetween({ x: 0, y: 0 }, { x: 0, y: 100 })).toBe(90);
|
||||
});
|
||||
});
|
||||
|
||||
describe('geometry – lineCircleIntersections', () => {
|
||||
const p1 = { x: 0, y: 0 };
|
||||
const p2 = { x: 200, y: 0 };
|
||||
const c = { x: 100, y: 0 };
|
||||
|
||||
it('findet zwei Schnittpunkte bei Sekante', () => {
|
||||
const pts = lineCircleIntersections(p1, p2, c, 40);
|
||||
expect(pts.length).toBe(2);
|
||||
const xs = pts.map((p) => Math.round(p.x)).sort((a, b) => a - b);
|
||||
expect(xs).toEqual([60, 140]);
|
||||
});
|
||||
|
||||
it('findet genau einen Punkt bei Tangente', () => {
|
||||
// Strecke entlang y=40 tangiert Kreis (100,0,r=40)
|
||||
const pts = lineCircleIntersections({ x: 50, y: 40 }, { x: 150, y: 40 }, { x: 100, y: 0 }, 40);
|
||||
expect(pts.length).toBe(1);
|
||||
expect(pts[0].x).toBeCloseTo(100);
|
||||
expect(pts[0].y).toBeCloseTo(40);
|
||||
});
|
||||
|
||||
it('liefert leeres Array ohne Schnitt (Diskriminante < 0)', () => {
|
||||
expect(lineCircleIntersections(p1, p2, { x: 100, y: 500 }, 10)).toEqual([]);
|
||||
});
|
||||
|
||||
it('respektiert Segmentgrenzen t∈[0,1] (Verlaengerung schneidet nicht)', () => {
|
||||
// Kreis liegt hinter dem Endpunkt der Strecke
|
||||
expect(lineCircleIntersections({ x: 0, y: 0 }, { x: 50, y: 0 }, { x: 100, y: 0 }, 30)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('geometry – circleCircleIntersections', () => {
|
||||
it('findet zwei Punkte bei Ueberlappung', () => {
|
||||
const pts = circleCircleIntersections({ x: 0, y: 0 }, 50, { x: 80, y: 0 }, 50);
|
||||
expect(pts.length).toBe(2);
|
||||
for (const pt of pts) {
|
||||
expect(pt.x).toBeCloseTo(40); // Symmetrieachse
|
||||
expect(Math.abs(pt.y)).toBeCloseTo(30); // Höhe via Satz des Pythagoras
|
||||
}
|
||||
});
|
||||
|
||||
it('liefert einen Punkt bei Tangenz', () => {
|
||||
const pts = circleCircleIntersections({ x: 0, y: 0 }, 50, { x: 100, y: 0 }, 50);
|
||||
expect(pts.length).toBe(1);
|
||||
expect(pts[0]).toEqual({ x: 50, y: 0 });
|
||||
});
|
||||
|
||||
it('liefert leer bei getrennten Kreisen und identischen Zentren', () => {
|
||||
expect(circleCircleIntersections({ x: 0, y: 0 }, 20, { x: 500, y: 0 }, 20)).toEqual([]);
|
||||
expect(circleCircleIntersections({ x: 0, y: 0 }, 20, { x: 0, y: 0 }, 20)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('geometry – lineArcIntersection', () => {
|
||||
function makeArc(id: string, cx: number, cy: number, r: number, startDeg: number, endDeg: number): CADElement {
|
||||
return {
|
||||
id,
|
||||
type: 'arc',
|
||||
layerId: 'layer-1',
|
||||
x: cx,
|
||||
y: cy,
|
||||
width: r * 2,
|
||||
height: r * 2,
|
||||
properties: { radius: r, startAngle: startDeg, endAngle: endDeg },
|
||||
} as unknown as CADElement;
|
||||
}
|
||||
|
||||
it('schneidet Strecke mit Halbkreis (0°..180°)', () => {
|
||||
const arc = makeArc('a1', 100, 0, 40, 0, 180); // obere Hälfte (CCW von 3Uhr)
|
||||
const pts = lineArcIntersection({ x: 60, y: 0 }, { x: 140, y: 40 }, arc);
|
||||
// Linie von (60,0) nach (140,40): schneidet Bogen einmal oben
|
||||
expect(pts.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('filtert Treffer außerhalb des Winkelbereichs heraus', () => {
|
||||
const arc = makeArc('a2', 100, 0, 40, 180, 360); // untere Hälfte
|
||||
// Strecke verläuft komplett oberhalb der Mittellinie → Schnitte lägen nur
|
||||
// im oberen Halbkreis (Winkel < 180°), der nicht Teil des Bogens ist.
|
||||
const pts = lineArcIntersection({ x: 60, y: 20 }, { x: 140, y: 40 }, arc);
|
||||
expect(pts).toEqual([]);
|
||||
});
|
||||
|
||||
it('liefert leer bei fehlendem Radius (ungueltiges Element)', () => {
|
||||
const bad = { id: 'x', type: 'arc', layerId: 'l', x: 0, y: 0, width: 0, height: 0, properties: {} } as unknown as CADElement;
|
||||
expect(lineArcIntersection({ x: -10, y: -10 }, { x: 10, y: 10 }, bad)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user