task(D11): snap extensions - quadrant (4 circle quadrants), perpendicular (foot point from refPoint), tangent (both tangent points via acos), fromOffset helper

This commit is contained in:
Agent Zero
2026-08-29 03:32:42 +02:00
parent 3c806437d6
commit c4e3206932
2 changed files with 244 additions and 1 deletions
+95 -1
View File
@@ -104,6 +104,16 @@ export class SnapEngine {
if (this.config.modes.has('nearest')) {
this.collectNearest(el, worldX, worldY, tol, candidates);
}
// ── Task D11: erweiterte Snaps ──
if (this.config.modes.has('quadrant')) {
this.collectQuadrant(el, worldX, worldY, tol, candidates);
}
if (this.config.modes.has('perpendicular') && refPoint) {
this.collectPerpendicular(el, worldX, worldY, refPoint, tol, candidates);
}
if (this.config.modes.has('tangent') && refPoint) {
this.collectTangent(el, worldX, worldY, refPoint, tol, candidates);
}
}
// Intersection snap (between pairs)
@@ -124,7 +134,8 @@ export class SnapEngine {
// Priority: endpoint > intersection > center > midpoint > nearest > grid
const priority: Record<string, number> = {
endpoint: 0, intersection: 1, center: 2, midpoint: 3, nearest: 4, grid: 5,
endpoint: 0, intersection: 1, center: 2, quadrant: 3, perpendicular: 4,
tangent: 5, midpoint: 6, nearest: 7, grid: 8,
};
// Find best within tolerance — prefer higher priority if distances are close
@@ -234,6 +245,79 @@ export class SnapEngine {
}
}
/** Task D11: Quadranten-Punkte bei Kreis/Bogen (0/90/180/270). */
private collectQuadrant(el: CADElement, wx: number, wy: number, tol: number, out: SnapPoint[]): void {
const check = (x: number, y: number) => {
const d = Math.sqrt((wx - x) ** 2 + (wy - y) ** 2);
if (d < tol) out.push({ x, y, type: 'quadrant' });
};
if (el.type === 'circle' || el.type === 'arc') {
const r = (el.properties as Record<string, unknown>).radius as number | undefined ?? el.width / 2;
check(el.x + r, el.y); // 0
check(el.x, el.y + r); // 90
check(el.x - r, el.y); // 180
check(el.x, el.y - r); // 270
}
}
/**
* Task D11: Perpendicular-Fußpunkt — der Punkt auf einem Segment,
* an dem die Verbindung zum refPoint SENKRECHT auf dem Segment steht.
*/
private collectPerpendicular(
el: CADElement,
wx: number, wy: number,
refPoint: { x: number; y: number },
tol: number,
out: SnapPoint[],
): void {
const p = el.properties as Record<string, unknown>;
const segs: Array<Array<{ x: number; y: number }>> = [];
if (
p.x1 !== undefined && p.y1 !== undefined &&
p.x2 !== undefined && p.y2 !== undefined
) {
segs.push([{ x: p.x1 as number, y: p.y1 as number }, { x: p.x2 as number, y: p.y2 as number }]);
} else if (Array.isArray(p.points)) {
const pts = p.points as Array<{ x: number; y: number }>;
for (let i = 0; i < pts.length - 1; i++) segs.push([pts[i], pts[i + 1]]);
}
for (const [a, b] of segs) {
const foot = this.nearestOnSegment(refPoint.x, refPoint.y, a.x, a.y, b.x, b.y);
const d = Math.sqrt((wx - foot.x) ** 2 + (wy - foot.y) ** 2);
if (d < tol) out.push({ x: foot.x, y: foot.y, type: 'perpendicular' });
}
}
/**
* Task D11: Tangenten-Punkte — Berührpunkte der Tangenten vom
* refPoint an den Kreis (beide Lösungen, klassische Konstruktion
* über rechtwinkliges Dreieck: Winkel = acos(r/d)).
*/
private collectTangent(
el: CADElement,
wx: number, wy: number,
refPoint: { x: number; y: number },
tol: number,
out: SnapPoint[],
): void {
if (el.type !== 'circle' && el.type !== 'arc') return;
const r = ((el.properties as Record<string, unknown>).radius as number | undefined) ?? el.width / 2;
const dx = refPoint.x - el.x;
const dy = refPoint.y - el.y;
const d = Math.hypot(dx, dy);
// refPoint im Zentrum oder im Kreisinneren: keine reale Tangente
if (d <= r) return;
const baseAng = Math.atan2(dy, dx);
const halfAng = Math.acos(r / d);
for (const t of [baseAng + halfAng, baseAng - halfAng]) {
const px = el.x + r * Math.cos(t);
const py = el.y + r * Math.sin(t);
const dist = Math.sqrt((wx - px) ** 2 + (wy - py) ** 2);
if (dist < tol) out.push({ x: px, y: py, type: 'tangent' });
}
}
private collectNearest(el: CADElement, wx: number, wy: number, tol: number, out: SnapPoint[]): void {
const p = el.properties;
const check = (x: number, y: number) => {
@@ -400,3 +484,13 @@ export class SnapEngine {
return { x: snapX, y: snapY, type: 'nearest' };
}
}
// ─────────────────────────────────────────────────────────────
// Task D11: fromOffset — Basispunkt + Eingabefeld dx/dy.
// Reine Funktion (für from-offset-Snap im UI nutzbar).
// ─────────────────────────────────────────────────────────────
/** Zielpunkt = Basispunkt + Offsets dx/dy (Task D11 from-offset). */
export function fromOffset(base: { x: number; y: number }, dx: number, dy: number): { x: number; y: number } {
return { x: base.x + dx, y: base.y + dy };
}