Files
DOSSIER-STANDALONE/src/model/geometry.ts
T
karim 400ec890b0 Grundriss-Gehrung: Flächenpaarung orientierungsbasiert statt per Distanz
An spitzen Wandecken wählte die Distanz-Heuristik in miterLine die falsche
B-Fläche und lieferte eine um 90° verdrehte Gehrungslinie — die Poché-Bänder
überlappten kreuzweise. Die Paarung erfolgt jetzt orientierungsbasiert: über
die Auslaufrichtungen der Achsen (dot(n, d)) wird Aussenfläche mit Aussen-,
Innen mit Innenfläche verschnitten, nie über Kreuz — für jeden Winkel. Rechte
und stumpfe Ecken bleiben bit-identisch (Rust-Parität gewahrt). Kein
Miter-Limit; die Gehrungslinie ist geometrisch exakt. Neuer Test joins.test.ts
mit rechtwinkligem und spitzwinkligem Fall (fängt den alten Bug).
2026-07-03 23:53:25 +02:00

114 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
/** Skalarprodukt zweier 2D-Vektoren. */
export const dot = (p: Vec2, q: Vec2): number => p.x * q.x + p.y * q.y;
/** 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)];
}