task(B2): line/circle/arc intersections (fixes BUG-2)

This commit is contained in:
Agent Zero
2026-08-26 13:47:25 +02:00
parent 1bbd653d34
commit 95b14cf86b
2 changed files with 268 additions and 12 deletions
+178 -12
View File
@@ -215,7 +215,15 @@ export function offsetElement(el: CADElement, distance: number): CADElement {
*/
export function trimElement(el: CADElement, boundary: CADElement): CADElement | null {
// Simplified: find intersection with boundary, trim line to that point
if (el.type !== 'line' || !el.properties.x1 || !el.properties.x2) return null;
// Hinweis: explizit auf undefined prüfen — Koordinate 0 ist gültig (nicht falsy behandeln)
const p = el.properties;
if (
el.type !== 'line' ||
p.x1 === undefined || p.y1 === undefined ||
p.x2 === undefined || p.y2 === undefined
) {
return null;
}
const intersect = findIntersection(el, boundary);
if (!intersect) return null;
@@ -247,7 +255,15 @@ export function trimElement(el: CADElement, boundary: CADElement): CADElement |
* Currently supports extending lines to a boundary intersection.
*/
export function extendElement(el: CADElement, boundary: CADElement): CADElement | null {
if (el.type !== 'line' || !el.properties.x1 || !el.properties.x2) return null;
// Explizit auf undefined prüfen — Koordinate 0 ist gültig (nicht falsy behandeln)
const p = el.properties;
if (
el.type !== 'line' ||
p.x1 === undefined || p.y1 === undefined ||
p.x2 === undefined || p.y2 === undefined
) {
return null;
}
const intersect = findIntersection(el, boundary);
if (!intersect) return null;
@@ -300,22 +316,172 @@ export function filletElements(el1: CADElement, el2: CADElement, radius: number)
/**
* Find intersection point of two elements (lines only for now).
*/
function findIntersection(el1: CADElement, el2: CADElement): { x: number; y: number } | null {
/** Punkt im 2D-Raum. */
export interface Pt {
x: number;
y: number;
}
/**
* Alle Schnittpunkte einer Strecke p1→p2 mit einem Kreis (Zentrum c, Radius r).
* Liefert nur Punkte innerhalb des Streckensegments (t ∈ [0,1]); [] wenn keine.
*/
export function lineCircleIntersections(p1: Pt, p2: Pt, c: Pt, r: number): Pt[] {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const fx = p1.x - c.x;
const fy = p1.y - c.y;
const a = dx * dx + dy * dy;
const b = 2 * (fx * dx + fy * dy);
const cc = fx * fx + fy * fy - r * r;
let discriminant = b * b - 4 * a * cc;
if (discriminant < 0) return []; // keine reale Lösung
discriminant = Math.sqrt(discriminant);
const t1 = (-b - discriminant) / (2 * a);
const t2 = (-b + discriminant) / (2 * a);
// Tangente: beide Lösungen identisch → nur einen Punkt liefern
const ts = Math.abs(t1 - t2) < 1e-7 ? [t1] : [t1, t2];
const eps = 1e-9;
const results: Pt[] = [];
for (const t of ts) {
if (t >= -eps && t <= 1 + eps) {
results.push({ x: p1.x + t * dx, y: p1.y + t * dy });
}
}
return results;
}
/**
* Schnittpunkte zweier Kreise (Zentren c1/c2, Radien r1/r2).
* Liefert 02 Punkte; [] wenn getrennt oder identisch.
*/
export function circleCircleIntersections(c1: Pt, r1: number, c2: Pt, r2: number): Pt[] {
const dVec = { x: c2.x - c1.x, y: c2.y - c1.y };
const d = Math.hypot(dVec.x, dVec.y);
if (d === 0) return []; // identische Zentren
if (d > r1 + r2 || d < Math.abs(r1 - r2)) return []; // getrennt / ineinander
const a = (r1 * r1 - r2 * r2 + d * d) / (2 * d);
const hSq = r1 * r1 - a * a;
const h = Math.sqrt(Math.max(0, hSq));
const baseX = c1.x + (a * dVec.x) / d;
const baseY = c1.y + (a * dVec.y) / d;
if (h === 0) return [{ x: baseX, y: baseY }]; // tangentiale Berührung
return [
{ x: baseX + (h * dVec.y) / d, y: baseY - (h * dVec.x) / d },
{ x: baseX - (h * dVec.y) / d, y: baseY + (h * dVec.x) / d },
];
}
/**
* Liest die Arc-Parameter eines CADElements nach der Konvention von
* RenderEngine.drawArc: Zentrum = el.x/el.y, radius/startAngle/endAngle aus
* properties; Winkel in GRAD, 0° = 3 Uhr, gegen Uhrzeigersinn.
*/
function readArc(el: CADElement): { c: Pt; r: number; startDeg: number; endDeg: number } | null {
const r = (el.properties?.radius as number | undefined) ?? (el as unknown as { width?: number }).width! / 2;
if (!Number.isFinite(r) || r <= 0) return null;
return {
c: { x: el.x, y: el.y },
r,
startDeg: ((el.properties?.startAngle as number | undefined) ?? 0),
endDeg: ((el.properties?.endAngle as number | undefined) ?? 360),
};
}
/** Prüft, ob ein Winkel (Grad) innerhalb eines Bogens liegt (CCW von start bis end). */
function angleInArcRange(deg: number, startDeg: number, endDeg: number): boolean {
const norm = (v: number) => ((v % 360) + 360) % 360;
let a = norm(deg);
const s = norm(startDeg);
const e = norm(endDeg);
if (s <= e) return a >= s && a <= e;
return a >= s || a <= e; // Bogen über 0°
}
/**
* Alle Schnittpunkte einer Strecke p1→p2 mit dem Bogen eines Elements
* (Konvention wie RenderEngine.drawArc). Nur Treffer innerhalb des Segments
* UND im Winkelbereich des Bogens werden zurückgegeben.
*/
export function lineArcIntersection(p1: Pt, p2: Pt, el: CADElement): Pt[] {
const arc = readArc(el);
if (!arc) return [];
return lineCircleIntersections(p1, p2, arc.c, arc.r)
.filter((pt) => angleInArcRange((Math.atan2(pt.y - arc.c.y, pt.x - arc.c.x) * 180) / Math.PI, arc.startDeg, arc.endDeg));
}
function lineLineIntersections(el1: CADElement, el2: CADElement): Pt[] {
const p1 = el1.properties;
const p2 = el2.properties;
if (p1.x1 === undefined || p1.y1 === undefined || p1.x2 === undefined || p1.y2 === undefined) return null;
if (p2.x1 === undefined || p2.y1 === undefined || p2.x2 === undefined || p2.y2 === undefined) return null;
if (p1.x1 === undefined || p1.y1 === undefined || p1.x2 === undefined || p1.y2 === undefined) return [];
if (p2.x1 === undefined || p2.y1 === undefined || p2.x2 === undefined || p2.y2 === undefined) return [];
// Line-line intersection
const denom = (p1.x1 - p1.x2) * (p2.y1 - p2.y2) - (p1.y1 - p1.y2) * (p2.x1 - p2.x2);
if (Math.abs(denom) < 1e-10) return null;
const denom = (p1.x1! - p1.x2!) * (p2.y1! - p2.y2!) - (p1.y1! - p1.y2!) * (p2.x1! - p2.x2!);
if (Math.abs(denom) < 1e-10) return [];
const t = ((p1.x1 - p2.x1) * (p2.y1 - p2.y2) - (p1.y1 - p2.y1) * (p2.x1 - p2.x2)) / denom;
const x = p1.x1 + t * (p1.x2 - p1.x1);
const y = p1.y1 + t * (p1.y2 - p1.y1);
const t = ((p1.x1! - p2.x1!) * (p2.y1! - p2.y2!) - (p1.y1! - p2.y1!) * (p2.x1! - p2.x2!)) / denom;
const x = p1.x1! + t * (p1.x2! - p1.x1!);
const y = p1.y1! + t * (p1.y2! - p1.y1!);
return [{ x, y }];
}
return { x, y };
/**
* Erster Schnittpunkt zweier CAD-Elemente (line/circle/arc), intern für
* trim/extend/fillet. Bei mehreren Treffern wird der zu el1 nächstgelegene
* gewählt. null wenn keine Kombination unterstützt oder kein Schnitt.
*/
function findIntersection(el1: CADElement, el2: CADElement): { x: number; y: number } | null {
const types = new Set([el1.type, el2.type]);
// Linie immer als erstes Argument normalisieren (reihenfolgeunabhängig)
const [lineEl, otherEl] = el1.type === 'line' ? [el1, el2] : [el2, el1];
let candidates: Pt[] = [];
let refFromFirst = true; // Referenzpunkt von el1 nehmen?
if (types.has('line') && types.size === 1) {
candidates = lineLineIntersections(el1, el2);
refFromFirst = false; // beide sind Linien; Referenz unten via el1-Startpunkt
} else if (types.has('line') && types.has('circle')) {
candidates = lineCircleIntersections(
{ x: lineEl.properties.x1!, y: lineEl.properties.y1! },
{ x: lineEl.properties.x2!, y: lineEl.properties.y2! },
{ x: otherEl.x, y: otherEl.y },
(otherEl.properties.radius as number | undefined) ?? otherEl.width / 2,
);
} else if (types.has('line') && types.has('arc')) {
candidates = lineArcIntersection(
{ x: lineEl.properties.x1!, y: lineEl.properties.y1! },
{ x: lineEl.properties.x2!, y: lineEl.properties.y2! },
otherEl,
);
} else if (types.has('circle') && types.size === 1) {
candidates = circleCircleIntersections(
{ x: el1.x, y: el1.y },
(el1.properties.radius as number | undefined) ?? el1.width / 2,
{ x: el2.x, y: el2.y },
(el2.properties.radius as number | undefined) ?? el2.width / 2,
);
refFromFirst = false;
}
if (candidates.length === 0) return null;
// Nächstliegenden Treffer zu el1 wählen (Startpunkt bei Linie, sonst Zentrum)
const isFirstLine = el1.type === 'line';
const refX = refFromFirst || isFirstLine
? (isFirstLine ? el1.properties.x1 : el1.x)
: el1.x;
const refY = refFromFirst || isFirstLine
? (isFirstLine ? el1.properties.y1 : el1.y)
: el1.y;
let best = candidates[0];
for (const c of candidates) {
if (Math.hypot(c.x - refX!, c.y - refY!) < Math.hypot(best.x - refX!, best.y - refY!)) best = c;
}
return best;
}
/**