8fd8987b70
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).
202 lines
7.6 KiB
TypeScript
202 lines
7.6 KiB
TypeScript
// 2D-Boolean-Operationen (Union / Subtract / Intersect) für GESCHLOSSENE
|
|
// 2D-Zeichenelemente (Drawing2D). Ergänzt Split/Join (`splitJoin.ts`). Wandelt
|
|
// Drawing2D ↔ polygon-clipping-Format (Martinez-Algorithmus, robust) und liefert
|
|
// neue Drawing2D-Listen. Stil-, Kategorie- und Level-Attribute werden geerbt
|
|
// (vom ersten bzw. Basis-Element).
|
|
//
|
|
// Loch-Handling: Das Drawing2D-Modell kennt KEINE Innenringe (Löcher). Liefert
|
|
// polygon-clipping ein Polygon mit Innenringen, wird der Außenring als gefüllte
|
|
// Form emittiert und JEDER Innenring als SEPARATE, ungefüllte geschlossene
|
|
// Polylinie (Loch-Umriss). So geht keine Geometrie verloren und nichts crasht;
|
|
// der Lochrand bleibt sichtbar, ohne dass eine gefüllte Insel entsteht.
|
|
//
|
|
// Nur geschlossene Elemente (rect / polyline closed) zählen; offene Linien/
|
|
// Polylinien und Kreise/Bögen/Text werden ignoriert. Wände sind kein Drawing2D
|
|
// und damit ohnehin ausgenommen (No-op).
|
|
//
|
|
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
|
|
|
import polygonClipping from "polygon-clipping";
|
|
import type { MultiPolygon, Polygon, Ring } from "polygon-clipping";
|
|
import type { Drawing2D, Drawing2DGeom, Vec2 } from "../model/types";
|
|
import { toPolyForm } from "./splitJoin";
|
|
import type { PolyForm } from "./splitJoin";
|
|
|
|
/** Ist die Geometrie eines Drawing2D eine GESCHLOSSENE Fläche (für Boolean)? */
|
|
export function isClosedArea(geom: Drawing2DGeom): boolean {
|
|
if (geom.shape === "rect") return true;
|
|
if (geom.shape === "polyline") return geom.closed && geom.pts.length >= 3;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Wandelt ein geschlossenes Drawing2D in ein polygon-clipping-`Polygon`
|
|
* (Ring-Liste). Reuse: `toPolyForm` aus splitJoin liefert die Stützpunkte. Der
|
|
* Ring wird als `[x,y]`-Paare gebildet; polygon-clipping schließt selbst
|
|
* (erster==letzter ist nicht erforderlich, aber unschädlich). Liefert `null`,
|
|
* wenn die Form nicht geschlossen ist oder zu wenige Punkte hat.
|
|
*/
|
|
export function drawingToPolygon(geom: Drawing2DGeom): Polygon | null {
|
|
if (!isClosedArea(geom)) return null;
|
|
const form = toPolyForm(geom);
|
|
if (!form) return null;
|
|
const ring = formToRing(form);
|
|
if (ring.length < 3) return null;
|
|
return [ring];
|
|
}
|
|
|
|
/** PolyForm → polygon-clipping-Ring (`[x,y]`-Paare). */
|
|
function formToRing(form: PolyForm): Ring {
|
|
return form.pts.map((p): [number, number] => [p.x, p.y]);
|
|
}
|
|
|
|
/** Ein polygon-clipping-Ring → Vec2-Punktliste (für fromPolyForm/Drawing2D). */
|
|
function ringToPts(ring: Ring): Vec2[] {
|
|
// polygon-clipping liefert geschlossene Ringe (letzter == erster Punkt). Den
|
|
// doppelten Schlusspunkt entfernen, damit unsere Polyline-Form sauber ist.
|
|
const pts = ring.map((p): Vec2 => ({ x: p[0], y: p[1] }));
|
|
if (
|
|
pts.length > 1 &&
|
|
pts[0].x === pts[pts.length - 1].x &&
|
|
pts[0].y === pts[pts.length - 1].y
|
|
) {
|
|
pts.pop();
|
|
}
|
|
return pts;
|
|
}
|
|
|
|
let counter = 0;
|
|
/** Eindeutige Drawing2D-ID (zeitbasiert + Zähler). */
|
|
function newId(prefix: string): string {
|
|
counter += 1;
|
|
return `${prefix}-${Date.now()}-${counter}`;
|
|
}
|
|
|
|
/**
|
|
* Erzeugt aus einem Punktring ein neues geschlossenes Drawing2D, das Stil/
|
|
* Kategorie/Level der Quelle erbt. `filled` steuert, ob Füllung/Schraffur
|
|
* mitwandern (Außenring = ja, Loch-Umriss = nein).
|
|
*/
|
|
function ringDrawing(src: Drawing2D, pts: Vec2[], filled: boolean): Drawing2D {
|
|
const out: Drawing2D = {
|
|
id: newId("bool"),
|
|
type: "drawing2d",
|
|
levelId: src.levelId,
|
|
categoryCode: src.categoryCode,
|
|
geom: { shape: "polyline", pts, closed: true },
|
|
};
|
|
if (src.lineStyleId !== undefined) out.lineStyleId = src.lineStyleId;
|
|
if (src.color !== undefined) out.color = src.color;
|
|
if (src.weightMm !== undefined) out.weightMm = src.weightMm;
|
|
if (filled) {
|
|
if (src.hatchId !== undefined) out.hatchId = src.hatchId;
|
|
if (src.fillColor !== undefined) out.fillColor = src.fillColor;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Wandelt eine polygon-clipping-`MultiPolygon` in Drawing2D[] zurück. Je Polygon
|
|
* wird der Außenring (filled) emittiert; jeder Innenring (Loch) als separater
|
|
* ungefüllter geschlossener Umriss. Quelle liefert Stil/Kategorie/Level.
|
|
*/
|
|
function multiPolygonToDrawings(
|
|
src: Drawing2D,
|
|
mp: MultiPolygon,
|
|
): Drawing2D[] {
|
|
const out: Drawing2D[] = [];
|
|
for (const poly of mp) {
|
|
poly.forEach((ring, i) => {
|
|
const pts = ringToPts(ring);
|
|
if (pts.length < 3) return;
|
|
// Erster Ring = Außenkontur (gefüllt), weitere = Löcher (nur Umriss).
|
|
out.push(ringDrawing(src, pts, i === 0));
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Geschlossene Quellen → polygon-clipping-Polygone (mit Drawing-Zuordnung). */
|
|
function collectPolygons(
|
|
srcs: Drawing2D[],
|
|
): { poly: Polygon; src: Drawing2D }[] {
|
|
const out: { poly: Polygon; src: Drawing2D }[] = [];
|
|
for (const d of srcs) {
|
|
const poly = drawingToPolygon(d.geom);
|
|
if (poly) out.push({ poly, src: d });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Vereinigung (Union) der gegebenen geschlossenen Drawing2D. Liefert die neuen
|
|
* Drawing2D (≥1 — MultiPolygon → mehrere) und die IDs der ERSETZTEN Quellen.
|
|
* Stil/Kategorie/Level werden vom ersten geschlossenen Element geerbt. Weniger
|
|
* als 2 geschlossene Quellen → No-op (`{ created: [], replacedIds: [] }`).
|
|
*/
|
|
export function unionDrawings(srcs: Drawing2D[]): BooleanResult {
|
|
const polys = collectPolygons(srcs);
|
|
if (polys.length < 2) return EMPTY_RESULT;
|
|
const first = polys[0];
|
|
const rest = polys.slice(1).map((p) => p.poly);
|
|
const mp = polygonClipping.union(first.poly, ...rest);
|
|
return {
|
|
created: multiPolygonToDrawings(first.src, mp),
|
|
replacedIds: polys.map((p) => p.src.id),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Differenz (Subtract): das ERSTE geschlossene Element ist die Basis, alle
|
|
* übrigen werden abgezogen. Liefert neue Drawing2D + ersetzte IDs (nur die
|
|
* tatsächlich beteiligten geschlossenen Quellen). Stil/Kategorie/Level von der
|
|
* Basis. Weniger als 2 geschlossene Quellen → No-op.
|
|
*/
|
|
export function differenceDrawings(srcs: Drawing2D[]): BooleanResult {
|
|
const polys = collectPolygons(srcs);
|
|
if (polys.length < 2) return EMPTY_RESULT;
|
|
const base = polys[0];
|
|
const clips = polys.slice(1).map((p) => p.poly);
|
|
const mp = polygonClipping.difference(base.poly, ...clips);
|
|
return {
|
|
created: multiPolygonToDrawings(base.src, mp),
|
|
replacedIds: polys.map((p) => p.src.id),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Schnittmenge (Intersection): nur die gemeinsame Überlappung aller geschlossenen
|
|
* Elemente bleibt. Liefert neue Drawing2D + ersetzte IDs. Stil/Kategorie/Level
|
|
* vom ersten Element. Weniger als 2 geschlossene Quellen → No-op. Ohne
|
|
* Überlappung ist `created` leer (die Quellen werden trotzdem ersetzt = entfernt,
|
|
* weil der leere Schnitt das gewünschte Ergebnis ist).
|
|
*/
|
|
export function intersectionDrawings(srcs: Drawing2D[]): BooleanResult {
|
|
const polys = collectPolygons(srcs);
|
|
if (polys.length < 2) return EMPTY_RESULT;
|
|
const first = polys[0];
|
|
const rest = polys.slice(1).map((p) => p.poly);
|
|
const mp = polygonClipping.intersection(first.poly, ...rest);
|
|
return {
|
|
created: multiPolygonToDrawings(first.src, mp),
|
|
replacedIds: polys.map((p) => p.src.id),
|
|
};
|
|
}
|
|
|
|
/** Ergebnis einer Boolean-Operation: neue Elemente + zu ersetzende Quell-IDs. */
|
|
export interface BooleanResult {
|
|
/** Neu erzeugte Drawing2D (ersetzen die Quellen). */
|
|
created: Drawing2D[];
|
|
/** IDs der geschlossenen Quellen, die durch das Ergebnis ersetzt werden. */
|
|
replacedIds: string[];
|
|
}
|
|
|
|
const EMPTY_RESULT: BooleanResult = { created: [], replacedIds: [] };
|
|
|
|
/** Zählt geschlossene Flächen in einer Auswahl (für Menü-Aktivierung). */
|
|
export function countClosedAreas(srcs: Drawing2D[]): number {
|
|
let n = 0;
|
|
for (const d of srcs) if (isClosedArea(d.geom)) n++;
|
|
return n;
|
|
}
|