render3d: Wandverschneidung an Ecken + sichtbare Schichten

mesh.rs berechnet die Ecken-Verschneidung jetzt selbst (portiert aus dem
Web-Kern computeJoins/clippedBand): Wandenden werden per Rundungs-Key
gruppiert, ueber Hoehen-Ueberlappung geclustert (gestapelte Geschosse mit
gleichem Grundriss verschneiden sich NICHT), und Zweier-Cluster ueber die
gemeinsame Gehrungslinie geschnitten. T-/X-Stoesse und freie Enden bleiben
stumpf, wie im Web-Kern. Endkappen-Normalen geometrisch aus dem Kreuzprodukt.

WallInput bekommt optional layers (WallLayer{thickness,color}, serde-default,
rueckwaertskompatibel): jede Schicht wird als eigenes Band ueber dieselbe
gehrte Grundflaeche extrudiert und materialgefaerbt. toWalls3d.ts loest die
WallType-Schichten je Wand auf (hexToRgb) und reicht sie durch.

39/39 Rust-Tests (7 neue: Gehrung, T-Stoss, freies Ende, Schichten, Kombi).
This commit is contained in:
2026-07-03 21:45:01 +02:00
parent 5fd0161bc7
commit 1f6da09b2b
7 changed files with 681 additions and 73 deletions
+356 -53
View File
@@ -8,8 +8,32 @@
// Diese 2D-Band-Ecken werden nach world abgebildet (XZ-Ebene) und entlang +Y von
// `base_elevation` bis `base_elevation + height` extrudiert — analog zur
// three.js-`ExtrudeGeometry`, die den 2D-`clippedBand` hochzieht (Viewport3D.tsx
// `addLayerPrism`). M1 nimmt die stumpfe (rechtwinklige) Band-Form; Gehrungen
// (`computeJoins`/`clippedBand`) sind ein spaeterer Milestone.
// `addLayerPrism`).
//
// ECKEN-VERSCHNEIDUNG (Gehrung): `compute_wall_miters` berechnet — analog zum
// Web-Pendant `src/model/joins.ts::computeJoins` + `geometry.ts::clippedBand` —
// fuer jede Wand die Gehrungslinien an ihren ECHTEN Achsenenden, rein aus der
// Wandkonnektivitaet (Endpunkte, die sich bei genau ZWEI, nicht-parallelen
// Waenden treffen). Anders als der Web-Pfad braucht das HIER keine eigenen
// Eingabefelder: die Berechnung laeuft direkt auf `WallInput` (Start/Ende/
// Dicke), robust auch gegenueber der Teilquader-Zerlegung an Oeffnungen (siehe
// unten) — zwei Teilstuecke DERSELBEN Wand haben stets dieselbe (parallele)
// Achsrichtung, ihre "Gehrung" waere kollinear und wird automatisch verworfen
// (siehe `miter_line`). Mehrgeschossige Modelle mit deckungsgleichem Grundriss
// werden durch einen Hoehen-Ueberlappungstest (`cluster_by_height_overlap`)
// sauber getrennt (kein Verschneiden ueber Geschosse hinweg). T-/X-Stoesse
// (mehr als zwei Enden an einem Knoten) bleiben — wie im Web-Pendant —
// rechtwinklig. Das Schnitt-Modul (`section.rs`) bleibt UNVERAENDERT bei der
// achsparallelen Bounding-Box-Naeherung (siehe dessen Moduldoc).
//
// SCHICHTEN (`WallInput::layers`): mehrschichtige Waende (Putz/Daemmung/
// Mauerwerk/...) werden nicht als ein Vollkoerper, sondern als je EIN Prisma
// pro Schicht extrudiert, quer zur Gesamtdicke gestapelt (gleiche Idee wie
// Viewport3D.tsx `addWallMeshes`: `off` laeuft ueber die Schichtdicken).
// Jede Schicht nutzt DASSELBE gehrte Fussabdruck-Band wie Gap 1 (nur mit ihrem
// eigenen Dicken-Versatz), sodass auch mehrschichtige Waende an Ecken saeuberlich
// stossen. Fehlt `layers` (None/leer), wird ein synthetisches Einzel-Layer aus
// `thickness`/`color` gebildet — bitgenau dasselbe Ergebnis wie zuvor.
//
// OEFFNUNGEN (Tueren/Fenster, `WallInput::openings`): eine Wand mit Oeffnungen
// wird NICHT als ein Vollkoerper-Quader extrudiert, sondern in mehrere
@@ -19,14 +43,19 @@
// fuer die Schnitt-Cut-Polygone (gemeinsame Basis: `crate::openings::
// split_range_by_voids`), nur direkt in Wandachsen-Metern statt in der
// Projektions-u-Achse einer Schnittebene — damit zeigt die 3D-Vollansicht
// (`?engine=wasm`) dieselbe Oeffnung wie der Schnitt.
// (`?engine=wasm`) dieselbe Oeffnung wie der Schnitt. Die Gehrung (siehe oben)
// greift dabei NUR an Teilstuecken, die tatsaechlich an ein echtes Wandende
// grenzen (Achsposition 0 bzw. Wandlaenge) — innere Teilstuecke (Laibungen an
// Oeffnungen) bleiben immer rechtwinklig.
//
// KOORDINATEN: model [x, y] -> world (x, elevation, y). Also Grundriss in der
// XZ-Ebene, Extrusion entlang +Y (Y-up), exakt wie Viewport3D.tsx:
// "Modell (x,y,z) -> Three (x, z, y) (Z = Hoehe nach oben)".
use std::collections::HashMap;
use crate::openings::{split_range_by_voids, MIN_SPAN};
use crate::types::{Mesh, Opening, Point2, Rgb, SlabInput, WallInput, FLOATS_PER_VERTEX};
use crate::types::{Mesh, Opening, Point2, Rgb, SlabInput, WallInput, WallLayer, FLOATS_PER_VERTEX};
/// Ein Quader-Mesh besteht aus 6 Seiten (Boden, Deckel, 4 Waende) zu je 2
/// Dreiecken = 12 Dreiecke, mit flachen Normalen also 24 Vertices (je Seite 4,
@@ -46,13 +75,24 @@ fn left_normal(start: Point2, end: Point2) -> Point2 {
[-dy / len, dx / len]
}
/// Extrudiert EINE Wand zu einem oder mehreren Quader-Meshes und haengt sie an
/// `mesh` an. Ohne Oeffnungen (Regressionsfall) entsteht wie zuvor GENAU ein
/// Quader (`VERTS_PER_BOX`/`INDICES_PER_BOX`); mit Oeffnungen wird die Wand
/// zuerst in Achsen-Teilstuecke (Pfeiler/Bruestung/Sturz) zerlegt und jedes
/// Teilstueck einzeln als Quader extrudiert (siehe `wall_solid_segments` und
/// die Oeffnungen-Erklaerung im Moduldoc oben).
/// Extrudiert EINE Wand (ohne Kenntnis von Nachbarn, also stets rechtwinklige
/// Enden) zu einem oder mehreren Quader-Meshes. Fuer Szenen mit mehreren
/// Waenden bitte `build_walls_mesh`/`build_model_mesh` nutzen — die berechnen
/// zuerst die Eck-Gehrung ueber alle Waende (siehe `compute_wall_miters`) und
/// wenden sie hier mit an. Diese Funktion bleibt der einfache Einzel-Wand-
/// Einstieg (u. a. fuer bestehende Tests) und entspricht exakt dem Aufruf mit
/// leeren Gehrungen.
pub fn extrude_wall(mesh: &mut Mesh, wall: &WallInput) {
extrude_wall_with_miters(mesh, wall, WallMiters::default());
}
/// Wie `extrude_wall`, aber mit den (ggf. leeren) Gehrungslinien an den
/// echten Wandenden. Ohne Oeffnungen und ohne `layers` entsteht wie zuvor
/// GENAU ein Quader je Wand (`VERTS_PER_BOX`/`INDICES_PER_BOX`); mit
/// Oeffnungen wird die Wand zuerst in Achsen-Teilstuecke (Pfeiler/Bruestung/
/// Sturz) zerlegt, mit `layers` zusaetzlich in Schicht-Baender quer zur
/// Dicke — siehe Moduldoc.
fn extrude_wall_with_miters(mesh: &mut Mesh, wall: &WallInput, miters: WallMiters) {
let n = left_normal(wall.start, wall.end);
// Degenerierte Wand (Start == Ende) ueberspringen.
if n == [0.0, 0.0] {
@@ -64,8 +104,42 @@ pub fn extrude_wall(mesh: &mut Mesh, wall: &WallInput) {
let z0 = wall.base_elevation;
let z1 = wall.base_elevation + wall.height;
for (a0, a1, y0, y1) in wall_solid_segments(length, &wall.openings, z0, z1) {
extrude_wall_segment(mesh, wall, n, length, a0, a1, y0, y1);
let segments = wall_solid_segments(length, &wall.openings, z0, z1);
// Schicht-Stapel: ohne `layers` (oder leer) EIN synthetisches Layer aus
// der Gesamtdicke/-Farbe — bitgenau das bisherige Vollkoerper-Verhalten.
let synthetic;
let layers: &[WallLayer] = match &wall.layers {
Some(ls) if !ls.is_empty() => ls.as_slice(),
_ => {
synthetic = [WallLayer {
thickness: wall.thickness,
color: wall.color,
}];
&synthetic
}
};
// Schichten von der +n-Seite (offA = +Gesamtdicke/2) zur -n-Seite stapeln,
// damit offA stets > offB gilt — exakt die Konvention, unter der die
// bisherige Ecken-/Flankennormalen-Herleitung (siehe `extrude_layer_segment`)
// gilt. Fuer das synthetische Einzel-Layer ergibt sich offA=+h, offB=-h wie
// zuvor.
let total: f32 = layers.iter().map(|l| l.thickness).sum();
let mut off_a = total * 0.5;
for layer in layers {
let off_b = off_a - layer.thickness;
for &(a0, a1, y0, y1) in &segments {
// Gehrung nur an Teilstuecken, die tatsaechlich an ein echtes
// Wandende grenzen (innere Laibungs-Teilstuecke bleiben eckig).
let start_cut = if a0 <= 1e-6 { miters.start } else { None };
let end_cut = if a1 >= length - 1e-6 { miters.end } else { None };
extrude_layer_segment(
mesh, wall, n, length, a0, a1, y0, y1, off_a, off_b, start_cut, end_cut,
layer.color,
);
}
off_a = off_b;
}
}
@@ -109,16 +183,28 @@ fn axis_point(wall: &WallInput, length: f32, a: f32) -> Point2 {
]
}
/// Extrudiert EIN Achsen-Teilstueck `[a0,a1]` (Hoehe `[y0,y1]`) einer Wand zu
/// Extrudiert EIN Schicht-Achsen-Teilstueck (Achse `[a0,a1]`, Hoehe `[y0,y1]`,
/// Dicken-Versatz `[off_a,off_b]` gegen die Achse entlang `n`) einer Wand zu
/// einem Quader-Mesh und haengt ihn an `mesh` an. Fuer `a0=0, a1=length,
/// y0=base_elevation, y1=base_elevation+height` (keine Oeffnungen) ist das
/// exakt die bisherige Vollkoerper-Extrusion.
/// y0=base_elevation, y1=base_elevation+height, off_a=+thickness/2,
/// off_b=-thickness/2` und ohne Gehrung (`start_cut`/`end_cut` beide `None`)
/// ist das exakt die bisherige Vollkoerper-Extrusion (Regressionsfall).
///
/// GEHRUNG: ist `start_cut`/`end_cut` gesetzt, werden die jeweiligen Band-Ecken
/// NICHT rechtwinklig bei `a0`/`a1` gekappt, sondern mit der Gehrungslinie
/// verschnitten (analog `clippedBand` im Web) — siehe `compute_wall_miters`.
/// Die beiden End-Kappenflaechen bleiben dabei planar (siehe Beweis im
/// Moduldoc von `compute_wall_miters`), ihre Normale wird deshalb geometrisch
/// aus den tatsaechlichen Kantenvektoren hergeleitet (`quad_normal`) statt aus
/// der festen Achsrichtung — das liefert im rechtwinkligen Fall exakt dieselbe
/// Normale wie zuvor UND bleibt bei einer schraegen Gehrung korrekt.
///
/// Aufbau: die vier Grundriss-Ecken des Bandes (CCW) bilden das Deckel-Rechteck.
/// Boden = gleiche XZ auf `y0`, Deckel = auf `y1`. Jede der sechs Seiten bekommt
/// eigene Vertices mit ihrer (nach aussen zeigenden) Flaechen-Normale — daher
/// flaches Shading und korrektes Backface-Culling.
fn extrude_wall_segment(
#[allow(clippy::too_many_arguments)]
fn extrude_layer_segment(
mesh: &mut Mesh,
wall: &WallInput,
n: Point2,
@@ -127,21 +213,35 @@ fn extrude_wall_segment(
a1: f32,
y0: f32,
y1: f32,
off_a: f32,
off_b: f32,
start_cut: Option<MiterLine>,
end_cut: Option<MiterLine>,
color: Rgb,
) {
let h = wall.thickness * 0.5;
// Vier Band-Ecken im Grundriss (CCW, wie `wallCorners(start,end,thickness)`),
// aber auf Basis der Achsen-Punkte des Teilstuecks statt Wandanfang/-ende:
// p0 = p(a0) + n*h, p1 = p(a1) + n*h, p2 = p(a1) - n*h, p3 = p(a0) - n*h.
let off = |p: Point2, s: f32| -> Point2 { [p[0] + n[0] * s * h, p[1] + n[1] * s * h] };
let p0 = axis_point(wall, length, a0);
let p1 = axis_point(wall, length, a1);
let g0 = off(p0, 1.0);
let g1 = off(p1, 1.0);
let g2 = off(p1, -1.0);
let g3 = off(p0, -1.0);
let color = wall.color;
// Achsenrichtung der GESAMTEN Wand (fuer Gehrungs-Schnitt und Kappen) —
// konstant ueber alle Teilstuecke, unabhaengig von `a0`/`a1`.
let ux = wall.end[0] - wall.start[0];
let uz = wall.end[1] - wall.start[1];
let ul = (ux * ux + uz * uz).sqrt().max(1e-9);
let u: Point2 = [ux / ul, uz / ul];
// Band-Eckpunkte, ggf. an der Gehrungslinie verschnitten statt rechtwinklig
// bei a0/a1 gekappt (siehe Moduldoc oben / `clippedBand` im Web).
let off = |p: Point2, s: f32| -> Point2 { [p[0] + n[0] * s, p[1] + n[1] * s] };
let clip = |base: Point2, cut: Option<MiterLine>| -> Point2 {
match cut {
None => base,
Some(c) => line_intersect_2(base, u, c.point, c.dir).unwrap_or(base),
}
};
let g0 = clip(off(p0, off_a), start_cut);
let g1 = clip(off(p1, off_a), end_cut);
let g2 = clip(off(p1, off_b), end_cut);
let g3 = clip(off(p0, off_b), start_cut);
// world-Position aus Grundriss-XZ + Hoehe: (x, y, z=grundriss.y).
let w = |g: Point2, y: f32| -> [f32; 3] { [g[0], y, g[1]] };
@@ -162,30 +262,230 @@ fn extrude_wall_segment(
// Boden unten (-Y): b0,b3,b2,b1 (von unten gesehen CCW).
push_quad(mesh, b0, b3, b2, b1, [0.0, -1.0, 0.0], color);
// Vier Mantelflaechen. Aussen-Normalen aus dem Grundriss:
// Kante g0->g1 liegt auf der +n-Seite -> Normale +n.
// Kante g2->g3 liegt auf der -n-Seite -> Normale -n.
// Kappen an a0 (g3->g0) und a1 (g1->g2) liegen an den Achsen-Enden des
// Teilstuecks (bei einem inneren Teilstueck also am Rand der Oeffnung
// -> Leibungsflaeche/Reveal statt Wandende).
let np = [n[0], 0.0, n[1]]; // +n-Seite (world)
let nn = [-n[0], 0.0, -n[1]]; // -n-Seite
// Achsenrichtung der GESAMTEN Wand (fuer die End-Kappen-Normalen) — konstant
// ueber alle Teilstuecke, unabhaengig von `a0`/`a1`.
let ux = wall.end[0] - wall.start[0];
let uz = wall.end[1] - wall.start[1];
let ul = (ux * ux + uz * uz).sqrt().max(1e-9);
let u = [ux / ul, 0.0, uz / ul];
let neg_u = [-u[0], 0.0, -u[2]];
// +n-Wand: aussen betrachtet CCW = b0, b1, t1, t0.
// Zwei Mantelflaechen (+n-/n-Seite): bleiben bei Gehrung EXAKT planar mit
// fester Normale, weil sich die Ecken nur entlang `u` verschieben (siehe
// Moduldoc-Beweis) — der `off_a`/`off_b`-Abstand zur Achse bleibt konstant.
let np = [n[0], 0.0, n[1]];
let nn = [-n[0], 0.0, -n[1]];
push_quad(mesh, b0, b1, t1, t0, np, color);
// -n-Wand: aussen betrachtet CCW = b2, b3, t3, t2.
push_quad(mesh, b2, b3, t3, t2, nn, color);
// End-Kappe bei a1, Normale +u: b1, b2, t2, t1.
push_quad(mesh, b1, b2, t2, t1, u, color);
// End-Kappe bei a0, Normale -u: b3, b0, t0, t3.
push_quad(mesh, b3, b0, t0, t3, neg_u, color);
// End-Kappen bei a1/a0: Normale geometrisch hergeleitet (siehe Moduldoc) —
// liefert im rechtwinkligen Fall exakt +u/-u, bei Gehrung die tatsaechliche
// (schraege) Kappen-Normale.
let end_normal = quad_normal(b1, b2, t1);
let start_normal = quad_normal(b3, b0, t3);
push_quad(mesh, b1, b2, t2, t1, end_normal, color);
push_quad(mesh, b3, b0, t0, t3, start_normal, color);
}
/// Aussen-Normale eines planaren, CCW-von-aussen georderten Quads `(a,b,_,d)`
/// (nur die drei ersten Ecken werden gebraucht: `d` ist die vierte, ueber `a`
/// benachbarte Ecke). `cross(b-a, d-a)` — Herleitung/Verifikation siehe
/// Moduldoc von `compute_wall_miters`. Degenerierte (nulllange) Flaechen liefern
/// den Nullvektor statt NaN (Winkel spielt dort keine Rolle, siehe Aufrufer).
fn quad_normal(a: [f32; 3], b: [f32; 3], d: [f32; 3]) -> [f32; 3] {
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let ad = [d[0] - a[0], d[1] - a[1], d[2] - a[2]];
let c = [
ab[1] * ad[2] - ab[2] * ad[1],
ab[2] * ad[0] - ab[0] * ad[2],
ab[0] * ad[1] - ab[1] * ad[0],
];
let len = (c[0] * c[0] + c[1] * c[1] + c[2] * c[2]).sqrt().max(1e-9);
[c[0] / len, c[1] / len, c[2] / len]
}
// ─────────────────────────────────────────────────────────────────────────────
// Ecken-Verschneidung (Gehrung): Wandkonnektivitaet -> Gehrungslinien je Wandende
// ─────────────────────────────────────────────────────────────────────────────
/// Eine Gehrungslinie (Punkt + Richtung, Modell-Meter) an einem Wandende.
#[derive(Debug, Clone, Copy)]
struct MiterLine {
point: Point2,
dir: Point2,
}
/// Die (ggf. fehlenden) Gehrungslinien EINER Wand an Start/Ende.
#[derive(Debug, Clone, Copy, Default)]
struct WallMiters {
start: Option<MiterLine>,
end: Option<MiterLine>,
}
/// Schnittpunkt der Geraden `a + t*da` mit `b + s*db` (2D). `None` bei
/// (nahezu) parallelen Richtungen — 2D-Pendant zu `math::cross`/`lineIntersect`
/// im Web (`src/model/geometry.ts`).
fn line_intersect_2(a: Point2, da: Point2, b: Point2, db: Point2) -> Option<Point2> {
let denom = da[0] * db[1] - da[1] * db[0];
if denom.abs() < 1e-9 {
return None;
}
let bx = b[0] - a[0];
let by = b[1] - a[1];
let t = (bx * db[1] - by * db[0]) / denom;
Some([a[0] + t * da[0], a[1] + t * da[1]])
}
/// Rundungsschluessel eines 2D-Punkts (0.1 mm), zur robusten Gruppierung von
/// Wandenden am selben Knoten — 1:1 das Muster von `joins.ts::roundKey`.
fn round_key(p: Point2) -> (i64, i64) {
(
(p[0] * 1e4).round() as i64,
(p[1] * 1e4).round() as i64,
)
}
/// Ein Wandende, wie es fuer die Knoten-Gruppierung gebraucht wird.
struct EndRef {
wall: usize,
is_start: bool,
point: Point2,
/// Normalisierte Achsenrichtung Start->Ende der GESAMTEN Wand (konstant,
/// unabhaengig davon, ob es sich um das Start- oder Endende handelt).
dir: Point2,
z0: f32,
z1: f32,
}
/// Gruppiert `members` (Indizes in `ends`) nach echt ueberlappendem
/// Hoehenbereich (Union-Find) — trennt uebereinanderliegende Geschosse mit
/// deckungsgleichem Grundriss (deren Hoehenbereiche sich hoechstens BERUEHREN,
/// nicht ueberlappen) von echten Knoten INNERHALB eines Geschosses (deren
/// Hoehenbereiche sich ueberschneiden, auch wenn Oeffnungen ein Teilstueck auf
/// Bruestung/Sturz reduzieren).
fn cluster_by_height_overlap(ends: &[EndRef], members: &[usize]) -> Vec<Vec<usize>> {
let n = members.len();
let mut parent: Vec<usize> = (0..n).collect();
fn find(parent: &mut [usize], x: usize) -> usize {
if parent[x] != x {
let root = find(parent, parent[x]);
parent[x] = root;
}
parent[x]
}
const OVERLAP_EPS: f32 = 1e-4;
for i in 0..n {
for j in (i + 1)..n {
let a = &ends[members[i]];
let b = &ends[members[j]];
let lo = a.z0.max(b.z0);
let hi = a.z1.min(b.z1);
if hi - lo > OVERLAP_EPS {
let ri = find(&mut parent, i);
let rj = find(&mut parent, j);
if ri != rj {
parent[ri] = rj;
}
}
}
}
let mut clusters: HashMap<usize, Vec<usize>> = HashMap::new();
for i in 0..n {
let r = find(&mut parent, i);
clusters.entry(r).or_default().push(members[i]);
}
clusters.into_values().collect()
}
/// Gemeinsame Gehrungslinie zweier Wandenden `a`, `b`, die sich im selben
/// Knoten treffen — 1:1-Port von `joins.ts::miterLine` (Referenzlinie "center",
/// da `WallInput` keine `referenceLine`-Information mitfuehrt): `a`s linke
/// Flaeche wird mit der naeher gelegenen Flaeche von `b` verschnitten, `a`s
/// rechte mit der jeweils anderen. Liefert `None` bei (nahezu) kollinearen
/// Richtungen — das deckt insbesondere zwei Teilstuecke DERSELBEN Wand ab
/// (identische Richtung, siehe Moduldoc).
fn miter_line(walls: &[WallInput], a: &EndRef, b: &EndRef) -> Option<MiterLine> {
let j = a.point;
let t_a = walls[a.wall].thickness;
let t_b = walls[b.wall].thickness;
let n_a: Point2 = [-a.dir[1], a.dir[0]];
let n_b: Point2 = [-b.dir[1], b.dir[0]];
let add_scaled = |p: Point2, v: Point2, s: f32| -> Point2 { [p[0] + v[0] * s, p[1] + v[1] * s] };
let p_la = add_scaled(j, n_a, t_a * 0.5);
let p_ra = add_scaled(j, n_a, -t_a * 0.5);
let p_lb = add_scaled(j, n_b, t_b * 0.5);
let p_rb = add_scaled(j, n_b, -t_b * 0.5);
let dist2 = |p: Point2, q: Point2| {
let dx = p[0] - q[0];
let dy = p[1] - q[1];
dx * dx + dy * dy
};
let lb_closer = dist2(p_la, p_lb) <= dist2(p_la, p_rb);
let b_for_left = if lb_closer { p_lb } else { p_rb };
let b_for_right = if lb_closer { p_rb } else { p_lb };
let c1 = line_intersect_2(p_la, a.dir, b_for_left, b.dir)?;
let c2 = line_intersect_2(p_ra, a.dir, b_for_right, b.dir)?;
let dir = [c2[0] - c1[0], c2[1] - c1[1]];
if (dir[0] * dir[0] + dir[1] * dir[1]).sqrt() < 1e-9 {
return None; // kollinear -> kein Schnitt
}
Some(MiterLine { point: c1, dir })
}
fn apply_cut(out: &mut [WallMiters], e: &EndRef, cut: MiterLine) {
if e.is_start {
out[e.wall].start = Some(cut);
} else {
out[e.wall].end = Some(cut);
}
}
/// Berechnet fuer JEDE Wand in `walls` die Gehrungslinien an ihren echten
/// Achsenenden — reine Funktion der Wandkonnektivitaet (Endpunkte + Dicke +
/// Hoehenbereich), siehe Moduldoc "Ecken-Verschneidung". Nur Knoten, an denen
/// sich (innerhalb desselben Hoehenbereichs) GENAU zwei, nicht-kollineare
/// Wandenden treffen, werden gehrt; freie Enden, T-/X-Stoesse und mehrdeutige
/// Knoten (z. B. eine Oeffnung direkt an einer echten Ecke, die das Ende in
/// mehrere Hoehen-Teilstuecke zerlegt) bleiben konservativ rechtwinklig.
fn compute_wall_miters(walls: &[WallInput]) -> Vec<WallMiters> {
let mut out = vec![WallMiters::default(); walls.len()];
let mut ends: Vec<EndRef> = Vec::with_capacity(walls.len() * 2);
for (i, w) in walls.iter().enumerate() {
let n = left_normal(w.start, w.end);
if n == [0.0, 0.0] {
continue; // entartete Wand, siehe extrude_wall
}
// Achsenrichtung aus der Links-Normale zurueckgedreht (90 Grad):
// n = (-dir.y, dir.x) => dir = (n.y, -n.x). Siehe section.rs::wall_prism.
let dir: Point2 = [n[1], -n[0]];
let z0 = w.base_elevation;
let z1 = w.base_elevation + w.height;
ends.push(EndRef { wall: i, is_start: true, point: w.start, dir, z0, z1 });
ends.push(EndRef { wall: i, is_start: false, point: w.end, dir, z0, z1 });
}
let mut groups: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
for (idx, e) in ends.iter().enumerate() {
groups.entry(round_key(e.point)).or_default().push(idx);
}
for members in groups.into_values() {
if members.len() < 2 {
continue; // freies Ende
}
for cluster in cluster_by_height_overlap(&ends, &members) {
if cluster.len() != 2 {
continue; // T-/X-Stoss oder mehrdeutig -> rechtwinklig
}
let (ia, ib) = (cluster[0], cluster[1]);
let (a, b) = (&ends[ia], &ends[ib]);
if a.wall == b.wall {
continue; // eine Wand kann sich nicht selbst verschneiden
}
let Some(cut) = miter_line(walls, a, b) else {
continue;
};
apply_cut(&mut out, a, cut);
apply_cut(&mut out, b, cut);
}
}
out
}
/// Haengt ein Quad (vier Ecken in CCW von aussen) als zwei Dreiecke an, mit fester
@@ -210,12 +510,15 @@ fn push_quad(
}
/// Baut ein einzelnes Mesh aus mehreren Waenden (alle in EINEN Puffer). Die
/// Reihenfolge der Vertices ist deterministisch (je Wand ein Quader), damit die
/// Tests stabile Zaehlungen erwarten koennen.
/// Reihenfolge der Vertices ist deterministisch (je Wand ein Quader, bzw. bei
/// `layers`/Oeffnungen mehrere), damit die Tests stabile Zaehlungen erwarten
/// koennen. Berechnet zuerst die Eck-Gehrung ueber ALLE Waende (siehe
/// `compute_wall_miters`) und extrudiert dann jede Wand mit ihren Gehrungen.
pub fn build_walls_mesh(walls: &[WallInput]) -> Mesh {
let mut mesh = Mesh::default();
for w in walls {
extrude_wall(&mut mesh, w);
let miters = compute_wall_miters(walls);
for (w, m) in walls.iter().zip(miters.iter()) {
extrude_wall_with_miters(&mut mesh, w, *m);
}
mesh
}