Files
DOSSIER-STANDALONE/src/commands/parseInput.ts
T
karim ca859c4aa4 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.
2026-06-30 20:52:27 +02:00

167 lines
6.7 KiB
TypeScript

// 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;
}
}