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,270 @@
|
||||
// Hidden-Line-Removal (HLR) über OCCT-WASM — Welle-C-Spike.
|
||||
//
|
||||
// Ziel: aus einfachen Volumenkörpern (Wände, Decke) eine 2D-Vektor-Projektion
|
||||
// (Schnitt/Ansicht) mit ENTFERNTEN verdeckten Kanten erzeugen — die Grundlage,
|
||||
// aus der später die bestehende SVG-Plan-Pipeline Schnitte/Ansichten zeichnet.
|
||||
//
|
||||
// Kernbefund des Spikes (siehe Report): der opencascade.js-Vollbuild 1.1.1
|
||||
// exportiert die klassischen HLR-Klassen (`HLRBRep_Algo`/`HLRBRep_HLRToShape`,
|
||||
// `HLRBRep_PolyAlgo`/`HLRBRep_PolyHLRToShape`) NICHT als konstruierbare Klassen,
|
||||
// sondern nur deren `Handle_…`-Wrapper. Konstruierbar ist aber der High-Level-
|
||||
// Wrapper `HLRAppli_ReflectLines`, der intern GENAU den exakten HLR-Algorithmus
|
||||
// (`HLRBRep_Algo`) fährt und sichtbare/verdeckte Kanten getrennt liefert —
|
||||
// aufgeteilt nach Kanten-Typ (Sharp/OutLine/…). Das ist der hier genutzte Pfad.
|
||||
//
|
||||
// Identifier englisch, Kommentare deutsch (CONVENTIONS.md). Einheiten: METER.
|
||||
|
||||
import {
|
||||
loadOcct,
|
||||
type GpPnt,
|
||||
type OpenCascadeInstance,
|
||||
type TopoDS_Shape,
|
||||
} from "./occt";
|
||||
|
||||
/** 2D-Punkt in der Projektionsebene (Meter). */
|
||||
export interface Pt2 {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/** Achsparalleler Quader: Ursprung (min-Ecke) + Größe (Meter). */
|
||||
export interface BoxSpec {
|
||||
origin: [number, number, number];
|
||||
size: [number, number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
* Orthographische Blickrichtung. `dir` = Blickrichtung (worauf die Kamera
|
||||
* schaut), `up` = Hoch-Achse der Projektionsebene. Voreinstellungen decken die
|
||||
* Ansichtstypen der Roadmap ab (Grundriss = Top, Schnitt/Ansicht = Front/…).
|
||||
*/
|
||||
export interface ViewDir {
|
||||
dir: [number, number, number];
|
||||
up: [number, number, number];
|
||||
}
|
||||
|
||||
/** Häufige Projektionen (rechtshändig, Z = oben). */
|
||||
export const VIEWS = {
|
||||
/** Grundriss: von oben (−Z), Hoch-Achse = +Y. */
|
||||
top: { dir: [0, 0, -1], up: [0, 1, 0] } as ViewDir,
|
||||
/** Ansicht/Schnitt von vorne (−Y), Hoch-Achse = +Z. */
|
||||
front: { dir: [0, -1, 0], up: [0, 0, 1] } as ViewDir,
|
||||
/** Ansicht/Schnitt von der Seite (−X), Hoch-Achse = +Z. */
|
||||
side: { dir: [-1, 0, 0], up: [0, 0, 1] } as ViewDir,
|
||||
} as const;
|
||||
|
||||
/** Eine projizierte Polylinie (2D) mit Sichtbarkeits-Flag. */
|
||||
export interface HlrPolyline {
|
||||
pts: Pt2[];
|
||||
/** `true` = sichtbare Kante, `false` = verdeckte Kante. */
|
||||
visible: boolean;
|
||||
/** Kanten-Herkunft (für spätere Stil-/Linien-Zuordnung). */
|
||||
kind: "sharp" | "outline";
|
||||
}
|
||||
|
||||
/** Ergebnis eines HLR-Laufs. */
|
||||
export interface HlrResult {
|
||||
visible: HlrPolyline[];
|
||||
hidden: HlrPolyline[];
|
||||
/** Reine Rechenzeit des HLR (ohne WASM-Init), Millisekunden. */
|
||||
hlrMs: number;
|
||||
}
|
||||
|
||||
export interface HlrOptions {
|
||||
/** Verdeckte Kanten mitberechnen (Default: true). */
|
||||
includeHidden?: boolean;
|
||||
/** Silhouetten-/OutLine-Kanten mitnehmen (Default: true). */
|
||||
includeOutline?: boolean;
|
||||
}
|
||||
|
||||
// ── Geometrie-Aufbau ─────────────────────────────────────────────────────────
|
||||
|
||||
function makeBox(oc: OpenCascadeInstance, spec: BoxSpec): TopoDS_Shape {
|
||||
const [ox, oy, oz] = spec.origin;
|
||||
const [sx, sy, sz] = spec.size;
|
||||
const p0 = new oc.gp_Pnt_3(ox, oy, oz);
|
||||
const p1 = new oc.gp_Pnt_3(ox + sx, oy + sy, oz + sz);
|
||||
return new oc.BRepPrimAPI_MakeBox_3(p0, p1).Shape();
|
||||
}
|
||||
|
||||
/** Mehrere Boxen zu EINEM Solid verschmelzen (BRepAlgoAPI_Fuse, sequenziell). */
|
||||
function fuseBoxes(oc: OpenCascadeInstance, specs: BoxSpec[]): TopoDS_Shape {
|
||||
if (specs.length === 0) throw new Error("hlr: keine Box-Spezifikation");
|
||||
let acc = makeBox(oc, specs[0]);
|
||||
for (let i = 1; i < specs.length; i++) {
|
||||
const next = makeBox(oc, specs[i]);
|
||||
const op = new oc.BRepAlgoAPI_Fuse_3(acc, next);
|
||||
op.Build?.();
|
||||
acc = op.Shape();
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
// ── Kanten-Extraktion ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Eine (bereits 2D-projizierte) OCCT-Kante als Polylinie ausgeben. Die HLR-
|
||||
* Kanten sind gerade Segmente (Box-Fusion), daher genügen Start-/Endpunkt der
|
||||
* Kurve; für spätere gekrümmte Geometrie hier zusätzlich tessellieren.
|
||||
*/
|
||||
function edgeToPolyline(
|
||||
oc: OpenCascadeInstance,
|
||||
edgeShape: TopoDS_Shape,
|
||||
): Pt2[] {
|
||||
const edge = oc.TopoDS.Edge_1(edgeShape);
|
||||
const adaptor = new oc.BRepAdaptor_Curve_2(edge);
|
||||
const u0 = adaptor.FirstParameter();
|
||||
const u1 = adaptor.LastParameter();
|
||||
const a: GpPnt = adaptor.Value(u0);
|
||||
const b: GpPnt = adaptor.Value(u1);
|
||||
// 2D-Projektion: X = horizontal, Y = vertikal (Z≈0 in der Projektionsebene).
|
||||
return [
|
||||
{ x: a.X(), y: a.Y() },
|
||||
{ x: b.X(), y: b.Y() },
|
||||
];
|
||||
}
|
||||
|
||||
function collectEdges(
|
||||
oc: OpenCascadeInstance,
|
||||
compound: TopoDS_Shape,
|
||||
visible: boolean,
|
||||
kind: "sharp" | "outline",
|
||||
): HlrPolyline[] {
|
||||
const out: HlrPolyline[] = [];
|
||||
const exp = new oc.TopExp_Explorer_2(
|
||||
compound,
|
||||
oc.TopAbs_ShapeEnum.TopAbs_EDGE,
|
||||
oc.TopAbs_ShapeEnum.TopAbs_SHAPE,
|
||||
);
|
||||
for (; exp.More(); exp.Next()) {
|
||||
const pts = edgeToPolyline(oc, exp.Current());
|
||||
if (pts.length >= 2) out.push({ pts, visible, kind });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Öffentliche API ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* HLR direkt auf einem bereits aufgebauten OCCT-Shape ausführen. Liefert
|
||||
* sichtbare (+ optional verdeckte) 2D-Polylinien in der Projektionsebene.
|
||||
*/
|
||||
export function hlrShape(
|
||||
oc: OpenCascadeInstance,
|
||||
shape: TopoDS_Shape,
|
||||
view: ViewDir,
|
||||
opts: HlrOptions = {},
|
||||
): HlrResult {
|
||||
const includeHidden = opts.includeHidden ?? true;
|
||||
const includeOutline = opts.includeOutline ?? true;
|
||||
|
||||
const t0 = performance.now();
|
||||
const rl = new oc.HLRAppli_ReflectLines(shape);
|
||||
const [dx, dy, dz] = view.dir;
|
||||
const [ux, uy, uz] = view.up;
|
||||
// SetAxes(projDir, projPoint(at), upDir). Blickpunkt = Ursprung genügt für
|
||||
// eine orthographische Projektion (Lage in der Ebene ist translationsfrei).
|
||||
rl.SetAxes(dx, dy, dz, 0, 0, 0, ux, uy, uz);
|
||||
rl.Perform();
|
||||
|
||||
const T = oc.HLRBRep_TypeOfResultingEdge;
|
||||
const visible: HlrPolyline[] = [];
|
||||
const hidden: HlrPolyline[] = [];
|
||||
|
||||
// in3d=false → 2D-projizierte Kanten.
|
||||
visible.push(
|
||||
...collectEdges(
|
||||
oc,
|
||||
rl.GetCompoundOf3dEdges(T.HLRBRep_Sharp, true, false),
|
||||
true,
|
||||
"sharp",
|
||||
),
|
||||
);
|
||||
if (includeOutline) {
|
||||
visible.push(
|
||||
...collectEdges(
|
||||
oc,
|
||||
rl.GetCompoundOf3dEdges(T.HLRBRep_OutLine, true, false),
|
||||
true,
|
||||
"outline",
|
||||
),
|
||||
);
|
||||
}
|
||||
if (includeHidden) {
|
||||
hidden.push(
|
||||
...collectEdges(
|
||||
oc,
|
||||
rl.GetCompoundOf3dEdges(T.HLRBRep_Sharp, false, false),
|
||||
false,
|
||||
"sharp",
|
||||
),
|
||||
);
|
||||
if (includeOutline) {
|
||||
hidden.push(
|
||||
...collectEdges(
|
||||
oc,
|
||||
rl.GetCompoundOf3dEdges(T.HLRBRep_OutLine, false, false),
|
||||
false,
|
||||
"outline",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const hlrMs = performance.now() - t0;
|
||||
return { visible, hidden, hlrMs };
|
||||
}
|
||||
|
||||
/**
|
||||
* Komfort-Einstieg: aus Box-Spezifikationen (Wände/Decke) einen Solid fusen und
|
||||
* HLR für die gegebene Blickrichtung laufen lassen. Lädt OCCT lazy.
|
||||
*/
|
||||
export async function hlrFromBoxes(
|
||||
boxes: BoxSpec[],
|
||||
view: ViewDir,
|
||||
opts: HlrOptions = {},
|
||||
): Promise<HlrResult> {
|
||||
const oc = await loadOcct();
|
||||
const shape = fuseBoxes(oc, boxes);
|
||||
return hlrShape(oc, shape, view, opts);
|
||||
}
|
||||
|
||||
// ── SVG-Ausgabe (Diagnose/Proof) ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Minimaler SVG-Dump zur visuellen Kontrolle: sichtbare Kanten durchgezogen,
|
||||
* verdeckte gestrichelt. Kein Bestandteil der Plan-Pipeline — nur Proof/Debug.
|
||||
*/
|
||||
export function hlrToSvg(result: HlrResult, padding = 0.5): string {
|
||||
const all = [...result.visible, ...result.hidden];
|
||||
const xs: number[] = [];
|
||||
const ys: number[] = [];
|
||||
for (const pl of all)
|
||||
for (const p of pl.pts) {
|
||||
xs.push(p.x);
|
||||
ys.push(p.y);
|
||||
}
|
||||
const minX = Math.min(...xs) - padding;
|
||||
const maxX = Math.max(...xs) + padding;
|
||||
const minY = Math.min(...ys) - padding;
|
||||
const maxY = Math.max(...ys) + padding;
|
||||
const w = maxX - minX;
|
||||
const h = maxY - minY;
|
||||
|
||||
// SVG-Y zeigt nach unten → Projektions-Y spiegeln.
|
||||
const line = (pl: HlrPolyline): string => {
|
||||
const d = pl.pts
|
||||
.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${maxY - (p.y - minY)}`)
|
||||
.join(" ");
|
||||
const stroke = pl.visible ? "#111" : "#999";
|
||||
const dash = pl.visible ? "" : ' stroke-dasharray="0.1 0.1"';
|
||||
const sw = pl.kind === "outline" ? 0.04 : 0.02;
|
||||
return `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${sw}"${dash}/>`;
|
||||
};
|
||||
|
||||
const paths = all.map(line).join("\n ");
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${minX} ${minY} ${w} ${h}" width="800">
|
||||
<rect x="${minX}" y="${minY}" width="${w}" height="${h}" fill="#fff"/>
|
||||
${paths}
|
||||
</svg>`;
|
||||
}
|
||||
Reference in New Issue
Block a user