Browser-BIM (cad): semantisches Modell, abgeleitete 2D/3D-Sichten, Zeichenwerkzeuge
Standalone-Browser-Port von DOSSIER. Enthaelt das semantische Modell mit Plan-/3D-Ableitung, Zeichen- und Editierwerkzeuge, Rhino-artiges Befehlssystem, dockbares Panel-System, Resource-Manager, DXF/.lin/.pat-Import, i18n (de/en) sowie Projektdokumentation und Probe-Harness.
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// Koordinaten- & Numerik-Parser (docs/design/rhino-command-system.md §2.7).
|
||||
//
|
||||
// Parst rohen Command-Line-Text TOLERANT (Komma/Space/Semikolon als Trenner) zu
|
||||
// einer Discriminated Union; die Engine löst die punktbezogenen Varianten mit
|
||||
// `lastPoint` + Cursor-Richtung zu einem konkreten Modellpunkt auf.
|
||||
//
|
||||
// Unterstützte Formen:
|
||||
// `5,3` → absolut X,Y
|
||||
// `5,3,2` → absolut X,Y (Z ignoriert, 2D)
|
||||
// `r5,3` → relativ zum letzten Punkt
|
||||
// `5<45` → polar: Distanz 5 unter 45° vom letzten Punkt
|
||||
// `<45` → nur Winkel-Constraint; Distanz folgt Maus/Zahl
|
||||
// `5` → nackte Zahl = Distanz-Lock entlang Cursor-/Constraint-Richtung
|
||||
// sonst → Roh-Text (Befehlsname / Optionsbuchstabe) — von der Engine geroutet
|
||||
//
|
||||
// Reine Funktionen, kein Zustand. Winkel in Grad, gegen die +X-Achse, CCW positiv
|
||||
// (passend zur Y-flip-freien Modellkonvention: +Y ist „oben").
|
||||
|
||||
import type { Vec2 } from "../model/types";
|
||||
|
||||
/** Geparste, noch nicht aufgelöste Eingabe. */
|
||||
export type ParsedInput =
|
||||
| { kind: "absolute"; point: Vec2 } // 5,3
|
||||
| { kind: "relative"; delta: Vec2 } // r5,3
|
||||
| { kind: "polar"; dist: number; angleDeg: number } // 5<45
|
||||
| { kind: "angle"; angleDeg: number } // <45
|
||||
| { kind: "distance"; value: number } // 5 (nackte Zahl)
|
||||
| { kind: "text"; text: string }; // Befehlsname / Option / unparsbar
|
||||
|
||||
const DEG = Math.PI / 180;
|
||||
|
||||
/** Tolerant: Zahl aus einem Token (erlaubt führendes +, Dezimalpunkt/-komma). */
|
||||
function parseNumber(raw: string): number | null {
|
||||
const s = raw.trim().replace(",", "."); // Hilfsfall: einzelnes Dezimalkomma
|
||||
if (s === "" || s === "+" || s === "-") return null;
|
||||
const n = Number(s);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zerlegt eine Koordinatenliste (X,Y[,Z]) tolerant in Zahlen. Trenner: Komma,
|
||||
* Semikolon oder Whitespace. Liefert null, wenn nicht ≥2 valide Zahlen vorliegen.
|
||||
* (Wir nutzen die echten Trenner, daher kollidiert das NICHT mit dem
|
||||
* Dezimalkomma-Hilfsfall der nackten Zahl.)
|
||||
*/
|
||||
function parseCoordList(raw: string): number[] | null {
|
||||
const parts = raw
|
||||
.trim()
|
||||
.split(/[\s,;]+/)
|
||||
.filter((p) => p.length > 0);
|
||||
if (parts.length < 2) return null;
|
||||
const nums: number[] = [];
|
||||
for (const p of parts) {
|
||||
const n = Number(p.trim());
|
||||
if (!Number.isFinite(n)) return null;
|
||||
nums.push(n);
|
||||
}
|
||||
return nums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst rohen Command-Line-Text zu einer {@link ParsedInput}. Befehlsnamen/
|
||||
* Optionsbuchstaben fallen als `{kind:"text"}` durch (die Engine routet sie).
|
||||
*/
|
||||
export function parseInput(raw: string): ParsedInput {
|
||||
const s = raw.trim();
|
||||
if (s === "") return { kind: "text", text: "" };
|
||||
|
||||
// ── polar / Winkel: enthält '<' ───────────────────────────────────────────
|
||||
if (s.includes("<")) {
|
||||
const i = s.indexOf("<");
|
||||
const left = s.slice(0, i).trim();
|
||||
const right = s.slice(i + 1).trim();
|
||||
const angle = parseNumber(right);
|
||||
if (angle == null) return { kind: "text", text: s };
|
||||
if (left === "") return { kind: "angle", angleDeg: angle };
|
||||
const dist = parseNumber(left);
|
||||
if (dist == null) return { kind: "text", text: s };
|
||||
return { kind: "polar", dist, angleDeg: angle };
|
||||
}
|
||||
|
||||
// ── relativ: führendes r / @ ──────────────────────────────────────────────
|
||||
if (/^[r@]/i.test(s)) {
|
||||
const rest = s.slice(1);
|
||||
const nums = parseCoordList(rest);
|
||||
if (nums && nums.length >= 2) {
|
||||
return { kind: "relative", delta: { x: nums[0], y: nums[1] } };
|
||||
}
|
||||
// `r5` → relative Distanz entlang Cursor-Richtung.
|
||||
const one = parseNumber(rest);
|
||||
if (one != null) return { kind: "distance", value: one };
|
||||
return { kind: "text", text: s };
|
||||
}
|
||||
|
||||
// ── absolute Koordinate: ≥2 zahlen mit echtem Trenner ─────────────────────
|
||||
const coords = parseCoordList(s);
|
||||
if (coords && coords.length >= 2) {
|
||||
return { kind: "absolute", point: { x: coords[0], y: coords[1] } };
|
||||
}
|
||||
|
||||
// ── nackte Zahl = Distanz-Lock ────────────────────────────────────────────
|
||||
const one = parseNumber(s);
|
||||
if (one != null) return { kind: "distance", value: one };
|
||||
|
||||
// ── sonst: Text (Befehl/Option) ───────────────────────────────────────────
|
||||
return { kind: "text", text: s };
|
||||
}
|
||||
|
||||
/** Punkt aus polarer Form (Distanz/Winkel) relativ zu einem Basispunkt. */
|
||||
export function polarPoint(base: Vec2, dist: number, angleDeg: number): Vec2 {
|
||||
const a = angleDeg * DEG;
|
||||
return { x: base.x + Math.cos(a) * dist, y: base.y + Math.sin(a) * dist };
|
||||
}
|
||||
|
||||
/**
|
||||
* Löst eine {@link ParsedInput} zu einem konkreten Modellpunkt auf — falls sie
|
||||
* sich als Punkt auflösen lässt. `lastPoint` ist der letzte gesetzte Punkt im
|
||||
* Befehl (für relativ/polar/Distanz), `cursorDir` die aktuelle (gesnappte)
|
||||
* Cursor-Richtung als Roh-Cursorpunkt (für nackte Zahl / reinen Winkel-Lock).
|
||||
*
|
||||
* Liefert null, wenn die Eingabe keinen Punkt ergibt (z. B. reiner Text oder
|
||||
* Distanz ohne Bezug).
|
||||
*/
|
||||
export function resolvePoint(
|
||||
parsed: ParsedInput,
|
||||
lastPoint: Vec2 | null,
|
||||
cursor: Vec2 | null,
|
||||
): Vec2 | null {
|
||||
switch (parsed.kind) {
|
||||
case "absolute":
|
||||
return parsed.point;
|
||||
case "relative":
|
||||
if (!lastPoint) return parsed.delta; // ohne Bezug = vom Ursprung
|
||||
return { x: lastPoint.x + parsed.delta.x, y: lastPoint.y + parsed.delta.y };
|
||||
case "polar": {
|
||||
const base = lastPoint ?? { x: 0, y: 0 };
|
||||
return polarPoint(base, parsed.dist, parsed.angleDeg);
|
||||
}
|
||||
case "angle": {
|
||||
// Reiner Winkel-Lock: ohne Distanz braucht es den Cursor, um die Länge
|
||||
// entlang der gelockten Richtung zu projizieren.
|
||||
if (!lastPoint || !cursor) return null;
|
||||
const a = parsed.angleDeg * DEG;
|
||||
const dir = { x: Math.cos(a), y: Math.sin(a) };
|
||||
const dx = cursor.x - lastPoint.x;
|
||||
const dy = cursor.y - lastPoint.y;
|
||||
const proj = dx * dir.x + dy * dir.y; // skalare Projektion
|
||||
const len = Math.max(0, proj);
|
||||
return { x: lastPoint.x + dir.x * len, y: lastPoint.y + dir.y * len };
|
||||
}
|
||||
case "distance": {
|
||||
// Distanz-Lock: Richtung per Cursor, Länge per Zahl.
|
||||
if (!lastPoint || !cursor) return null;
|
||||
const dx = cursor.x - lastPoint.x;
|
||||
const dy = cursor.y - lastPoint.y;
|
||||
const l = Math.hypot(dx, dy);
|
||||
if (l < 1e-9) return { x: lastPoint.x + parsed.value, y: lastPoint.y };
|
||||
return {
|
||||
x: lastPoint.x + (dx / l) * parsed.value,
|
||||
y: lastPoint.y + (dy / l) * parsed.value,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user