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).
271 lines
8.9 KiB
TypeScript
271 lines
8.9 KiB
TypeScript
// 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>`;
|
||
}
|