Snap: Center-/Quadrant-Snaps für Kreise + Bögen (Bogen nur im Spannbereich) + Bogen-Endpunkte, mit Tests
This commit is contained in:
+1
-1
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
- [ ] **Zeichenwerkzeuge ergänzen: Kreis + Bogen.** `Drawing2DGeom` hat `circle` und `arc` bereits als Typen; per DXF-Import können beide entstehen, aber es gibt kein Toolbar-Werkzeug zum manuellen Zeichnen. Zwei getrennte Aufgaben:
|
- [ ] **Zeichenwerkzeuge ergänzen: Kreis + Bogen.** `Drawing2DGeom` hat `circle` und `arc` bereits als Typen; per DXF-Import können beide entstehen, aber es gibt kein Toolbar-Werkzeug zum manuellen Zeichnen. Zwei getrennte Aufgaben:
|
||||||
- [x] ~~**Kreis-Toolbar** (trivial)~~ — **erledigt `e454eab` (2026-07-05):** `ToolId`+`"circle"`, Platzhalter `circleTool` (nicht floorOnly), `TOOL_COMMAND`+`TOOL_ORDER`, Kreis-Icon in `ToolsPanel`, i18n `tool.circle`/`tool.circle.hint`. tsc + Suite 331 grün. (Kreise rendern seit `4ac99d3` glatt als `<circle>`.)
|
- [x] ~~**Kreis-Toolbar** (trivial)~~ — **erledigt `e454eab` (2026-07-05):** `ToolId`+`"circle"`, Platzhalter `circleTool` (nicht floorOnly), `TOOL_COMMAND`+`TOOL_ORDER`, Kreis-Icon in `ToolsPanel`, i18n `tool.circle`/`tool.circle.hint`. tsc + Suite 331 grün. (Kreise rendern seit `4ac99d3` glatt als `<circle>`.)
|
||||||
- [x] ~~**Bogen-Werkzeug** (mittel)~~ — **erledigt (2026-07-05):** `arcCommand` in `src/commands/cmds/arc.ts` (3-Klick: Mittelpunkt → Start/Radius → Endwinkel, CCW; Vorschau via `arcPts`/`circlePts`), registriert in `registry.ts` (Alias `a`/`bogen`), `"arc"` als ToolId + Toolbar-Eintrag (Bogen-Icon) + i18n. tsc + Suite 331 grün. **Offen (klein):** Center-/Quadrant-Snaps für Bögen in `src/tools/snapping.ts` noch nicht ergänzt.
|
- [x] ~~**Bogen-Werkzeug** (mittel)~~ — **erledigt (2026-07-05):** `arcCommand` in `src/commands/cmds/arc.ts` (3-Klick: Mittelpunkt → Start/Radius → Endwinkel, CCW; Vorschau via `arcPts`/`circlePts`), registriert in `registry.ts` (Alias `a`/`bogen`), `"arc"` als ToolId + Toolbar-Eintrag (Bogen-Icon) + i18n. tsc + Suite 331 grün. ✅ **Center-/Quadrant-Snaps ergänzt (2026-07-05):** `collectCircles` + Snap-Block in `snapping.ts` — Mittelpunkt + Quadranten (Kreis: alle 4; Bogen: nur im Spannbereich) unter der `center`-Einstellung, Bogen-Endpunkte unter `endpoint`; +3 Tests (Suite 334).
|
||||||
|
|
||||||
- [ ] **BAUTEILE aufs Rhino-Niveau heben (Treppe/Fenster/Tür).** Vergleich Rhino-Plugin ↔ TS + priorisierte Ansätze: **[RESEARCH_BAUTEILE_RHINO.md](RESEARCH_BAUTEILE_RHINO.md)**. Gruppe A (2D, sofort, kernel2d): (1) **Treppe Aussenlinie/Outline** (gerade/L/Wendel), (2) **Fenster Brüstungslinie** im Plan, (3) **Tür Sturzlinien** (gestrichelt, SIA), (4) Treppe Referenz links/mitte/rechts, (5) Fenster Flügel-Mittelpfosten, (6) Tür `wandoeffnung`-Typ. Gruppe B (2D mittel), Gruppe C (3D, nach Mesh-Pipeline/truck). **A-Items 1–3 fallen mit „Schnitt- vs. Ansichts-Darstellung" zusammen** (Sturz = Bauteil unter Schnittebene → gestrichelte Überkopf-Projektion).
|
- [ ] **BAUTEILE aufs Rhino-Niveau heben (Treppe/Fenster/Tür).** Vergleich Rhino-Plugin ↔ TS + priorisierte Ansätze: **[RESEARCH_BAUTEILE_RHINO.md](RESEARCH_BAUTEILE_RHINO.md)**. Gruppe A (2D, sofort, kernel2d): (1) **Treppe Aussenlinie/Outline** (gerade/L/Wendel), (2) **Fenster Brüstungslinie** im Plan, (3) **Tür Sturzlinien** (gestrichelt, SIA), (4) Treppe Referenz links/mitte/rechts, (5) Fenster Flügel-Mittelpfosten, (6) Tür `wandoeffnung`-Typ. Gruppe B (2D mittel), Gruppe C (3D, nach Mesh-Pipeline/truck). **A-Items 1–3 fallen mit „Schnitt- vs. Ansichts-Darstellung" zusammen** (Sturz = Bauteil unter Schnittebene → gestrichelte Überkopf-Projektion).
|
||||||
- [ ] **DWG/DXF-Import via `acadrust` (weiterbauen).** ✅ Spike `763a558`: `acadrust` 0.4 (MPL-2.0, pure Rust) **baut zu wasm32** (Crate `src-tauri/dwgimport`, 839 KB), parst DXF aus Byte-Buffer (`DxfReader::from_reader`+`Cursor`), headless getestet. **Offen:** (1) Entity→DOSSIER-Modell-Mapping (LINE/ARC/… → Wand/Öffnung — die eigentliche Domainarbeit, Wochen), (2) Datei-Upload-Glue im Browser (`<input type=file>`→Uint8Array→`parse_dxf_summary_json`, trivial), (3) DWG-binär (`DwgReader::from_reader` analog, aber R13–R2018-Korrektheit unverifiziert), (4) WASM-Größe (nalgebra Haupttreiber). **Klarstellung:** der TS-DXF/DWG-Import (`parseDxf`/`parseDwg`/`dxfToDrawings`) + Upload-UI (`App.tsx`, `ImportDialog.tsx`) existieren längst und funktionieren — der acadrust-Weg wäre eine Rust-Neuimplementierung des Lesens (nur DWG-**Schreiben** ist eine echte Lücke). ✅ **2887794 (2026-07-05): Kurven-Abdeckungslücke geschlossen** — `parseDxf` deckt jetzt ARC/CIRCLE/ELLIPSE (tesselliert zu Konturen, Winkel Radiant, voller Umlauf geschlossen) zusätzlich zu LINE/LWPOLYLINE/POLYLINE/MESH ab; 5 Tests, volle Suite 307 grün. ✅ **c481373 (2026-07-05): SPLINE + INSERT ergänzt** — `parseDxf` wertet SPLINE als echte B-Spline (De Boor, Grad/Knoten; Fallback fitPoints/Kontrollpolygon) aus und expandiert INSERT-Block-Referenzen (2D-Transform Scale/Rotation/Basispunkt + MINSERT-Array + verschachtelte Blöcke, Tiefe ≤8) zu transformierten Konturen; Kontur-Dispatch in gemeinsamen `collectContours` refaktoriert; +7 Tests, volle Suite 314 grün. **Bekannte Grenzen:** rationale SPLINE-Gewichte ignoriert (dxf-parser liefert sie nicht); Block-interne MESH/3DFACE-Entities werden im 2D-Import nicht expandiert. ✅ **c29f27e (2026-07-05): HATCH ergänzt** — dxf-parser hat KEINEN HATCH-Handler (verwarf HATCH stumm); Lösung via `registerEntityHandler` + eigenem `HatchHandler` (sammelt rohe Gruppencodes) + testbarer `hatchContours`-Auswertung: Randpfade (Polyline-Pfade + Linien-/Bogen-Kanten, Bögen über vorhandene Tessellierung) → geschlossene Konturen mit `Contour.filled`; `contoursToDrawings` macht daraus gefüllte `polyline`-Drawing2D (fillColor-Default, restylebar). +6 Tests, Suite 320 grün. ✅ **05bc5aa (2026-07-05): HATCH-Ellipse/Spline-Kanten** ergänzt (Kantentyp 3/4 tesselliert; B-Spline-Sampling in `sampleBSpline` extrahiert). ✅ **4b93ac9 (2026-07-05): TEXT/MTEXT** — `parseDxf` liefert `DxfImportResult.texts` (`ImportedText`: Position/Höhe-in-Metern/Winkel-Radiant; MTEXT-Formatcodes grob gesäubert); `textsToDrawings` → `{shape:"text"}`-Drawing2D; **Darstellung neu**: `addDrawing2D` emittiert ein schlankes `kind:"drawingText"`-Primitiv, PlanView rendert es rein per SVG (modellverankert, Rotation; GPU-Guard so, dass es in ALLEN Renderer-Modi im SVG bleibt); `toRenderScene` überspringt es; ImportDialog zählt/importiert Texte. +5 Tests, Suite 327 grün. **Bekannte Grenzen HATCH:** Bulges an Polyline-Rändern als Sehne; Insel-Loops = eigene Ringe (keine echten Löcher). **Bekannte Grenzen TEXT:** importierte Texte (pointerEvents:none) noch nicht per Canvas-Klick selektierbar; MTEXT-Feinformatierung flachgeklopft; Block-interne TEXT/MTEXT nicht expandiert. **Text im Tauri visuell abgenommen (Nutzer 2026-07-05)** — auch gedreht korrekt. ✅ **4ac99d3: CIRCLE/ARC als echte glatte Formen** — `Contour.curve` trägt die wahre Kreis-/Bogen-Geometrie (pts bleiben für Kontext/3D); `contoursToDrawings` baut `{shape:"circle"|"arc"}`; neue Primitive `drawingCircle` (SVG `<circle>`) + `drawingArc` (SVG-Bogenpfad), `toRenderScene` tesselliert sie für den nativen Pfad; +4 Tests, Suite 331. Ellipse bleibt tesselliert (kein Ellipsen-Primitiv). **Weiter offen:** Entity→Wand-Semantik (die dicke Domainarbeit); DWG-Schreiben (einzige echte Export-Lücke).
|
- [ ] **DWG/DXF-Import via `acadrust` (weiterbauen).** ✅ Spike `763a558`: `acadrust` 0.4 (MPL-2.0, pure Rust) **baut zu wasm32** (Crate `src-tauri/dwgimport`, 839 KB), parst DXF aus Byte-Buffer (`DxfReader::from_reader`+`Cursor`), headless getestet. **Offen:** (1) Entity→DOSSIER-Modell-Mapping (LINE/ARC/… → Wand/Öffnung — die eigentliche Domainarbeit, Wochen), (2) Datei-Upload-Glue im Browser (`<input type=file>`→Uint8Array→`parse_dxf_summary_json`, trivial), (3) DWG-binär (`DwgReader::from_reader` analog, aber R13–R2018-Korrektheit unverifiziert), (4) WASM-Größe (nalgebra Haupttreiber). **Klarstellung:** der TS-DXF/DWG-Import (`parseDxf`/`parseDwg`/`dxfToDrawings`) + Upload-UI (`App.tsx`, `ImportDialog.tsx`) existieren längst und funktionieren — der acadrust-Weg wäre eine Rust-Neuimplementierung des Lesens (nur DWG-**Schreiben** ist eine echte Lücke). ✅ **2887794 (2026-07-05): Kurven-Abdeckungslücke geschlossen** — `parseDxf` deckt jetzt ARC/CIRCLE/ELLIPSE (tesselliert zu Konturen, Winkel Radiant, voller Umlauf geschlossen) zusätzlich zu LINE/LWPOLYLINE/POLYLINE/MESH ab; 5 Tests, volle Suite 307 grün. ✅ **c481373 (2026-07-05): SPLINE + INSERT ergänzt** — `parseDxf` wertet SPLINE als echte B-Spline (De Boor, Grad/Knoten; Fallback fitPoints/Kontrollpolygon) aus und expandiert INSERT-Block-Referenzen (2D-Transform Scale/Rotation/Basispunkt + MINSERT-Array + verschachtelte Blöcke, Tiefe ≤8) zu transformierten Konturen; Kontur-Dispatch in gemeinsamen `collectContours` refaktoriert; +7 Tests, volle Suite 314 grün. **Bekannte Grenzen:** rationale SPLINE-Gewichte ignoriert (dxf-parser liefert sie nicht); Block-interne MESH/3DFACE-Entities werden im 2D-Import nicht expandiert. ✅ **c29f27e (2026-07-05): HATCH ergänzt** — dxf-parser hat KEINEN HATCH-Handler (verwarf HATCH stumm); Lösung via `registerEntityHandler` + eigenem `HatchHandler` (sammelt rohe Gruppencodes) + testbarer `hatchContours`-Auswertung: Randpfade (Polyline-Pfade + Linien-/Bogen-Kanten, Bögen über vorhandene Tessellierung) → geschlossene Konturen mit `Contour.filled`; `contoursToDrawings` macht daraus gefüllte `polyline`-Drawing2D (fillColor-Default, restylebar). +6 Tests, Suite 320 grün. ✅ **05bc5aa (2026-07-05): HATCH-Ellipse/Spline-Kanten** ergänzt (Kantentyp 3/4 tesselliert; B-Spline-Sampling in `sampleBSpline` extrahiert). ✅ **4b93ac9 (2026-07-05): TEXT/MTEXT** — `parseDxf` liefert `DxfImportResult.texts` (`ImportedText`: Position/Höhe-in-Metern/Winkel-Radiant; MTEXT-Formatcodes grob gesäubert); `textsToDrawings` → `{shape:"text"}`-Drawing2D; **Darstellung neu**: `addDrawing2D` emittiert ein schlankes `kind:"drawingText"`-Primitiv, PlanView rendert es rein per SVG (modellverankert, Rotation; GPU-Guard so, dass es in ALLEN Renderer-Modi im SVG bleibt); `toRenderScene` überspringt es; ImportDialog zählt/importiert Texte. +5 Tests, Suite 327 grün. **Bekannte Grenzen HATCH:** Bulges an Polyline-Rändern als Sehne; Insel-Loops = eigene Ringe (keine echten Löcher). **Bekannte Grenzen TEXT:** importierte Texte (pointerEvents:none) noch nicht per Canvas-Klick selektierbar; MTEXT-Feinformatierung flachgeklopft; Block-interne TEXT/MTEXT nicht expandiert. **Text im Tauri visuell abgenommen (Nutzer 2026-07-05)** — auch gedreht korrekt. ✅ **4ac99d3: CIRCLE/ARC als echte glatte Formen** — `Contour.curve` trägt die wahre Kreis-/Bogen-Geometrie (pts bleiben für Kontext/3D); `contoursToDrawings` baut `{shape:"circle"|"arc"}`; neue Primitive `drawingCircle` (SVG `<circle>`) + `drawingArc` (SVG-Bogenpfad), `toRenderScene` tesselliert sie für den nativen Pfad; +4 Tests, Suite 331. Ellipse bleibt tesselliert (kein Ellipsen-Primitiv). **Weiter offen:** Entity→Wand-Semantik (die dicke Domainarbeit); DWG-Schreiben (einzige echte Export-Lücke).
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { computeSnap, wallLayerBoundarySegments } from "./snapping";
|
import { computeSnap, wallLayerBoundarySegments } from "./snapping";
|
||||||
import { DEFAULT_SNAP } from "./types";
|
import { DEFAULT_SNAP } from "./types";
|
||||||
import type { Project, Vec2, Wall } from "../model/types";
|
import type { Drawing2D, Project, Vec2, Wall } from "../model/types";
|
||||||
|
|
||||||
/** Minimalprojekt mit einer dreischichtigen Wand (0.1 / 0.15 / 0.2 m). */
|
/** Minimalprojekt mit einer dreischichtigen Wand (0.1 / 0.15 / 0.2 m). */
|
||||||
function wallProject(wall: Wall): Project {
|
function wallProject(wall: Wall): Project {
|
||||||
@@ -134,3 +134,95 @@ describe("computeSnap — fängt auf Wand-Schichttrennlinien", () => {
|
|||||||
expect(result!.point.y).toBeCloseTo(0.025, 6);
|
expect(result!.point.y).toBeCloseTo(0.025, 6);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Kreis-/Bogen-Snaps (Center/Quadrant/Bogen-Endpunkt) ──────────────────────
|
||||||
|
|
||||||
|
/** Minimalprojekt mit gegebenen 2D-Geometrien auf dem Geschoss „eg". */
|
||||||
|
function drawProject(geoms: Drawing2D["geom"][]): Project {
|
||||||
|
return {
|
||||||
|
id: "t",
|
||||||
|
name: "T",
|
||||||
|
lineStyles: [],
|
||||||
|
hatches: [],
|
||||||
|
components: [{ id: "c", name: "C", color: "#ccc", hatchId: "none", joinPriority: 10 }],
|
||||||
|
wallTypes: [],
|
||||||
|
drawingLevels: [
|
||||||
|
{
|
||||||
|
id: "eg",
|
||||||
|
name: "EG",
|
||||||
|
kind: "floor",
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
floorHeight: 2.6,
|
||||||
|
cutHeight: 1.0,
|
||||||
|
baseElevation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layers: [{ code: "20", name: "Zeichnung", color: "#0a0a0a", lw: 0.5, visible: true, locked: false }],
|
||||||
|
walls: [],
|
||||||
|
doors: [],
|
||||||
|
openings: [],
|
||||||
|
ceilings: [],
|
||||||
|
stairs: [],
|
||||||
|
rooms: [],
|
||||||
|
drawings2d: geoms.map((geom, i) => ({
|
||||||
|
id: `d${i}`,
|
||||||
|
type: "drawing2d" as const,
|
||||||
|
levelId: "eg",
|
||||||
|
categoryCode: "20",
|
||||||
|
geom,
|
||||||
|
})),
|
||||||
|
context: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapAt(project: Project, raw: Vec2, settings = DEFAULT_SNAP) {
|
||||||
|
return computeSnap({
|
||||||
|
raw,
|
||||||
|
project,
|
||||||
|
levelId: "eg",
|
||||||
|
visibleCodes: new Set(["20"]),
|
||||||
|
settings,
|
||||||
|
draftPoints: [],
|
||||||
|
lastPoint: null,
|
||||||
|
pxPerMeter: 100,
|
||||||
|
shift: false,
|
||||||
|
ctrl: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("computeSnap — Kreis/Bogen", () => {
|
||||||
|
const withCenter = { ...DEFAULT_SNAP, center: true };
|
||||||
|
|
||||||
|
it("fängt Mittelpunkt + Quadranten eines Kreises (center aktiv)", () => {
|
||||||
|
const p = drawProject([{ shape: "circle", center: { x: 0, y: 0 }, r: 1 }]);
|
||||||
|
const c = snapAt(p, { x: 0, y: 0 }, withCenter);
|
||||||
|
expect(c?.kind).toBe("center");
|
||||||
|
expect(c!.point.x).toBeCloseTo(0, 6);
|
||||||
|
expect(c!.point.y).toBeCloseTo(0, 6);
|
||||||
|
const q = snapAt(p, { x: 1, y: 0 }, withCenter);
|
||||||
|
expect(q?.kind).toBe("quadrant");
|
||||||
|
expect(q!.point.x).toBeCloseTo(1, 6);
|
||||||
|
expect(q!.point.y).toBeCloseTo(0, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fängt Bogen-Endpunkte (endpoint per Default aktiv)", () => {
|
||||||
|
// Viertelbogen um (5,0), r=1, 0…90° → Endpunkte (6,0) und (5,1).
|
||||||
|
const p = drawProject([
|
||||||
|
{ shape: "arc", center: { x: 5, y: 0 }, r: 1, a0: 0, a1: Math.PI / 2 },
|
||||||
|
]);
|
||||||
|
const e = snapAt(p, { x: 6, y: 0 });
|
||||||
|
expect(e?.kind).toBe("endpoint");
|
||||||
|
expect(e!.point.x).toBeCloseTo(6, 6);
|
||||||
|
expect(e!.point.y).toBeCloseTo(0, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("bietet Quadranten AUSSERHALB der Bogen-Spanne NICHT an", () => {
|
||||||
|
// Viertelbogen 0…90°: der West-Quadrant (4,0) liegt NICHT im Bogen.
|
||||||
|
const p = drawProject([
|
||||||
|
{ shape: "arc", center: { x: 5, y: 0 }, r: 1, a0: 0, a1: Math.PI / 2 },
|
||||||
|
]);
|
||||||
|
const r = snapAt(p, { x: 4, y: 0 }, withCenter);
|
||||||
|
expect(r?.kind).not.toBe("quadrant");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -108,6 +108,28 @@ function collectSegments(input: SnapInput): Seg[] {
|
|||||||
return segs;
|
return segs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Kreis-/Bogen-Geometrie für Center-/Quadrant-/Endpunkt-Snaps. `arc=null` =
|
||||||
|
* Vollkreis (keine Endpunkte, alle vier Quadranten). */
|
||||||
|
interface CircleGeom {
|
||||||
|
center: Vec2;
|
||||||
|
r: number;
|
||||||
|
arc: { a0: number; a1: number } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sammelt sichtbare Kreise/Bögen des aktiven Geschosses (aus `drawings2d`). */
|
||||||
|
function collectCircles(input: SnapInput): CircleGeom[] {
|
||||||
|
const out: CircleGeom[] = [];
|
||||||
|
for (const d of input.project.drawings2d) {
|
||||||
|
if (d.levelId !== input.levelId) continue;
|
||||||
|
if (!input.visibleCodes.has(d.categoryCode)) continue;
|
||||||
|
const g = d.geom;
|
||||||
|
if (g.shape === "circle") out.push({ center: g.center, r: g.r, arc: null });
|
||||||
|
else if (g.shape === "arc")
|
||||||
|
out.push({ center: g.center, r: g.r, arc: { a0: g.a0, a1: g.a1 } });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/** Lotfußpunkt von p auf die Strecke a-b, auf das Segment geklemmt. */
|
/** Lotfußpunkt von p auf die Strecke a-b, auf das Segment geklemmt. */
|
||||||
function perpFoot(p: Vec2, a: Vec2, b: Vec2): Vec2 {
|
function perpFoot(p: Vec2, a: Vec2, b: Vec2): Vec2 {
|
||||||
const dx = b.x - a.x, dy = b.y - a.y;
|
const dx = b.x - a.x, dy = b.y - a.y;
|
||||||
@@ -184,6 +206,34 @@ export function computeSnap(input: SnapInput): SnapResult | null {
|
|||||||
for (const [a, b] of segs) consider(perpFoot(raw, a, b), "onEdge");
|
for (const [a, b] of segs) consider(perpFoot(raw, a, b), "onEdge");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kreis-/Bogen-Snaps: Mittelpunkt + Quadranten (Ost/Nord/West/Süd); bei Bögen
|
||||||
|
// nur die Quadranten IM Bogen-Bereich, plus die beiden Bogen-Endpunkte. Center/
|
||||||
|
// Quadrant hängen an der `center`-Einstellung, die Endpunkte an `endpoint`.
|
||||||
|
if (settings.center || settings.endpoint) {
|
||||||
|
const TAU = Math.PI * 2;
|
||||||
|
const norm = (x: number) => ((x % TAU) + TAU) % TAU;
|
||||||
|
const onCircle = (c: CircleGeom, ang: number): Vec2 => ({
|
||||||
|
x: c.center.x + c.r * Math.cos(ang),
|
||||||
|
y: c.center.y + c.r * Math.sin(ang),
|
||||||
|
});
|
||||||
|
for (const c of collectCircles(input)) {
|
||||||
|
if (settings.center) {
|
||||||
|
consider(c.center, "center");
|
||||||
|
// Volle CCW-Spanne (0 ⇒ Vollkreis); Quadrant nur, wenn im Bogen liegt.
|
||||||
|
const sweep = c.arc ? norm(c.arc.a1 - c.arc.a0) || TAU : TAU;
|
||||||
|
for (let q = 0; q < 4; q++) {
|
||||||
|
const ang = q * (Math.PI / 2);
|
||||||
|
if (c.arc && norm(ang - c.arc.a0) > sweep + 1e-9) continue;
|
||||||
|
consider(onCircle(c, ang), "quadrant");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (settings.endpoint && c.arc) {
|
||||||
|
consider(onCircle(c, c.arc.a0), "endpoint");
|
||||||
|
consider(onCircle(c, c.arc.a1), "endpoint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (best) return best;
|
if (best) return best;
|
||||||
|
|
||||||
// Ortho / Winkelraster (Projektion relativ zum letzten Punkt).
|
// Ortho / Winkelraster (Projektion relativ zum letzten Punkt).
|
||||||
|
|||||||
Reference in New Issue
Block a user