2D-Plan-Renderer auf WebGL2 (GPU) + akkumulierter Funktionsstand
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).
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user