Geometry-Crate: T-Stoss-Verschneidung nach Rust portiert (TS<->Rust-Paritaet)
compute_joins behandelt jetzt wie die TS-Referenz auch T-Knoten (3 Enden, kollineares Paar = Durchgang, Abzweig an dessen Flaeche geschnitten) und Mittelspannen-Stoesse (freies Ende trifft Wandseite). miter_line/L-Ecke bit-identisch. Rust-Tests aktualisiert (t_junction_branch_gets_face_cut, mid_span_tee_free_end_hits_wall_side). cargo test + parity.test.ts gruen.
This commit is contained in:
+261
-28
@@ -124,8 +124,10 @@ fn dir_of(w: &WallInput) -> Vec2 {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Berechnet fuer jede Wand die Gehrungs-Schnittlinien.
|
* Berechnet fuer jede Wand die Gehrungs-Schnittlinien.
|
||||||
* Nur L-Ecken (genau zwei Wandenden treffen sich) werden behandelt; freie
|
* Behandelt werden: L-Ecken (genau zwei Wandenden treffen sich), T-Knoten
|
||||||
* Enden und T-/X-Stoesse bleiben rechtwinklig.
|
* (drei Wandenden treffen sich, zwei davon kollinear) und Mittelspannen-
|
||||||
|
* T-Stoesse (ein freies Wandende trifft auf die Seite einer anderen Wand).
|
||||||
|
* X-Stoesse (vier und mehr Enden) bleiben vorerst rechtwinklig.
|
||||||
*/
|
*/
|
||||||
pub fn compute_joins(input: JoinInput) -> Vec<WallCuts> {
|
pub fn compute_joins(input: JoinInput) -> Vec<WallCuts> {
|
||||||
let walls = input.walls;
|
let walls = input.walls;
|
||||||
@@ -167,33 +169,51 @@ pub fn compute_joins(input: JoinInput) -> Vec<WallCuts> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (_key, ends) in &junctions {
|
for (_key, ends) in &junctions {
|
||||||
// Freies Ende -> kein Schnitt.
|
// Freies Ende -> kein Schnitt hier (wird unten separat auf Mittelspannen-T
|
||||||
|
// geprueft, Fall 2).
|
||||||
if ends.len() == 1 {
|
if ends.len() == 1 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// T-/X-Stoesse (>2 Enden): vorerst rechtwinklig lassen.
|
|
||||||
if ends.len() != 2 {
|
if ends.len() == 2 {
|
||||||
|
let a_idx = match index.get(ends[0].wall_id.as_str()) {
|
||||||
|
Some(i) => *i,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let b_idx = match index.get(ends[1].wall_id.as_str()) {
|
||||||
|
Some(i) => *i,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let a = &walls[a_idx];
|
||||||
|
let b = &walls[b_idx];
|
||||||
|
|
||||||
|
let cut = match miter_line(a, ends[0].end, b) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => continue, // kollinear -> kein Schnitt
|
||||||
|
};
|
||||||
|
|
||||||
|
set_cut(&mut result[a_idx], ends[0].end, cut);
|
||||||
|
set_cut(&mut result[b_idx], ends[1].end, cut);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let a_idx = match index.get(ends[0].wall_id.as_str()) {
|
if ends.len() == 3 {
|
||||||
Some(i) => *i,
|
apply_t_junction(&walls, &index, ends, &mut result);
|
||||||
None => continue,
|
continue;
|
||||||
};
|
}
|
||||||
let b_idx = match index.get(ends[1].wall_id.as_str()) {
|
|
||||||
Some(i) => *i,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
let a = &walls[a_idx];
|
|
||||||
let b = &walls[b_idx];
|
|
||||||
|
|
||||||
let cut = match miter_line(a, ends[0].end, b) {
|
// X-Stoesse u. ae. (>3 Enden): vorerst rechtwinklig lassen (Folge-Arbeit).
|
||||||
Some(c) => c,
|
}
|
||||||
None => continue, // kollinear -> kein Schnitt
|
|
||||||
};
|
|
||||||
|
|
||||||
set_cut(&mut result[a_idx], ends[0].end, cut);
|
// Fall 2: Mittelspannen-T-Stoss. Ein freies Wandende (kein anderes Wandende
|
||||||
set_cut(&mut result[b_idx], ends[1].end, cut);
|
// teilt sich seinen Knoten) kann trotzdem auf die Seite einer anderen Wand
|
||||||
|
// treffen (nicht an deren Endpunkten). In dem Fall bekommt das freie Ende
|
||||||
|
// einen Schnitt entlang der zugewandten Flaeche dieser Wand.
|
||||||
|
for (_key, ends) in &junctions {
|
||||||
|
if ends.len() != 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
apply_mid_span_tee(&walls, &index, &ends[0], &mut result);
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
@@ -207,6 +227,168 @@ fn set_cut(cuts: &mut WallCuts, end: WallEndKind, cut: Line) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fall 1 -- T-Knoten: drei Wandenden treffen sich im selben (gerundeten)
|
||||||
|
* Knoten. Typisch sind zwei davon kollinear (die zwei Haelften der
|
||||||
|
* Durchgangswand, die geometrisch als eine Achse durchlaeuft) und das dritte
|
||||||
|
* zweigt ab. Die kollinearen zwei bleiben ungeschnitten; der Abzweig bekommt
|
||||||
|
* einen Schnitt entlang der ihm zugewandten Flaeche der Durchgangswand.
|
||||||
|
* Findet sich kein eindeutig kollineares Paar (z. B. echter Y-/X-Knoten),
|
||||||
|
* bleibt der Knoten unangetastet (rechtwinklig).
|
||||||
|
*/
|
||||||
|
fn apply_t_junction(
|
||||||
|
walls: &[WallInput],
|
||||||
|
index: &std::collections::HashMap<&str, usize>,
|
||||||
|
ends: &[WallEnd],
|
||||||
|
result: &mut [WallCuts],
|
||||||
|
) {
|
||||||
|
let idx_of = |id: &str| index.get(id).copied();
|
||||||
|
|
||||||
|
let dirs: Vec<Vec2> = match ends
|
||||||
|
.iter()
|
||||||
|
.map(|e| idx_of(e.wall_id.as_str()).map(|i| dir_of(&walls[i])))
|
||||||
|
.collect::<Option<Vec<Vec2>>>()
|
||||||
|
{
|
||||||
|
Some(d) => d,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Suche das (naeherungsweise) kollineare Paar: |cos(Winkel der Achsen)| ~ 1.
|
||||||
|
let mut through_pair: Option<(usize, usize)> = None;
|
||||||
|
'outer: for i in 0..3 {
|
||||||
|
for k in (i + 1)..3 {
|
||||||
|
if 1.0 - dot(dirs[i], dirs[k]).abs() < 1e-6 {
|
||||||
|
through_pair = Some((i, k));
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (ti, tk) = match through_pair {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return, // kein eindeutiger Durchgang erkennbar
|
||||||
|
};
|
||||||
|
|
||||||
|
let branch_idx = match (0..3).find(|&i| i != ti && i != tk) {
|
||||||
|
Some(i) => i,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let branch_end = &ends[branch_idx];
|
||||||
|
let through_end = &ends[ti];
|
||||||
|
let branch_wall_idx = match idx_of(branch_end.wall_id.as_str()) {
|
||||||
|
Some(i) => i,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let through_wall_idx = match idx_of(through_end.wall_id.as_str()) {
|
||||||
|
Some(i) => i,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let branch_wall = &walls[branch_wall_idx];
|
||||||
|
let through_wall = &walls[through_wall_idx];
|
||||||
|
|
||||||
|
let j = match through_end.end {
|
||||||
|
WallEndKind::Start => through_wall.start,
|
||||||
|
WallEndKind::End => through_wall.end,
|
||||||
|
};
|
||||||
|
let t_t = through_wall.thickness;
|
||||||
|
let u_t = dir_of(through_wall);
|
||||||
|
let n_t = left_normal(u_t);
|
||||||
|
let off_t = through_wall.reference_offset;
|
||||||
|
|
||||||
|
// Auslaufrichtung des Abzweigs vom Knoten weg, in seinen eigenen Koerper
|
||||||
|
// (gleiche Konvention wie d_a/d_b in miter_line).
|
||||||
|
let d_branch = match branch_end.end {
|
||||||
|
WallEndKind::Start => dir_of(branch_wall),
|
||||||
|
WallEndKind::End => scale(dir_of(branch_wall), -1.0),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zugewandte Flaeche: die Seite der Durchgangsachse, zu der der Abzweig
|
||||||
|
// zeigt (positive n_t-Seite, wenn der Abzweig dorthin auslaeuft).
|
||||||
|
let sign = if dot(n_t, d_branch) >= 0.0 { 1.0 } else { -1.0 };
|
||||||
|
let face_point = add(j, scale(n_t, off_t + sign * (t_t / 2.0)));
|
||||||
|
|
||||||
|
set_cut(
|
||||||
|
&mut result[branch_wall_idx],
|
||||||
|
branch_end.end,
|
||||||
|
Line { point: face_point, dir: u_t },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fall 2 -- Mittelspannen-T-Stoss: prueft, ob das freie Wandende `free_end`
|
||||||
|
* auf die innere Spanne einer anderen Wand trifft (projizierter Parameter im
|
||||||
|
* Inneren, nicht an deren Endpunkten) und dabei senkrecht nah genug an deren
|
||||||
|
* Achse liegt (<= halbe Dicke + Toleranz). Bei Treffer bekommt das freie Ende
|
||||||
|
* einen Schnitt entlang der zugewandten Flaeche der getroffenen Wand; bei
|
||||||
|
* mehreren Treffern gewinnt die naechstliegende Wand.
|
||||||
|
*/
|
||||||
|
fn apply_mid_span_tee(
|
||||||
|
walls: &[WallInput],
|
||||||
|
index: &std::collections::HashMap<&str, usize>,
|
||||||
|
free_end: &WallEnd,
|
||||||
|
result: &mut [WallCuts],
|
||||||
|
) {
|
||||||
|
let free_wall_idx = match index.get(free_end.wall_id.as_str()) {
|
||||||
|
Some(i) => *i,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let free_wall = &walls[free_wall_idx];
|
||||||
|
let p = match free_end.end {
|
||||||
|
WallEndKind::Start => free_wall.start,
|
||||||
|
WallEndKind::End => free_wall.end,
|
||||||
|
};
|
||||||
|
|
||||||
|
let end_gap = 1e-4; // Mindestabstand zu den Wandenden fuer "innere Spanne"
|
||||||
|
let tol = 1e-3; // Toleranz fuer den senkrechten Abstand zur Wandflaeche
|
||||||
|
|
||||||
|
// (Index in `walls`, perp-Abstand vorzeichenbehaftet, absoluter Abstand).
|
||||||
|
let mut best: Option<(usize, f64, f64)> = None;
|
||||||
|
for (i, w) in walls.iter().enumerate() {
|
||||||
|
if w.id == free_wall.id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let u = dir_of(w);
|
||||||
|
let w_len = len(sub(w.end, w.start));
|
||||||
|
if w_len < 1e-9 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rel = sub(p, w.start);
|
||||||
|
let dist_along = dot(rel, u);
|
||||||
|
if dist_along <= end_gap || dist_along >= w_len - end_gap {
|
||||||
|
continue; // an Endpunkt, kein Mittelspann
|
||||||
|
}
|
||||||
|
|
||||||
|
let perp = dot(rel, left_normal(u));
|
||||||
|
let tw = w.thickness;
|
||||||
|
let dist = perp.abs();
|
||||||
|
if dist > tw / 2.0 + tol {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if best.map_or(true, |(_, _, best_dist)| dist < best_dist) {
|
||||||
|
best = Some((i, perp, dist));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (w_idx, perp, _dist) = match best {
|
||||||
|
Some(b) => b,
|
||||||
|
None => return, // kein Treffer -> rechtwinklig
|
||||||
|
};
|
||||||
|
|
||||||
|
let w = &walls[w_idx];
|
||||||
|
let tw = w.thickness;
|
||||||
|
let u_w = dir_of(w);
|
||||||
|
let n_w = left_normal(u_w);
|
||||||
|
let off_w = w.reference_offset;
|
||||||
|
let sign = if perp >= 0.0 { 1.0 } else { -1.0 };
|
||||||
|
let face_point = add(w.start, scale(n_w, off_w + sign * (tw / 2.0)));
|
||||||
|
|
||||||
|
set_cut(
|
||||||
|
&mut result[free_wall_idx],
|
||||||
|
free_end.end,
|
||||||
|
Line { point: face_point, dir: u_w },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gemeinsame Gehrungslinie zweier Waende A, B, die sich im Knoten J treffen.
|
* Gemeinsame Gehrungslinie zweier Waende A, B, die sich im Knoten J treffen.
|
||||||
* Robust gegen beliebige Wicklung, ungleiche Dicken und JEDEN Oeffnungswinkel
|
* Robust gegen beliebige Wicklung, ungleiche Dicken und JEDEN Oeffnungswinkel
|
||||||
@@ -378,8 +560,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t_junction_no_cut() {
|
fn t_junction_branch_gets_face_cut() {
|
||||||
// Drei Enden treffen sich in (5,0): rechtwinklig lassen.
|
// Drei Enden treffen sich in (5,0): A/C sind kollinear (Durchgangswand),
|
||||||
|
// B zweigt ab. A/C bleiben ungeschnitten; B bekommt einen Schnitt entlang
|
||||||
|
// der ihm zugewandten Flaeche der Durchgangswand (hier: y = +0.1, da B
|
||||||
|
// nach +y auslaeuft und die Durchgangswand entlang +x verlaeuft, also
|
||||||
|
// liegt deren linke Flaeche bei +0.1).
|
||||||
let input = JoinInput {
|
let input = JoinInput {
|
||||||
walls: vec![
|
walls: vec![
|
||||||
w("A", 0.0, 0.0, 5.0, 0.0, 0.2),
|
w("A", 0.0, 0.0, 5.0, 0.0, 0.2),
|
||||||
@@ -389,11 +575,21 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let out = compute_joins(input);
|
let out = compute_joins(input);
|
||||||
assert_eq!(out.len(), 3);
|
assert_eq!(out.len(), 3);
|
||||||
for id in ["A", "B", "C"] {
|
|
||||||
let c = find(&out, id);
|
let ca = find(&out, "A");
|
||||||
assert!(c.start_cut.is_none(), "{id} startCut none");
|
let cc = find(&out, "C");
|
||||||
assert!(c.end_cut.is_none(), "{id} endCut none");
|
assert!(ca.start_cut.is_none(), "A startCut ist freies Ende");
|
||||||
}
|
assert!(ca.end_cut.is_none(), "A durchgehend -> kein Schnitt");
|
||||||
|
assert!(cc.start_cut.is_none(), "C durchgehend -> kein Schnitt");
|
||||||
|
assert!(cc.end_cut.is_none(), "C endCut ist freies Ende");
|
||||||
|
|
||||||
|
let cb = find(&out, "B");
|
||||||
|
let cut = cb.start_cut.expect("B startCut gesetzt");
|
||||||
|
assert!(cb.end_cut.is_none(), "B endCut ist freies Ende");
|
||||||
|
assert!((cut.point.x - 5.0).abs() < 1e-9);
|
||||||
|
assert!((cut.point.y - 0.1).abs() < 1e-9, "Flaeche bei y=+0.1 erwartet");
|
||||||
|
// Schnittlinie verlaeuft entlang der Durchgangsachse (parallel zu +x).
|
||||||
|
assert!(cut.dir.y.abs() / len(cut.dir) < 1e-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -413,4 +609,41 @@ mod tests {
|
|||||||
assert!(c.end_cut.is_none(), "{id} endCut none");
|
assert!(c.end_cut.is_none(), "{id} endCut none");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mid_span_tee_free_end_hits_wall_side() {
|
||||||
|
// Freie Wand B endet bei (5,0) exakt auf der Achse der durchgehenden
|
||||||
|
// Wand A (0,0)-(10,0), mittig auf deren Spanne (nicht an deren Enden).
|
||||||
|
// B's anderes Ende (5,2) bleibt frei (zu weit von A entfernt). Der
|
||||||
|
// Treffer liegt exakt auf A's Achse (perp=0) -> die zugewandte Flaeche
|
||||||
|
// ist A's linke Flaeche bei y=+tw/2=+0.1 (sign=+1 per Konvention bei
|
||||||
|
// perp>=0). Die Schnittlinie ist als Punkt+Richtung auf A's Anker
|
||||||
|
// (A.start) verankert, verlaeuft aber unabhaengig davon entlang y=0.1.
|
||||||
|
let input = JoinInput {
|
||||||
|
walls: vec![
|
||||||
|
w("A", 0.0, 0.0, 10.0, 0.0, 0.2),
|
||||||
|
w("B", 5.0, 2.0, 5.0, 0.0, 0.2),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let out = compute_joins(input);
|
||||||
|
assert_eq!(out.len(), 2);
|
||||||
|
|
||||||
|
let ca = find(&out, "A");
|
||||||
|
assert!(ca.start_cut.is_none(), "A durchgehend -> kein Schnitt");
|
||||||
|
assert!(ca.end_cut.is_none(), "A durchgehend -> kein Schnitt");
|
||||||
|
|
||||||
|
let cb = find(&out, "B");
|
||||||
|
assert!(cb.start_cut.is_none(), "B startCut ist freies Ende bei (5,2)");
|
||||||
|
let cut = cb.end_cut.expect("B endCut (Mittelspann-Treffer) gesetzt");
|
||||||
|
|
||||||
|
// Verankert an A.start = (0,0), verschoben um A's halbe Dicke entlang
|
||||||
|
// ihrer linken Normale (0,1) -> Punkt (0, 0.1), Richtung parallel zu A.
|
||||||
|
assert!((cut.point.x - 0.0).abs() < 1e-9);
|
||||||
|
assert!((cut.point.y - 0.1).abs() < 1e-9);
|
||||||
|
assert!(cut.dir.y.abs() / len(cut.dir) < 1e-9, "Schnittlinie parallel zu A");
|
||||||
|
|
||||||
|
// Robuster: die Gerade verlaeuft exakt bei y=0.1, unabhaengig vom x.
|
||||||
|
let dist = |q: Vec2| cross(cut.dir, sub(q, cut.point)).abs() / len(cut.dir);
|
||||||
|
assert!(dist(Vec2 { x: 5.0, y: 0.1 }) < 1e-9, "Treffpunkt (5,0.1) auf Gehrung");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user