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).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// EPS aus kernel2d.ts (Primitive/Schnitt/Trim). ACHTUNG: `lineIntersect`
|
||||
/// 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") --------------------------------------
|
||||
// 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
|
||||
@@ -517,6 +1080,79 @@ struct FilletQuery {
|
||||
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")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
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)
|
||||
}
|
||||
|
||||
#[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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -855,4 +1604,67 @@ mod tests {
|
||||
)
|
||||
.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 {
|
||||
circleCircleIntersect,
|
||||
closestPointOnSegment,
|
||||
extendSegment,
|
||||
filletCorner,
|
||||
isCCW,
|
||||
joinChains,
|
||||
lineCircleIntersect,
|
||||
lineSegmentIntersect,
|
||||
offsetPolyline,
|
||||
offsetSegment,
|
||||
pointSegmentDistance,
|
||||
projectParam,
|
||||
removeSegment,
|
||||
segmentCircleIntersect,
|
||||
segmentIntersect,
|
||||
segmentPolylineHits,
|
||||
signedArea,
|
||||
splitAtIntersections,
|
||||
splitClosedByChord,
|
||||
splitPolylineAtParam,
|
||||
splitSegmentByCutters,
|
||||
trimPolyline,
|
||||
trimSegment,
|
||||
type Fillet,
|
||||
type Hit,
|
||||
} from "./kernel2d";
|
||||
|
||||
type Poly = { pts: Vec2[]; closed: boolean };
|
||||
|
||||
// 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),
|
||||
// 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 {
|
||||
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;
|
||||
|
||||
@@ -287,6 +325,123 @@ describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Zufall", () => {
|
||||
});
|
||||
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)", () => {
|
||||
|
||||
Reference in New Issue
Block a user