kernel2d-Port Phase 4: Trim/Split/Join (Loewenanteil)
Portiert (1:1, reihenfolge-/strukturtreu):
- splitSegmentByCutters, trimSegment, trimPolyline (+ span/nearestParamOnChain),
extendSegment.
- splitPolylineAtParam, splitClosedByChord (+ dedupeRing), removeSegment.
- splitAtIntersections (+ polylineEdgesAuto, splitOpenByEdgeHits/AtHits, EdgeHit).
- joinChains: greedy i<j-erster-Treffer-dann-Neustart, exakt wie TS
(splice-Semantik via remove(j)+open[i]=combined).
- Polyline-Struct {pts,closed}; stabile (edge,t)-Sortierung, 1e-6-Dedup.
- 9 Batch-Fassaden + 4 native Unit-Tests.
Harness: 5 neue Paritaets-Bloecke (Struktur EXAKT + Werte), Cutter-/Polylinien-
Generatoren, joinChains mit re-mergebaren Ketten. cargo test 18/18, vitest 247
(5 neu, davon 17 Parity) gruen, tsc sauber, build:kernel2d sauber.
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
// (f64 nicht assoziativ; kein Kahan/Reorder).
|
// (f64 nicht assoziativ; kein Kahan/Reorder).
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
/// EPS aus kernel2d.ts (Primitive/Schnitt/Trim). ACHTUNG: `lineIntersect`
|
/// EPS aus kernel2d.ts (Primitive/Schnitt/Trim). ACHTUNG: `lineIntersect`
|
||||||
/// benutzt bewusst ein ANDERES, hartkodiertes 1e-9 — nicht dieses EPS.
|
/// benutzt bewusst ein ANDERES, hartkodiertes 1e-9 — nicht dieses EPS.
|
||||||
@@ -414,6 +415,568 @@ pub fn fillet_corner(corner: Vec2, p1: Vec2, p2: Vec2, r: f64) -> Option<Fillet>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Trim / Split / Join -----------------------------------------------------
|
||||||
|
// Reine Geometrie auf Polylinien (`Vec2[]` + `closed`). Struktur- und
|
||||||
|
// reihenfolgeabhaengig — Sortier-Reihenfolge, Dedup-Schwellen (1e-6) und die
|
||||||
|
// greedy-Verbindungslogik von joinChains muessen EXAKT wie TS sein.
|
||||||
|
|
||||||
|
/// Polylinie / Cutter / Kette: `{pts, closed}` (Port des TS-`{pts, closed}`).
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct Polyline {
|
||||||
|
pub pts: Vec<Vec2>,
|
||||||
|
pub closed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ein Schnitt-Treffer auf einer Kante: Kantenindex + Parameter + Punkt.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct EdgeHit {
|
||||||
|
edge: usize,
|
||||||
|
t: f64,
|
||||||
|
point: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lineare Interpolation zweier Punkte.
|
||||||
|
fn lerp(a: Vec2, b: Vec2, t: f64) -> Vec2 {
|
||||||
|
add(a, scale(sub(b, a), t))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entfernt aufeinanderfolgende (nahezu) gleiche Punkte (kein Ringschluss).
|
||||||
|
fn dedupe_consecutive(pts: &[Vec2]) -> Vec<Vec2> {
|
||||||
|
let mut out: Vec<Vec2> = Vec::new();
|
||||||
|
for &p in pts {
|
||||||
|
if out.is_empty() || !vec_equal(out[out.len() - 1], p, EPS) {
|
||||||
|
out.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dedup aufeinanderfolgender Punkte UND schliessender Duplikat-Endpunkt.
|
||||||
|
fn dedupe_ring(pts: &[Vec2]) -> Vec<Vec2> {
|
||||||
|
let mut out = dedupe_consecutive(pts);
|
||||||
|
if out.len() > 1 && vec_equal(out[0], out[out.len() - 1], EPS) {
|
||||||
|
out.pop();
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vergleichsfunktion `(edge, t)` wie TS `(p.edge-q.edge) || (p.t-q.t)`, stabil.
|
||||||
|
fn cmp_edge_t(p: &EdgeHit, q: &EdgeHit) -> Ordering {
|
||||||
|
p.edge
|
||||||
|
.cmp(&q.edge)
|
||||||
|
.then(p.t.partial_cmp(&q.t).unwrap_or(Ordering::Equal))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zerschneidet eine STRECKE an allen inneren Cutter-Schnitten (t ∈ (EPS,1-EPS)).
|
||||||
|
pub fn split_segment_by_cutters(a1: Vec2, a2: Vec2, cutters: &[Polyline]) -> Vec<(Vec2, Vec2)> {
|
||||||
|
let mut ts: Vec<f64> = vec![0.0, 1.0];
|
||||||
|
for c in cutters {
|
||||||
|
for h in segment_polyline_hits(a1, a2, &c.pts, c.closed) {
|
||||||
|
if h.t > EPS && h.t < 1.0 - EPS {
|
||||||
|
ts.push(h.t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ts.sort_by(|p, q| p.partial_cmp(q).unwrap_or(Ordering::Equal));
|
||||||
|
let da = sub(a2, a1);
|
||||||
|
let mut pieces: Vec<(Vec2, Vec2)> = Vec::new();
|
||||||
|
for i in 0..ts.len() - 1 {
|
||||||
|
if ts[i + 1] - ts[i] < 1e-6 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pieces.push((add(a1, scale(da, ts[i])), add(a1, scale(da, ts[i + 1]))));
|
||||||
|
}
|
||||||
|
pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trim: schneidet an den Cuttern und VERWIRFT das dem `pick` naechste Teilstueck.
|
||||||
|
pub fn trim_segment(a1: Vec2, a2: Vec2, cutters: &[Polyline], pick: Vec2) -> Vec<(Vec2, Vec2)> {
|
||||||
|
let pieces = split_segment_by_cutters(a1, a2, cutters);
|
||||||
|
if pieces.len() <= 1 {
|
||||||
|
return pieces;
|
||||||
|
}
|
||||||
|
let mut best = 0usize;
|
||||||
|
let mut best_d = f64::INFINITY;
|
||||||
|
for (i, pc) in pieces.iter().enumerate() {
|
||||||
|
let d = point_segment_distance(pick, pc.0, pc.1);
|
||||||
|
if d < best_d {
|
||||||
|
best_d = d;
|
||||||
|
best = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pieces
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| *i != best)
|
||||||
|
.map(|(_, p)| p)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Globaler Lauf-Parameter (edgeIndex + t) des dem Punkt naechsten Kettenpunktes.
|
||||||
|
fn nearest_param_on_chain(edges: &[(Vec2, Vec2)], p: Vec2) -> f64 {
|
||||||
|
let mut best = 0.0;
|
||||||
|
let mut best_d = f64::INFINITY;
|
||||||
|
for (ei, &(a, b)) in edges.iter().enumerate() {
|
||||||
|
let t = project_param(p, a, b).min(1.0).max(0.0);
|
||||||
|
let q = add(a, scale(sub(b, a), t));
|
||||||
|
let d = dist(p, q);
|
||||||
|
if d < best_d {
|
||||||
|
best_d = d;
|
||||||
|
best = ei as f64 + t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quick-Trim einer ganzen Kurve an einem Klickpunkt (siehe TS-Doku).
|
||||||
|
pub fn trim_polyline(pts: &[Vec2], closed: bool, cutters: &[Polyline], pick: Vec2) -> Vec<Polyline> {
|
||||||
|
let edges = polyline_edges(pts, closed);
|
||||||
|
if edges.is_empty() {
|
||||||
|
return vec![Polyline { pts: pts.to_vec(), closed }];
|
||||||
|
}
|
||||||
|
let mut cuts: Vec<EdgeHit> = Vec::new();
|
||||||
|
for ei in 0..edges.len() {
|
||||||
|
let (a1, a2) = edges[ei];
|
||||||
|
for c in cutters {
|
||||||
|
for h in segment_polyline_hits(a1, a2, &c.pts, c.closed) {
|
||||||
|
if h.t > EPS && h.t < 1.0 - EPS {
|
||||||
|
cuts.push(EdgeHit { edge: ei, t: h.t, point: h.point });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cuts.sort_by(cmp_edge_t);
|
||||||
|
let mut cut: Vec<EdgeHit> = Vec::new();
|
||||||
|
for h in cuts {
|
||||||
|
if let Some(prev) = cut.last() {
|
||||||
|
if prev.edge == h.edge && (prev.t - h.t).abs() < 1e-6 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cut.push(h);
|
||||||
|
}
|
||||||
|
if cut.is_empty() {
|
||||||
|
return vec![Polyline { pts: pts.to_vec(), closed }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if !closed {
|
||||||
|
let pick_pos = nearest_param_on_chain(&edges, pick);
|
||||||
|
let cut_pos: Vec<f64> = cut.iter().map(|c| c.edge as f64 + c.t).collect();
|
||||||
|
let mut lo_idx: isize = -1;
|
||||||
|
let mut hi_idx: usize = cut.len();
|
||||||
|
for i in 0..cut.len() {
|
||||||
|
if cut_pos[i] <= pick_pos {
|
||||||
|
lo_idx = i as isize;
|
||||||
|
} else {
|
||||||
|
hi_idx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut result: Vec<Polyline> = Vec::new();
|
||||||
|
if lo_idx >= 0 {
|
||||||
|
let c = cut[lo_idx as usize];
|
||||||
|
let mut head: Vec<Vec2> = pts[..c.edge + 1].to_vec();
|
||||||
|
head.push(c.point);
|
||||||
|
let d = dedupe_consecutive(&head);
|
||||||
|
if d.len() >= 2 {
|
||||||
|
result.push(Polyline { pts: d, closed: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hi_idx < cut.len() {
|
||||||
|
let c = cut[hi_idx];
|
||||||
|
let mut tail: Vec<Vec2> = vec![c.point];
|
||||||
|
for k in c.edge + 1..pts.len() {
|
||||||
|
tail.push(pts[k]);
|
||||||
|
}
|
||||||
|
let d = dedupe_consecutive(&tail);
|
||||||
|
if d.len() >= 2 {
|
||||||
|
result.push(Polyline { pts: d, closed: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Geschlossen.
|
||||||
|
if cut.len() == 1 {
|
||||||
|
return vec![Polyline { pts: pts.to_vec(), closed: true }];
|
||||||
|
}
|
||||||
|
let total = edges.len();
|
||||||
|
let span = |e_x: usize, p_x: Vec2, e_y: usize, p_y: Vec2| -> Vec<Vec2> {
|
||||||
|
let mut out: Vec<Vec2> = vec![p_x];
|
||||||
|
let mut e = e_x;
|
||||||
|
let mut steps = 0usize;
|
||||||
|
while steps <= total {
|
||||||
|
if e == e_y {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.push(edges[e].1);
|
||||||
|
e = (e + 1) % total;
|
||||||
|
steps += 1;
|
||||||
|
}
|
||||||
|
out.push(p_y);
|
||||||
|
dedupe_consecutive(&out)
|
||||||
|
};
|
||||||
|
let pick_pos = nearest_param_on_chain(&edges, pick);
|
||||||
|
let cut_pos: Vec<f64> = cut.iter().map(|c| c.edge as f64 + c.t).collect();
|
||||||
|
let mut seg: isize = -1;
|
||||||
|
for i in 0..cut.len() {
|
||||||
|
let a = cut_pos[i];
|
||||||
|
let b = cut_pos[(i + 1) % cut.len()];
|
||||||
|
let inside = if i == cut.len() - 1 {
|
||||||
|
pick_pos >= a || pick_pos <= b
|
||||||
|
} else {
|
||||||
|
pick_pos >= a && pick_pos <= b
|
||||||
|
};
|
||||||
|
if inside {
|
||||||
|
seg = i as isize;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seg < 0 {
|
||||||
|
seg = 0;
|
||||||
|
}
|
||||||
|
let seg = seg as usize;
|
||||||
|
let from = cut[(seg + 1) % cut.len()];
|
||||||
|
let to = cut[seg];
|
||||||
|
let chain = span(from.edge, from.point, to.edge, to.point);
|
||||||
|
if chain.len() < 2 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
vec![Polyline { pts: chain, closed: false }]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verlaengert das gewaehlte Ende bis zur naechsten Cutter-Kante (oder None).
|
||||||
|
pub fn extend_segment(
|
||||||
|
a1: Vec2,
|
||||||
|
a2: Vec2,
|
||||||
|
end: &str,
|
||||||
|
cutters: &[Polyline],
|
||||||
|
) -> Option<(Vec2, Vec2)> {
|
||||||
|
let mut best_t: Option<f64> = None;
|
||||||
|
for c in cutters {
|
||||||
|
for (b1, b2) in polyline_edges(&c.pts, c.closed) {
|
||||||
|
let h = match line_segment_intersect(a1, a2, b1, b2, EPS) {
|
||||||
|
Some(h) => h,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
if end == "end" && h.t > 1.0 + EPS {
|
||||||
|
if best_t.map_or(true, |bt| h.t < bt) {
|
||||||
|
best_t = Some(h.t);
|
||||||
|
}
|
||||||
|
} else if end == "start" && h.t < -EPS {
|
||||||
|
if best_t.map_or(true, |bt| h.t > bt) {
|
||||||
|
best_t = Some(h.t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bt = best_t?;
|
||||||
|
let da = sub(a2, a1);
|
||||||
|
let hit_point = add(a1, scale(da, bt));
|
||||||
|
Some(if end == "end" {
|
||||||
|
(a1, hit_point)
|
||||||
|
} else {
|
||||||
|
(hit_point, a2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Teilt eine Polylinie an EINEM Punkt P = lerp(pts[edgeIndex], next, t).
|
||||||
|
pub fn split_polyline_at_param(
|
||||||
|
pts: &[Vec2],
|
||||||
|
closed: bool,
|
||||||
|
edge_index: usize,
|
||||||
|
t: f64,
|
||||||
|
) -> Vec<Vec<Vec2>> {
|
||||||
|
let n = pts.len();
|
||||||
|
if n < 2 {
|
||||||
|
return vec![pts.to_vec()];
|
||||||
|
}
|
||||||
|
let a = pts[edge_index];
|
||||||
|
let b = pts[(edge_index + 1) % n];
|
||||||
|
let p = lerp(a, b, t);
|
||||||
|
let at_start = t <= EPS;
|
||||||
|
let at_end = t >= 1.0 - EPS;
|
||||||
|
|
||||||
|
if !closed {
|
||||||
|
let mut left: Vec<Vec2> = pts[..edge_index + 1].to_vec();
|
||||||
|
if !at_start && !at_end {
|
||||||
|
left.push(p);
|
||||||
|
} else if at_end {
|
||||||
|
left.push(b);
|
||||||
|
}
|
||||||
|
let mut right: Vec<Vec2> = Vec::new();
|
||||||
|
if !at_start && !at_end {
|
||||||
|
right.push(p);
|
||||||
|
} else if at_start {
|
||||||
|
right.push(a);
|
||||||
|
}
|
||||||
|
for i in edge_index + 1..n {
|
||||||
|
right.push(pts[i]);
|
||||||
|
}
|
||||||
|
let pieces: Vec<Vec<Vec2>> = [left, right]
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| s.len() >= 2)
|
||||||
|
.collect();
|
||||||
|
return if !pieces.is_empty() {
|
||||||
|
pieces
|
||||||
|
} else {
|
||||||
|
vec![pts.to_vec()]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out: Vec<Vec2> = Vec::new();
|
||||||
|
if !at_start {
|
||||||
|
out.push(p);
|
||||||
|
}
|
||||||
|
for k in 1..=n {
|
||||||
|
out.push(pts[(edge_index + k) % n]);
|
||||||
|
}
|
||||||
|
if !at_start {
|
||||||
|
out.push(p);
|
||||||
|
} else {
|
||||||
|
out.push(a);
|
||||||
|
}
|
||||||
|
vec![dedupe_consecutive(&out)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Teilt ein GESCHLOSSENES Polygon an zwei Randpunkten via Sehne (2 Ringe).
|
||||||
|
pub fn split_closed_by_chord(
|
||||||
|
pts: &[Vec2],
|
||||||
|
i: usize,
|
||||||
|
ti: f64,
|
||||||
|
j: usize,
|
||||||
|
tj: f64,
|
||||||
|
) -> Option<(Vec<Vec2>, Vec<Vec2>)> {
|
||||||
|
let n = pts.len();
|
||||||
|
if n < 3 || i == j {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (ia, ta, ib, tb) = if i > j {
|
||||||
|
(j, tj, i, ti)
|
||||||
|
} else {
|
||||||
|
(i, ti, j, tj)
|
||||||
|
};
|
||||||
|
let p_a = lerp(pts[ia], pts[(ia + 1) % n], ta);
|
||||||
|
let p_b = lerp(pts[ib], pts[(ib + 1) % n], tb);
|
||||||
|
let mut arc1: Vec<Vec2> = vec![p_a];
|
||||||
|
for k in ia + 1..=ib {
|
||||||
|
arc1.push(pts[k % n]);
|
||||||
|
}
|
||||||
|
arc1.push(p_b);
|
||||||
|
let mut arc2: Vec<Vec2> = vec![p_b];
|
||||||
|
for k in ib + 1..=ia + n {
|
||||||
|
arc2.push(pts[k % n]);
|
||||||
|
}
|
||||||
|
arc2.push(p_a);
|
||||||
|
Some((dedupe_ring(&arc1), dedupe_ring(&arc2)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entfernt das Segment `edge_index` (offen: laengeres Stueck; geschlossen: auftrennen).
|
||||||
|
pub fn remove_segment(pts: &[Vec2], closed: bool, edge_index: usize) -> Polyline {
|
||||||
|
let n = pts.len();
|
||||||
|
if closed {
|
||||||
|
let mut out: Vec<Vec2> = Vec::new();
|
||||||
|
for k in 1..=n {
|
||||||
|
out.push(pts[(edge_index + k) % n]);
|
||||||
|
}
|
||||||
|
return Polyline { pts: dedupe_consecutive(&out), closed: false };
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
return Polyline { pts: Vec::new(), closed: false };
|
||||||
|
}
|
||||||
|
if edge_index == 0 {
|
||||||
|
return Polyline { pts: pts[1..].to_vec(), closed: false };
|
||||||
|
}
|
||||||
|
if edge_index >= n - 1 {
|
||||||
|
return Polyline { pts: pts[..n - 1].to_vec(), closed: false };
|
||||||
|
}
|
||||||
|
let left = pts[..edge_index + 1].to_vec();
|
||||||
|
let right = pts[edge_index + 1..].to_vec();
|
||||||
|
if left.len() >= right.len() {
|
||||||
|
Polyline { pts: left, closed: false }
|
||||||
|
} else {
|
||||||
|
Polyline { pts: right, closed: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kanten einer (offenen ODER implizit geschlossenen) Punktliste.
|
||||||
|
fn polyline_edges_auto(pts: &[Vec2]) -> Vec<(Vec2, Vec2)> {
|
||||||
|
let closed = pts.len() > 2 && vec_equal(pts[0], pts[pts.len() - 1], EPS);
|
||||||
|
if closed {
|
||||||
|
polyline_edges(&pts[..pts.len() - 1], true)
|
||||||
|
} else {
|
||||||
|
polyline_edges(pts, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offene Polylinie an gegebenen EdgeHits (auf ihren Kanten) zerschneiden.
|
||||||
|
fn split_open_by_edge_hits(pts: &[Vec2], hits: &[EdgeHit]) -> Vec<Vec<Vec2>> {
|
||||||
|
if pts.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let mut pieces: Vec<Vec<Vec2>> = Vec::new();
|
||||||
|
let mut cur: Vec<Vec2> = vec![pts[0]];
|
||||||
|
let mut hi = 0usize;
|
||||||
|
for ei in 0..pts.len() - 1 {
|
||||||
|
while hi < hits.len() && hits[hi].edge == ei {
|
||||||
|
let p = hits[hi].point;
|
||||||
|
cur.push(p);
|
||||||
|
pieces.push(cur.clone());
|
||||||
|
cur = vec![p];
|
||||||
|
hi += 1;
|
||||||
|
}
|
||||||
|
cur.push(pts[ei + 1]);
|
||||||
|
}
|
||||||
|
pieces.push(cur);
|
||||||
|
pieces
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| dedupe_consecutive(&s))
|
||||||
|
.filter(|s| s.len() >= 2)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offene Polylinie an einer Liste von Schnitt-PUNKTEN (auf dem Zug) zerschneiden.
|
||||||
|
fn split_open_at_hits(pts: &[Vec2], cut_points: &[Vec2]) -> Vec<Vec<Vec2>> {
|
||||||
|
if cut_points.is_empty() {
|
||||||
|
return vec![pts.to_vec()];
|
||||||
|
}
|
||||||
|
let mut hits: Vec<EdgeHit> = Vec::new();
|
||||||
|
for ei in 0..pts.len().saturating_sub(1) {
|
||||||
|
let a = pts[ei];
|
||||||
|
let b = pts[ei + 1];
|
||||||
|
for &cp in cut_points {
|
||||||
|
let t = project_param(cp, a, b);
|
||||||
|
if t > EPS && t < 1.0 - EPS && point_segment_distance(cp, a, b) < 1e-6 {
|
||||||
|
hits.push(EdgeHit { edge: ei, t, point: cp });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hits.sort_by(cmp_edge_t);
|
||||||
|
split_open_by_edge_hits(pts, &hits)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Teilt das Ziel an allen Schnittpunkten mit den anderen Polylinien.
|
||||||
|
pub fn split_at_intersections(
|
||||||
|
target_pts: &[Vec2],
|
||||||
|
closed: bool,
|
||||||
|
others: &[Vec<Vec2>],
|
||||||
|
) -> Vec<Vec<Vec2>> {
|
||||||
|
let edges = polyline_edges(target_pts, closed);
|
||||||
|
let mut hits: Vec<EdgeHit> = Vec::new();
|
||||||
|
for ei in 0..edges.len() {
|
||||||
|
let (a1, a2) = edges[ei];
|
||||||
|
for o in others {
|
||||||
|
for (b1, b2) in polyline_edges_auto(o) {
|
||||||
|
if let Some(h) = segment_intersect(a1, a2, b1, b2, EPS) {
|
||||||
|
hits.push(EdgeHit { edge: ei, t: h.t, point: h.point });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hits.sort_by(cmp_edge_t);
|
||||||
|
let mut dedup: Vec<EdgeHit> = Vec::new();
|
||||||
|
for h in hits {
|
||||||
|
if let Some(prev) = dedup.last() {
|
||||||
|
if prev.edge == h.edge && (prev.t - h.t).abs() < 1e-6 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if h.t <= EPS || h.t >= 1.0 - EPS {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dedup.push(h);
|
||||||
|
}
|
||||||
|
if dedup.is_empty() {
|
||||||
|
return vec![target_pts.to_vec()];
|
||||||
|
}
|
||||||
|
if closed {
|
||||||
|
if dedup.len() == 2 {
|
||||||
|
return match split_closed_by_chord(
|
||||||
|
target_pts, dedup[0].edge, dedup[0].t, dedup[1].edge, dedup[1].t,
|
||||||
|
) {
|
||||||
|
Some((a, b)) => vec![a, b],
|
||||||
|
None => vec![target_pts.to_vec()],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let opened = split_polyline_at_param(target_pts, true, dedup[0].edge, dedup[0].t)
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let cut_pts: Vec<Vec2> = dedup[1..].iter().map(|h| h.point).collect();
|
||||||
|
return split_open_at_hits(&opened, &cut_pts);
|
||||||
|
}
|
||||||
|
split_open_by_edge_hits(target_pts, &dedup)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verschmilzt Polylinien an koinzidenten Endpunkten zu laengeren Ketten.
|
||||||
|
/// Greedy `i<j`-erster-Treffer-dann-Neustart — reihenfolgeabhaengig, exakt wie TS.
|
||||||
|
pub fn join_chains(polylines: &[Polyline]) -> Vec<Polyline> {
|
||||||
|
let mut closed_out: Vec<Polyline> = Vec::new();
|
||||||
|
let mut open: Vec<Vec<Vec2>> = Vec::new();
|
||||||
|
for pl in polylines {
|
||||||
|
if pl.closed {
|
||||||
|
closed_out.push(Polyline { pts: pl.pts.clone(), closed: true });
|
||||||
|
} else if pl.pts.len() >= 2 {
|
||||||
|
open.push(pl.pts.clone());
|
||||||
|
} else if pl.pts.len() == 1 {
|
||||||
|
open.push(pl.pts.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut merged = true;
|
||||||
|
while merged {
|
||||||
|
merged = false;
|
||||||
|
'outer: for i in 0..open.len() {
|
||||||
|
for j in i + 1..open.len() {
|
||||||
|
let a = &open[i];
|
||||||
|
let b = &open[j];
|
||||||
|
let a_s = a[0];
|
||||||
|
let a_e = a[a.len() - 1];
|
||||||
|
let b_s = b[0];
|
||||||
|
let b_e = b[b.len() - 1];
|
||||||
|
let combined: Option<Vec<Vec2>> = if vec_equal(a_e, b_s, EPS) {
|
||||||
|
let mut v = a.clone();
|
||||||
|
v.extend_from_slice(&b[1..]);
|
||||||
|
Some(v)
|
||||||
|
} else if vec_equal(a_e, b_e, EPS) {
|
||||||
|
let mut v = a.clone();
|
||||||
|
let mut rev: Vec<Vec2> = b[..b.len() - 1].to_vec();
|
||||||
|
rev.reverse();
|
||||||
|
v.extend(rev);
|
||||||
|
Some(v)
|
||||||
|
} else if vec_equal(a_s, b_e, EPS) {
|
||||||
|
let mut v = b.clone();
|
||||||
|
v.extend_from_slice(&a[1..]);
|
||||||
|
Some(v)
|
||||||
|
} else if vec_equal(a_s, b_s, EPS) {
|
||||||
|
let mut v: Vec<Vec2> = b.clone();
|
||||||
|
v.reverse();
|
||||||
|
v.extend_from_slice(&a[1..]);
|
||||||
|
Some(v)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(c) = combined {
|
||||||
|
open.remove(j);
|
||||||
|
open[i] = c;
|
||||||
|
merged = true;
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out: Vec<Polyline> = closed_out;
|
||||||
|
for chain in open {
|
||||||
|
if chain.len() > 2 && vec_equal(chain[0], chain[chain.len() - 1], EPS) {
|
||||||
|
out.push(Polyline { pts: chain[..chain.len() - 1].to_vec(), closed: true });
|
||||||
|
} else {
|
||||||
|
out.push(Polyline { pts: chain, closed: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
// --- Batch-WASM-Fassade (Feature "web") --------------------------------------
|
// --- Batch-WASM-Fassade (Feature "web") --------------------------------------
|
||||||
// Phase 1: nur ein Versions-/Ping-Export, um die WASM-Grenze + das Tooling
|
// Phase 1: nur ein Versions-/Ping-Export, um die WASM-Grenze + das Tooling
|
||||||
// (wasm-pack → pkgKernel2d → Vite/vitest) end-to-end gruen zu bekommen. Die
|
// (wasm-pack → pkgKernel2d → Vite/vitest) end-to-end gruen zu bekommen. Die
|
||||||
@@ -517,6 +1080,79 @@ struct FilletQuery {
|
|||||||
r: f64,
|
r: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SplitCuttersQuery {
|
||||||
|
a1: Vec2,
|
||||||
|
a2: Vec2,
|
||||||
|
cutters: Vec<Polyline>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TrimSegQuery {
|
||||||
|
a1: Vec2,
|
||||||
|
a2: Vec2,
|
||||||
|
cutters: Vec<Polyline>,
|
||||||
|
pick: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TrimPolyQuery {
|
||||||
|
pts: Vec<Vec2>,
|
||||||
|
closed: bool,
|
||||||
|
cutters: Vec<Polyline>,
|
||||||
|
pick: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ExtendQuery {
|
||||||
|
a1: Vec2,
|
||||||
|
a2: Vec2,
|
||||||
|
end: String,
|
||||||
|
cutters: Vec<Polyline>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SplitAtParamQuery {
|
||||||
|
pts: Vec<Vec2>,
|
||||||
|
closed: bool,
|
||||||
|
#[serde(rename = "edgeIndex")]
|
||||||
|
edge_index: usize,
|
||||||
|
t: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ChordQuery {
|
||||||
|
pts: Vec<Vec2>,
|
||||||
|
i: usize,
|
||||||
|
ti: f64,
|
||||||
|
j: usize,
|
||||||
|
tj: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RemoveSegQuery {
|
||||||
|
pts: Vec<Vec2>,
|
||||||
|
closed: bool,
|
||||||
|
#[serde(rename = "edgeIndex")]
|
||||||
|
edge_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SplitIntersectQuery {
|
||||||
|
#[serde(rename = "targetPts")]
|
||||||
|
target_pts: Vec<Vec2>,
|
||||||
|
closed: bool,
|
||||||
|
others: Vec<Vec<Vec2>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
pub fn project_param_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
|
pub fn project_param_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
@@ -673,6 +1309,119 @@ pub fn fillet_corner_batch_json(input_json: &str) -> Result<String, wasm_bindgen
|
|||||||
to_js(&out)
|
to_js(&out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn split_segment_by_cutters_batch_json(
|
||||||
|
input_json: &str,
|
||||||
|
) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
let qs: Vec<SplitCuttersQuery> = from_js(input_json)?;
|
||||||
|
let out: Vec<Vec<(Vec2, Vec2)>> = qs
|
||||||
|
.iter()
|
||||||
|
.map(|q| split_segment_by_cutters(q.a1, q.a2, &q.cutters))
|
||||||
|
.collect();
|
||||||
|
to_js(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn trim_segment_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
let qs: Vec<TrimSegQuery> = from_js(input_json)?;
|
||||||
|
let out: Vec<Vec<(Vec2, Vec2)>> = qs
|
||||||
|
.iter()
|
||||||
|
.map(|q| trim_segment(q.a1, q.a2, &q.cutters, q.pick))
|
||||||
|
.collect();
|
||||||
|
to_js(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn trim_polyline_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
let qs: Vec<TrimPolyQuery> = from_js(input_json)?;
|
||||||
|
let out: Vec<Vec<Polyline>> = qs
|
||||||
|
.iter()
|
||||||
|
.map(|q| trim_polyline(&q.pts, q.closed, &q.cutters, q.pick))
|
||||||
|
.collect();
|
||||||
|
to_js(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn extend_segment_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
let qs: Vec<ExtendQuery> = from_js(input_json)?;
|
||||||
|
let out: Vec<Option<(Vec2, Vec2)>> = qs
|
||||||
|
.iter()
|
||||||
|
.map(|q| extend_segment(q.a1, q.a2, &q.end, &q.cutters))
|
||||||
|
.collect();
|
||||||
|
to_js(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn split_polyline_at_param_batch_json(
|
||||||
|
input_json: &str,
|
||||||
|
) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
let qs: Vec<SplitAtParamQuery> = from_js(input_json)?;
|
||||||
|
let out: Vec<Vec<Vec<Vec2>>> = qs
|
||||||
|
.iter()
|
||||||
|
.map(|q| split_polyline_at_param(&q.pts, q.closed, q.edge_index, q.t))
|
||||||
|
.collect();
|
||||||
|
to_js(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn split_closed_by_chord_batch_json(
|
||||||
|
input_json: &str,
|
||||||
|
) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
let qs: Vec<ChordQuery> = from_js(input_json)?;
|
||||||
|
let out: Vec<Option<(Vec<Vec2>, Vec<Vec2>)>> = qs
|
||||||
|
.iter()
|
||||||
|
.map(|q| split_closed_by_chord(&q.pts, q.i, q.ti, q.j, q.tj))
|
||||||
|
.collect();
|
||||||
|
to_js(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn remove_segment_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
let qs: Vec<RemoveSegQuery> = from_js(input_json)?;
|
||||||
|
let out: Vec<Polyline> = qs
|
||||||
|
.iter()
|
||||||
|
.map(|q| remove_segment(&q.pts, q.closed, q.edge_index))
|
||||||
|
.collect();
|
||||||
|
to_js(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn split_at_intersections_batch_json(
|
||||||
|
input_json: &str,
|
||||||
|
) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
let qs: Vec<SplitIntersectQuery> = from_js(input_json)?;
|
||||||
|
let out: Vec<Vec<Vec<Vec2>>> = qs
|
||||||
|
.iter()
|
||||||
|
.map(|q| split_at_intersections(&q.target_pts, q.closed, &q.others))
|
||||||
|
.collect();
|
||||||
|
to_js(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn join_chains_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
let groups: Vec<Vec<Polyline>> = from_js(input_json)?;
|
||||||
|
let out: Vec<Vec<Polyline>> = groups.iter().map(|g| join_chains(g)).collect();
|
||||||
|
to_js(&out)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -855,4 +1604,67 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.is_none());
|
.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn poly(pts: &[(f64, f64)], closed: bool) -> Polyline {
|
||||||
|
Polyline {
|
||||||
|
pts: pts.iter().map(|&(x, y)| Vec2::new(x, y)).collect(),
|
||||||
|
closed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trim_segment_removes_picked_piece() {
|
||||||
|
// Strecke (0,0)->(4,0), ein vertikaler Cutter bei x=2 → zwei Stuecke.
|
||||||
|
// Pick bei (0.5,0) → linkes Stueck faellt weg, rechtes bleibt.
|
||||||
|
let cutters = [poly(&[(2.0, -1.0), (2.0, 1.0)], false)];
|
||||||
|
let rest = trim_segment(
|
||||||
|
Vec2::new(0.0, 0.0),
|
||||||
|
Vec2::new(4.0, 0.0),
|
||||||
|
&cutters,
|
||||||
|
Vec2::new(0.5, 0.0),
|
||||||
|
);
|
||||||
|
assert_eq!(rest.len(), 1);
|
||||||
|
assert!((rest[0].0.x - 2.0).abs() < 1e-9 && (rest[0].1.x - 4.0).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_at_intersections_closed_square_by_two_cuts() {
|
||||||
|
// Einheitsquadrat, ein waagerechter Schneider y=0.5 quer → zwei Ringe.
|
||||||
|
let square = [
|
||||||
|
Vec2::new(0.0, 0.0),
|
||||||
|
Vec2::new(1.0, 0.0),
|
||||||
|
Vec2::new(1.0, 1.0),
|
||||||
|
Vec2::new(0.0, 1.0),
|
||||||
|
];
|
||||||
|
let others = vec![vec![Vec2::new(-1.0, 0.5), Vec2::new(2.0, 0.5)]];
|
||||||
|
let parts = split_at_intersections(&square, true, &others);
|
||||||
|
assert_eq!(parts.len(), 2, "zwei geschlossene Teilpolygone");
|
||||||
|
for p in &parts {
|
||||||
|
assert!(p.len() >= 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn join_chains_merges_and_closes() {
|
||||||
|
// Drei offene Kanten eines Dreiecks → eine geschlossene Kette.
|
||||||
|
let input = vec![
|
||||||
|
poly(&[(0.0, 0.0), (1.0, 0.0)], false),
|
||||||
|
poly(&[(1.0, 0.0), (0.5, 1.0)], false),
|
||||||
|
poly(&[(0.5, 1.0), (0.0, 0.0)], false),
|
||||||
|
];
|
||||||
|
let out = join_chains(&input);
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert!(out[0].closed, "Dreieck schliesst sich");
|
||||||
|
assert_eq!(out[0].pts.len(), 3, "Schlusspunkt-Duplikat entfernt");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_segment_open_keeps_longer_piece() {
|
||||||
|
// Offene Polylinie mit 5 Punkten; innere Kante 1 entfernen → laengeres Stueck.
|
||||||
|
let pts: Vec<Vec2> = (0..5).map(|i| Vec2::new(i as f64, 0.0)).collect();
|
||||||
|
let out = remove_segment(&pts, false, 1);
|
||||||
|
assert!(!out.closed);
|
||||||
|
assert_eq!(out.pts.len(), 3, "rechtes (laengeres) Stueck pts[2..5]");
|
||||||
|
assert!((out.pts[0].x - 2.0).abs() < 1e-9);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,22 +18,33 @@ import type { Vec2 } from "../model/types";
|
|||||||
import {
|
import {
|
||||||
circleCircleIntersect,
|
circleCircleIntersect,
|
||||||
closestPointOnSegment,
|
closestPointOnSegment,
|
||||||
|
extendSegment,
|
||||||
filletCorner,
|
filletCorner,
|
||||||
isCCW,
|
isCCW,
|
||||||
|
joinChains,
|
||||||
lineCircleIntersect,
|
lineCircleIntersect,
|
||||||
lineSegmentIntersect,
|
lineSegmentIntersect,
|
||||||
offsetPolyline,
|
offsetPolyline,
|
||||||
offsetSegment,
|
offsetSegment,
|
||||||
pointSegmentDistance,
|
pointSegmentDistance,
|
||||||
projectParam,
|
projectParam,
|
||||||
|
removeSegment,
|
||||||
segmentCircleIntersect,
|
segmentCircleIntersect,
|
||||||
segmentIntersect,
|
segmentIntersect,
|
||||||
segmentPolylineHits,
|
segmentPolylineHits,
|
||||||
signedArea,
|
signedArea,
|
||||||
|
splitAtIntersections,
|
||||||
|
splitClosedByChord,
|
||||||
|
splitPolylineAtParam,
|
||||||
|
splitSegmentByCutters,
|
||||||
|
trimPolyline,
|
||||||
|
trimSegment,
|
||||||
type Fillet,
|
type Fillet,
|
||||||
type Hit,
|
type Hit,
|
||||||
} from "./kernel2d";
|
} from "./kernel2d";
|
||||||
|
|
||||||
|
type Poly = { pts: Vec2[]; closed: boolean };
|
||||||
|
|
||||||
// Das WASM-Paket ist git-ignoriert und wird nur von `npm run build:kernel2d`
|
// Das WASM-Paket ist git-ignoriert und wird nur von `npm run build:kernel2d`
|
||||||
// erzeugt. Fehlt es, wird die Suite SAUBER uebersprungen (statt Collection-Fehler),
|
// erzeugt. Fehlt es, wird die Suite SAUBER uebersprungen (statt Collection-Fehler),
|
||||||
// damit `vitest run` ohne vorherigen WASM-Build gruen bleibt. Darum dynamischer
|
// damit `vitest run` ohne vorherigen WASM-Build gruen bleibt. Darum dynamischer
|
||||||
@@ -108,6 +119,33 @@ function closeVec(a: Vec2, b: Vec2, rel = 1e-9): boolean {
|
|||||||
function closeHit(a: Hit, b: Hit): boolean {
|
function closeHit(a: Hit, b: Hit): boolean {
|
||||||
return closeVec(a.point, b.point) && closeNum(a.t, b.t) && closeNum(a.s, b.s);
|
return closeVec(a.point, b.point) && closeNum(a.t, b.t) && closeNum(a.s, b.s);
|
||||||
}
|
}
|
||||||
|
function eqPts(w: Vec2[], t: Vec2[]): boolean {
|
||||||
|
return w.length === t.length && t.every((p, i) => closeVec(w[i], p));
|
||||||
|
}
|
||||||
|
function eqPtsLists(w: Vec2[][], t: Vec2[][]): boolean {
|
||||||
|
return w.length === t.length && t.every((s, i) => eqPts(w[i], s));
|
||||||
|
}
|
||||||
|
function eqPolyList(w: Poly[], t: Poly[]): boolean {
|
||||||
|
return w.length === t.length && t.every((pl, i) => w[i].closed === pl.closed && eqPts(w[i].pts, pl.pts));
|
||||||
|
}
|
||||||
|
function eqPairs(w: [Vec2, Vec2][], t: [Vec2, Vec2][]): boolean {
|
||||||
|
return w.length === t.length && t.every((pr, i) => closeVec(w[i][0], pr[0]) && closeVec(w[i][1], pr[1]));
|
||||||
|
}
|
||||||
|
/** Ein paar lange Schneider-Segmente quer durch die BBox (als offene Cutter). */
|
||||||
|
function genCutters(rng: () => number): Poly[] {
|
||||||
|
const k = 1 + Math.floor(rng() * 3);
|
||||||
|
return Array.from({ length: k }, () => ({
|
||||||
|
pts: [
|
||||||
|
{ x: -80, y: (rng() * 2 - 1) * 60 },
|
||||||
|
{ x: 80, y: (rng() * 2 - 1) * 60 },
|
||||||
|
],
|
||||||
|
closed: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
function genPolyline(rng: () => number, minN: number, maxN: number): Vec2[] {
|
||||||
|
const n = minN + Math.floor(rng() * (maxN - minN + 1));
|
||||||
|
return Array.from({ length: n }, () => ({ x: (rng() * 2 - 1) * 50, y: (rng() * 2 - 1) * 50 }));
|
||||||
|
}
|
||||||
|
|
||||||
const N = 300;
|
const N = 300;
|
||||||
|
|
||||||
@@ -287,6 +325,123 @@ describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Zufall", () => {
|
|||||||
});
|
});
|
||||||
expect(valid, "keine gueltige Verrundung — Test waere aussagelos").toBeGreaterThan(50);
|
expect(valid, "keine gueltige Verrundung — Test waere aussagelos").toBeGreaterThan(50);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("splitSegmentByCutters / trimSegment (Struktur + Werte)", () => {
|
||||||
|
const rng = mulberry32(9);
|
||||||
|
const qs = Array.from({ length: N }, () => ({
|
||||||
|
a1: { x: -60, y: (rng() * 2 - 1) * 40 },
|
||||||
|
a2: { x: 60, y: (rng() * 2 - 1) * 40 },
|
||||||
|
cutters: genCutters(rng),
|
||||||
|
pick: { x: (rng() * 2 - 1) * 60, y: (rng() * 2 - 1) * 40 },
|
||||||
|
}));
|
||||||
|
const wSplit = JSON.parse(K.split_segment_by_cutters_batch_json(JSON.stringify(qs))) as [Vec2, Vec2][][];
|
||||||
|
const wTrim = JSON.parse(K.trim_segment_batch_json(JSON.stringify(qs))) as [Vec2, Vec2][][];
|
||||||
|
qs.forEach((q, i) => {
|
||||||
|
expect(eqPairs(wSplit[i], splitSegmentByCutters(q.a1, q.a2, q.cutters)), `split#${i}`).toBe(true);
|
||||||
|
expect(eqPairs(wTrim[i], trimSegment(q.a1, q.a2, q.cutters, q.pick)), `trim#${i}`).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("trimPolyline / splitAtIntersections (offen+geschlossen, Struktur exakt)", () => {
|
||||||
|
const rng = mulberry32(10);
|
||||||
|
const qs = Array.from({ length: N }, () => ({
|
||||||
|
pts: genPolyline(rng, 4, 9),
|
||||||
|
closed: rng() < 0.5,
|
||||||
|
cutters: genCutters(rng),
|
||||||
|
pick: { x: (rng() * 2 - 1) * 50, y: (rng() * 2 - 1) * 50 },
|
||||||
|
others: genCutters(rng).map((c) => c.pts),
|
||||||
|
}));
|
||||||
|
const wTrim = JSON.parse(K.trim_polyline_batch_json(JSON.stringify(qs))) as Poly[][];
|
||||||
|
const wSplit = JSON.parse(
|
||||||
|
K.split_at_intersections_batch_json(
|
||||||
|
JSON.stringify(qs.map((q) => ({ targetPts: q.pts, closed: q.closed, others: q.others }))),
|
||||||
|
),
|
||||||
|
) as Vec2[][][];
|
||||||
|
qs.forEach((q, i) => {
|
||||||
|
expect(eqPolyList(wTrim[i], trimPolyline(q.pts, q.closed, q.cutters, q.pick)), `trimPoly#${i}`).toBe(true);
|
||||||
|
expect(eqPtsLists(wSplit[i], splitAtIntersections(q.pts, q.closed, q.others)), `splitAt#${i}`).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("splitPolylineAtParam / splitClosedByChord / removeSegment", () => {
|
||||||
|
const rng = mulberry32(11);
|
||||||
|
const qs = Array.from({ length: N }, () => {
|
||||||
|
const pts = genPolyline(rng, 4, 9);
|
||||||
|
const n = pts.length;
|
||||||
|
const closed = rng() < 0.5;
|
||||||
|
return { pts, closed, edgeIndex: Math.floor(rng() * (closed ? n : n - 1)), t: rng() };
|
||||||
|
});
|
||||||
|
const wSap = JSON.parse(K.split_polyline_at_param_batch_json(JSON.stringify(qs))) as Vec2[][][];
|
||||||
|
const wRem = JSON.parse(
|
||||||
|
K.remove_segment_batch_json(
|
||||||
|
JSON.stringify(qs.map((q) => ({ pts: q.pts, closed: q.closed, edgeIndex: q.edgeIndex }))),
|
||||||
|
),
|
||||||
|
) as Poly[];
|
||||||
|
const chords = Array.from({ length: N }, () => {
|
||||||
|
const pts = genPolyline(rng, 4, 8);
|
||||||
|
const n = pts.length;
|
||||||
|
const i = Math.floor(rng() * n);
|
||||||
|
let j = Math.floor(rng() * n);
|
||||||
|
if (j === i) j = (j + 1) % n;
|
||||||
|
return { pts, i, ti: rng(), j, tj: rng() };
|
||||||
|
});
|
||||||
|
const wChord = JSON.parse(K.split_closed_by_chord_batch_json(JSON.stringify(chords))) as ([Vec2[], Vec2[]] | null)[];
|
||||||
|
qs.forEach((q, i) => {
|
||||||
|
expect(eqPtsLists(wSap[i], splitPolylineAtParam(q.pts, q.closed, q.edgeIndex, q.t)), `sap#${i}`).toBe(true);
|
||||||
|
const tr = removeSegment(q.pts, q.closed, q.edgeIndex);
|
||||||
|
expect(wRem[i].closed === tr.closed && eqPts(wRem[i].pts, tr.pts), `rem#${i}`).toBe(true);
|
||||||
|
});
|
||||||
|
chords.forEach((q, i) => {
|
||||||
|
const t = splitClosedByChord(q.pts, q.i, q.ti, q.j, q.tj);
|
||||||
|
expect((wChord[i] === null) === (t === null), `chord-null#${i}`).toBe(true);
|
||||||
|
if (t && wChord[i]) {
|
||||||
|
expect(eqPts(wChord[i]![0], t[0]) && eqPts(wChord[i]![1], t[1]), `chord#${i}`).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extendSegment (start/end, null-Paritaet + Werte)", () => {
|
||||||
|
const rng = mulberry32(12);
|
||||||
|
const qs = Array.from({ length: N }, (_, k) => {
|
||||||
|
const c = { x: (rng() * 2 - 1) * 30, y: (rng() * 2 - 1) * 30 };
|
||||||
|
return {
|
||||||
|
a1: { x: c.x - 1, y: c.y },
|
||||||
|
a2: { x: c.x + 1, y: c.y },
|
||||||
|
end: (k % 2 === 0 ? "end" : "start") as "start" | "end",
|
||||||
|
cutters: genCutters(rng).concat([
|
||||||
|
{ pts: [{ x: c.x + 5, y: -50 }, { x: c.x + 5, y: 50 }], closed: false },
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const w = JSON.parse(K.extend_segment_batch_json(JSON.stringify(qs))) as ([Vec2, Vec2] | null)[];
|
||||||
|
qs.forEach((q, i) => {
|
||||||
|
const t = extendSegment(q.a1, q.a2, q.end, q.cutters);
|
||||||
|
expect((w[i] === null) === (t === null), `ext-null#${i}`).toBe(true);
|
||||||
|
if (t && w[i]) expect(closeVec(w[i]![0], t[0]) && closeVec(w[i]![1], t[1]), `ext#${i}`).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("joinChains (mergeable + Zufall, Struktur exakt)", () => {
|
||||||
|
const rng = mulberry32(13);
|
||||||
|
const groups = Array.from({ length: N }, () => {
|
||||||
|
const base = genPolyline(rng, 4, 7);
|
||||||
|
const chains: Poly[] = [];
|
||||||
|
for (let k = 0; k < base.length - 1; k++) {
|
||||||
|
const seg = [base[k], base[k + 1]];
|
||||||
|
chains.push({ pts: rng() < 0.5 ? seg : [seg[1], seg[0]], closed: false });
|
||||||
|
}
|
||||||
|
if (rng() < 0.5) chains.push({ pts: genPolyline(rng, 2, 3), closed: false });
|
||||||
|
for (let k = chains.length - 1; k > 0; k--) {
|
||||||
|
const j = Math.floor(rng() * (k + 1));
|
||||||
|
[chains[k], chains[j]] = [chains[j], chains[k]];
|
||||||
|
}
|
||||||
|
return chains;
|
||||||
|
});
|
||||||
|
const w = JSON.parse(K.join_chains_batch_json(JSON.stringify(groups))) as Poly[][];
|
||||||
|
groups.forEach((g, i) => {
|
||||||
|
expect(eqPolyList(w[i], joinChains(g)), `join#${i}`).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Golden (Grenzfaelle)", () => {
|
describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Golden (Grenzfaelle)", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user