task(A4.10): migrate trim/extend/fillet to v2 + real CAD extend fallback

This commit is contained in:
Agent Zero
2026-08-27 01:21:44 +02:00
parent ebafcf0c65
commit 19f838fbf8
3 changed files with 271 additions and 7 deletions
+69 -2
View File
@@ -272,8 +272,50 @@ export function extendElement(el: CADElement, boundary: CADElement): CADElement
return null;
}
const intersect = findIntersection(el, boundary);
if (!intersect) return null;
let intersect = findIntersection(el, boundary);
// Echtes CAD-Extend (Fallback): Wenn die Strecke die Kante NICHT berührt,
// suche Schnitte auf der VERLÄNGERUNG der Linie in Richtung des nahen Endes.
if (!intersect) {
const c = { x: boundary.x, y: boundary.y };
const r =
boundary.type === 'circle' || boundary.type === 'arc'
? ((boundary.properties?.radius as number | undefined) ?? boundary.width / 2)
: null;
if (r === null) return null; // nur Kreis/Bogen-Kanten erweiterbar
const candidates = lineCircleIntersectionsInfinite(
{ x: p.x1!, y: p.y1! },
{ x: p.x2!, y: p.y2! },
c,
r as number,
);
if (candidates.length === 0) return null;
// Wähle den Schnittpunkt, der an der Seite des NÄHEREN Endes liegt:
// Erst feststellen, welches Ende näher am Kreiszentrum liegt → dieses wird gestreckt.
const d1 = Math.hypot(p.x1! - c.x, p.y1! - c.y);
const d2 = Math.hypot(p.x2! - c.x, p.y2! - c.y);
const nearerIsStart = d1 <= d2;
const anchor = nearerIsStart ? { x: p.x1!, y: p.y1! } : { x: p.x2!, y: p.y2! };
intersect = candidates.reduce((best, cur) =>
Math.hypot(cur.x - anchor.x, cur.y - anchor.y) < Math.hypot(best.x - anchor.x, best.y - anchor.y) ? cur : best,
);
// Neu berechnete bbox/center auf Basis der Länge nach dem Strekken:
const nx1 = nearerIsStart ? intersect.x : p.x1!;
const ny1 = nearerIsStart ? intersect.y : p.y1!;
const nx2 = nearerIsStart ? p.x2! : intersect.x;
const ny2 = nearerIsStart ? p.y2! : intersect.y;
return {
...el,
x: (nx1 + nx2) / 2,
y: (ny1 + ny2) / 2,
width: Math.abs(nx2 - nx1),
height: Math.abs(ny2 - ny1),
properties: { ...p, x1: nx1, y1: ny1, x2: nx2, y2: ny2 },
};
}
const props = { ...el.properties };
// Extend the end that is closer to the intersection
@@ -409,6 +451,31 @@ function angleInArcRange(deg: number, startDeg: number, endDeg: number): boolean
return a >= s || a <= e; // Bogen über 0°
}
/**
* Wie lineCircleIntersections, aber OHNE Segmentbegrenzung (t ∈ ):
* findet Schnitte auch auf der VERLÄNGERUNG der Strecke — Grundlage für
* echtes CAD-Extend (Task A4.10), bei dem das Ziel die Kante oft noch
* gar nicht berührt.
*/
export function lineCircleIntersectionsInfinite(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;
if (a < 1e-12) return [];
const b = 2 * (fx * dx + fy * dy);
const cc = fx * fx + fy * fy - r * r;
let disc = b * b - 4 * a * cc;
if (disc < 0) return [];
disc = Math.sqrt(disc);
const out: Pt[] = [];
for (const t of [(-b - disc) / (2 * a), (-b + disc) / (2 * a)]) {
out.push({ x: p1.x + t * dx, y: p1.y + t * dy });
}
return out;
}
/**
* Alle Schnittpunkte einer Strecke p1→p2 mit dem Bogen eines Elements
* (Konvention wie RenderEngine.drawArc). Nur Treffer innerhalb des Segments