3d2d4d6321
Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung (konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text- Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback, falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform) fuer fluessige Interaktion ohne React-Re-Render je Frame. Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM (Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext- Import, Tauri-Compute-Boundary-PoC).
114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
// Geometrie-Helfer für Decken (Slabs) — reine Funktionen über `Vec2`, kein
|
|
// externer Kernel. Eine Decke ist ein GESCHLOSSENER Umriss (Polygon) plus eine
|
|
// Dicke; diese Helfer normalisieren den Umriss, berechnen Fläche/Umriss-Prüfung
|
|
// und liefern eine extrude-fertige Form. Baut auf kernel2d/model/geometry auf.
|
|
//
|
|
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
|
|
|
import type { Vec2 } from "../model/types";
|
|
import { signedArea, isCCW, vecEqual } from "./kernel2d";
|
|
|
|
/** Kleinste sinnvolle Deckenfläche (m²) — darunter gilt der Umriss als entartet. */
|
|
export const MIN_CEILING_AREA = 1e-4;
|
|
|
|
/**
|
|
* Normalisiert einen Deckenumriss:
|
|
* • entfernt aufeinanderfolgende (nahezu) gleiche Punkte,
|
|
* • entfernt einen schließenden Duplikat-Endpunkt (pts[last] == pts[0]),
|
|
* • erzwingt CCW-Wicklung (positive Fläche) — konsistent für Plan-Füllung und
|
|
* 3D-Extrusion.
|
|
* Liefert `null`, wenn nach der Bereinigung < 3 Punkte übrig bleiben (kein
|
|
* gültiges Polygon).
|
|
*/
|
|
export function normalizeOutline(pts: Vec2[]): Vec2[] | null {
|
|
const clean: Vec2[] = [];
|
|
for (const p of pts) {
|
|
if (clean.length === 0 || !vecEqual(clean[clean.length - 1], p)) clean.push(p);
|
|
}
|
|
// Schließenden Duplikat-Endpunkt verwerfen.
|
|
if (clean.length > 1 && vecEqual(clean[0], clean[clean.length - 1])) clean.pop();
|
|
if (clean.length < 3) return null;
|
|
// CCW erzwingen.
|
|
return isCCW(clean) ? clean : clean.slice().reverse();
|
|
}
|
|
|
|
/** Ob ein Umriss ein gültiges (nicht entartetes) Deckenpolygon bildet. */
|
|
export function isValidOutline(pts: Vec2[]): boolean {
|
|
const norm = normalizeOutline(pts);
|
|
return norm != null && ceilingArea(norm) >= MIN_CEILING_AREA;
|
|
}
|
|
|
|
/** Fläche eines Deckenumrisses in m² (immer positiv). */
|
|
export function ceilingArea(pts: Vec2[]): number {
|
|
return Math.abs(signedArea(pts));
|
|
}
|
|
|
|
/**
|
|
* Achsparallele Bounding-Box eines Umrisses (Modell-Meter). Leerer/entarteter
|
|
* Umriss → Null-Box.
|
|
*/
|
|
export function outlineBBox(pts: Vec2[]): {
|
|
minX: number;
|
|
minY: number;
|
|
maxX: number;
|
|
maxY: number;
|
|
} {
|
|
let minX = Infinity,
|
|
minY = Infinity,
|
|
maxX = -Infinity,
|
|
maxY = -Infinity;
|
|
for (const p of pts) {
|
|
minX = Math.min(minX, p.x);
|
|
minY = Math.min(minY, p.y);
|
|
maxX = Math.max(maxX, p.x);
|
|
maxY = Math.max(maxY, p.y);
|
|
}
|
|
if (!isFinite(minX)) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
|
return { minX, minY, maxX, maxY };
|
|
}
|
|
|
|
/** Schwerpunkt (Zentroid) des Umriss-Polygons (für HUD/Beschriftung). */
|
|
export function outlineCentroid(pts: Vec2[]): Vec2 {
|
|
const a = signedArea(pts);
|
|
if (Math.abs(a) < 1e-12) {
|
|
// Entartet: einfacher Mittelwert der Punkte.
|
|
let sx = 0,
|
|
sy = 0;
|
|
for (const p of pts) {
|
|
sx += p.x;
|
|
sy += p.y;
|
|
}
|
|
const n = pts.length || 1;
|
|
return { x: sx / n, y: sy / n };
|
|
}
|
|
let cx = 0,
|
|
cy = 0;
|
|
for (let i = 0; i < pts.length; i++) {
|
|
const p = pts[i];
|
|
const q = pts[(i + 1) % pts.length];
|
|
const cr = p.x * q.y - q.x * p.y;
|
|
cx += (p.x + q.x) * cr;
|
|
cy += (p.y + q.y) * cr;
|
|
}
|
|
const f = 1 / (6 * a);
|
|
return { x: cx * f, y: cy * f };
|
|
}
|
|
|
|
/**
|
|
* Punkt-in-Polygon-Test (Ray-Casting) für einen geschlossenen Umriss — dient dem
|
|
* Body-Select einer Decke im Grundriss/3D. Randpunkte gelten als innen genug.
|
|
*/
|
|
export function pointInOutline(p: Vec2, outline: Vec2[]): boolean {
|
|
let inside = false;
|
|
const n = outline.length;
|
|
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
const a = outline[i];
|
|
const b = outline[j];
|
|
const intersect =
|
|
a.y > p.y !== b.y > p.y &&
|
|
p.x < ((b.x - a.x) * (p.y - a.y)) / (b.y - a.y || 1e-12) + a.x;
|
|
if (intersect) inside = !inside;
|
|
}
|
|
return inside;
|
|
}
|