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:
2026-06-30 20:52:27 +02:00
commit 359ce35737
138 changed files with 31585 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
// Geometrie-Helfer für das Gebäudemodell. Reine 2D/3D-Mathematik,
// kein externer Kernel nötig (Phase 0).
import type { Vec2 } from "./types";
export const sub = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x - b.x, y: a.y - b.y });
export const add = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x + b.x, y: a.y + b.y });
export const scale = (a: Vec2, s: number): Vec2 => ({ x: a.x * s, y: a.y * s });
export const len = (a: Vec2): number => Math.hypot(a.x, a.y);
export const normalize = (a: Vec2): Vec2 => {
const l = len(a) || 1;
return { x: a.x / l, y: a.y / l };
};
/** Linke Normale (90° gegen den Uhrzeigersinn gedreht). */
export const leftNormal = (a: Vec2): Vec2 => ({ x: -a.y, y: a.x });
/** Kreuzprodukt (Z-Komponente) zweier 2D-Vektoren. */
export const cross = (p: Vec2, q: Vec2): number => p.x * q.y - p.y * q.x;
/** Eine unendliche Gerade als Stützpunkt + Richtung. */
export interface Line {
point: Vec2;
dir: Vec2;
}
/**
* Schnittpunkt der Geraden (a + t·da) mit (b + s·db).
* Liefert null bei (nahezu) parallelen Richtungen.
*/
export function lineIntersect(
a: Vec2,
da: Vec2,
b: Vec2,
db: Vec2,
): Vec2 | null {
const denom = cross(da, db);
if (Math.abs(denom) < 1e-9) return null; // parallel → kein Schnitt
const t = cross(sub(b, a), db) / denom;
return add(a, scale(da, t));
}
/** Punkt entlang einer Strecke p1→p2 im Abstand d vom Start. */
export const along = (p1: Vec2, p2: Vec2, d: number): Vec2 =>
add(p1, scale(normalize(sub(p2, p1)), d));
/**
* Die vier Eckpunkte eines Bandes, das quer zur Normalen zwischen den
* Offsets offA und offB liegt (gemessen entlang der linken Normalen n).
* Reihenfolge: start+n*offA, end+n*offA, end+n*offB, start+n*offB.
*/
export function wallBand(
start: Vec2,
end: Vec2,
offA: number,
offB: number,
): [Vec2, Vec2, Vec2, Vec2] {
const u = normalize(sub(end, start));
const n = leftNormal(u);
return [
add(start, scale(n, offA)),
add(end, scale(n, offA)),
add(end, scale(n, offB)),
add(start, scale(n, offB)),
];
}
/** Die vier Eckpunkte einer Wand als Polygon (gegen den Uhrzeigersinn). */
export function wallCorners(
start: Vec2,
end: Vec2,
thickness: number,
): [Vec2, Vec2, Vec2, Vec2] {
const h = thickness / 2;
return wallBand(start, end, h, -h);
}
/**
* Wie {@link wallBand}, aber die Bandenden werden bei Bedarf an Geh­rungs­linien
* (startCut / endCut) abgeschnitten statt rechtwinklig gekappt. Jede der beiden
* Bandkanten (bei Offset offA bzw. offB, Richtung u) wird mit der jeweiligen
* Schnittlinie verschnitten; fehlt eine Schnittlinie, bleibt das Ende
* rechtwinklig (Punkt auf der Achssenkrechten).
* Reihenfolge bleibt konsistent zu wallBand: [A.start, A.end, B.end, B.start].
*/
export function clippedBand(
start: Vec2,
end: Vec2,
offA: number,
offB: number,
startCut: Line | null,
endCut: Line | null,
): [Vec2, Vec2, Vec2, Vec2] {
const u = normalize(sub(end, start));
const n = leftNormal(u);
const edgeStart = (off: number): Vec2 => {
const base = add(start, scale(n, off));
if (!startCut) return base;
return lineIntersect(base, u, startCut.point, startCut.dir) ?? base;
};
const edgeEnd = (off: number): Vec2 => {
const base = add(end, scale(n, off));
if (!endCut) return base;
return lineIntersect(base, u, endCut.point, endCut.dir) ?? base;
};
return [edgeStart(offA), edgeEnd(offA), edgeEnd(offB), edgeStart(offB)];
}