kernel2d-Port Phase 3: Offset (Miter+1e-9-Fallback) + Fillet
- offsetSegment / offsetPolyline: Gehrung via line_intersect, Fallback auf verschobenen Endpunkt an EXAKT 1e-9 (nicht EPS) — Selbstschnitte ungeheilt wie TS. Dedup der Eingabe innerhalb EPS. - filletCorner + Fillet-Struct (serde camelCase): acos/tan/sin/atan2-Kette, Klemmung cos∈[-1,1], None bei kollinear (<1e-4 / π-θ<1e-4) oder zu kurzem Schenkel. Winkel im Diff-Test abs 1e-7 rad (libm-ULP-Drift), Struktur exakt. - 3 Batch-Fassaden (offset_segment/offset_polyline/fillet_corner) + 4 Unit-Tests. - Harness: Offset (rel 1e-9) + Fillet (halb kontrolliert/halb Zufall, Winkel 1e-7) + Golden (L-Ecke, rechter Winkel, kollinear/zu-gross → null). cargo test 14/14, vitest 242 (3 neu, davon 12 Parity) gruen, tsc sauber.
This commit is contained in:
@@ -300,6 +300,120 @@ pub fn is_ccw(pts: &[Vec2]) -> bool {
|
|||||||
signed_area(pts) > 0.0
|
signed_area(pts) > 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Offset ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Offset einer einzelnen Strecke um `d` (links positiv).
|
||||||
|
pub fn offset_segment(a: Vec2, b: Vec2, d: f64) -> (Vec2, Vec2) {
|
||||||
|
let n = left_normal(normalize(sub(b, a)));
|
||||||
|
let off = scale(n, d);
|
||||||
|
(add(a, off), add(b, off))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offset einer Polylinie um `d` (links positiv) mit GEHRUNG (miter). Bei
|
||||||
|
/// (nahezu) parallelen Nachbarkanten faellt `line_intersect` (Schwelle 1e-9)
|
||||||
|
/// auf den verschobenen Endpunkt zurueck — dieser geometrische Sprung MUSS an
|
||||||
|
/// exakt 1e-9 haengen (nicht EPS). Selbstschnitte werden NICHT geheilt (wie TS).
|
||||||
|
pub fn offset_polyline(pts: &[Vec2], d: f64, closed: bool) -> Vec<Vec2> {
|
||||||
|
// Auf signifikante Kanten reduzieren (Duplikate verwerfen).
|
||||||
|
let mut clean: Vec<Vec2> = Vec::new();
|
||||||
|
for &p in pts {
|
||||||
|
if clean.is_empty() || dist(clean[clean.len() - 1], p) > EPS {
|
||||||
|
clean.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if closed && clean.len() > 1 && dist(clean[0], clean[clean.len() - 1]) <= EPS {
|
||||||
|
clean.pop();
|
||||||
|
}
|
||||||
|
let n = clean.len();
|
||||||
|
if n < 2 {
|
||||||
|
return pts.to_vec();
|
||||||
|
}
|
||||||
|
let edges: Vec<(Vec2, Vec2)> = polyline_edges(&clean, closed)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(a, b)| offset_segment(a, b, d))
|
||||||
|
.collect();
|
||||||
|
if edges.is_empty() {
|
||||||
|
return pts.to_vec();
|
||||||
|
}
|
||||||
|
// Schnitt zweier (verschobener) Kanten als unendliche Geraden; None → fallback.
|
||||||
|
let join = |e1: (Vec2, Vec2), e2: (Vec2, Vec2), fallback: Vec2| -> Vec2 {
|
||||||
|
let d1 = sub(e1.1, e1.0);
|
||||||
|
let d2 = sub(e2.1, e2.0);
|
||||||
|
line_intersect(e1.0, d1, e2.0, d2).unwrap_or(fallback)
|
||||||
|
};
|
||||||
|
let m = edges.len();
|
||||||
|
let mut result: Vec<Vec2> = Vec::new();
|
||||||
|
if !closed {
|
||||||
|
result.push(edges[0].0);
|
||||||
|
for i in 0..m - 1 {
|
||||||
|
result.push(join(edges[i], edges[i + 1], edges[i].1));
|
||||||
|
}
|
||||||
|
result.push(edges[m - 1].1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for i in 0..m {
|
||||||
|
let prev = edges[(i + m - 1) % m];
|
||||||
|
let curr = edges[i];
|
||||||
|
result.push(join(prev, curr, curr.0));
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fillet (Eck-Verrundung) -------------------------------------------------
|
||||||
|
|
||||||
|
/// Ergebnis einer Eck-Verrundung (Port von `Fillet`). serde-camelCase, damit die
|
||||||
|
/// JSON-Keys (`tangentA`/`startAngle` …) exakt der TS-Referenz entsprechen.
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Fillet {
|
||||||
|
pub center: Vec2,
|
||||||
|
pub radius: f64,
|
||||||
|
/// Tangentenpunkt auf dem ersten Schenkel (corner→p1).
|
||||||
|
pub tangent_a: Vec2,
|
||||||
|
/// Tangentenpunkt auf dem zweiten Schenkel (corner→p2).
|
||||||
|
pub tangent_b: Vec2,
|
||||||
|
pub start_angle: f64,
|
||||||
|
pub end_angle: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verrundet die Ecke bei `corner` (Schenkel corner→p1, corner→p2) mit Radius r.
|
||||||
|
/// None bei (nahezu) kollinearen/zu kurzen Schenkeln. Transzendente Kette
|
||||||
|
/// (`acos/tan/sin/atan2`) — libm nativ↔wasm↔JS driftet um letzte ULP, daher im
|
||||||
|
/// Diff-Test Winkel-Epsilon 1e-7 rad (Struktur/None-Entscheidung bleibt exakt).
|
||||||
|
pub fn fillet_corner(corner: Vec2, p1: Vec2, p2: Vec2, r: f64) -> Option<Fillet> {
|
||||||
|
let u1 = normalize(sub(p1, corner));
|
||||||
|
let u2 = normalize(sub(p2, corner));
|
||||||
|
// Klemm-Reihenfolge wie TS `Math.max(-1, Math.min(1, dot))`.
|
||||||
|
let cos_theta = dot(u1, u2).min(1.0).max(-1.0);
|
||||||
|
let theta = cos_theta.acos();
|
||||||
|
if theta < 1e-4 || std::f64::consts::PI - theta < 1e-4 {
|
||||||
|
return None; // kollinear
|
||||||
|
}
|
||||||
|
let tan_half = (theta / 2.0).tan();
|
||||||
|
if tan_half < EPS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let setback = r / tan_half;
|
||||||
|
if setback > len(sub(p1, corner)) + EPS || setback > len(sub(p2, corner)) + EPS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let tangent_a = add(corner, scale(u1, setback));
|
||||||
|
let tangent_b = add(corner, scale(u2, setback));
|
||||||
|
let bis = normalize(add(u1, u2));
|
||||||
|
let center_dist = r / (theta / 2.0).sin();
|
||||||
|
let center = add(corner, scale(bis, center_dist));
|
||||||
|
let start_angle = (tangent_a.y - center.y).atan2(tangent_a.x - center.x);
|
||||||
|
let end_angle = (tangent_b.y - center.y).atan2(tangent_b.x - center.x);
|
||||||
|
Some(Fillet {
|
||||||
|
center,
|
||||||
|
radius: r,
|
||||||
|
tangent_a,
|
||||||
|
tangent_b,
|
||||||
|
start_angle,
|
||||||
|
end_angle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// --- 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
|
||||||
@@ -378,6 +492,31 @@ struct CircleCircleQuery {
|
|||||||
r2: f64,
|
r2: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct OffsetSegQuery {
|
||||||
|
a: Vec2,
|
||||||
|
b: Vec2,
|
||||||
|
d: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct OffsetPolyQuery {
|
||||||
|
pts: Vec<Vec2>,
|
||||||
|
d: f64,
|
||||||
|
closed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct FilletQuery {
|
||||||
|
corner: Vec2,
|
||||||
|
p1: Vec2,
|
||||||
|
p2: Vec2,
|
||||||
|
r: f64,
|
||||||
|
}
|
||||||
|
|
||||||
#[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> {
|
||||||
@@ -501,6 +640,39 @@ pub fn is_ccw_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsVal
|
|||||||
to_js(&out)
|
to_js(&out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn offset_segment_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
let qs: Vec<OffsetSegQuery> = from_js(input_json)?;
|
||||||
|
let out: Vec<(Vec2, Vec2)> = qs.iter().map(|q| offset_segment(q.a, q.b, q.d)).collect();
|
||||||
|
to_js(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn offset_polyline_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
let qs: Vec<OffsetPolyQuery> = from_js(input_json)?;
|
||||||
|
let out: Vec<Vec<Vec2>> = qs
|
||||||
|
.iter()
|
||||||
|
.map(|q| offset_polyline(&q.pts, q.d, q.closed))
|
||||||
|
.collect();
|
||||||
|
to_js(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn fillet_corner_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
let qs: Vec<FilletQuery> = from_js(input_json)?;
|
||||||
|
let out: Vec<Option<Fillet>> = qs
|
||||||
|
.iter()
|
||||||
|
.map(|q| fillet_corner(q.corner, q.p1, q.p2, q.r))
|
||||||
|
.collect();
|
||||||
|
to_js(&out)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -624,4 +796,63 @@ mod tests {
|
|||||||
assert!(polyline_edges(&[], true).is_empty());
|
assert!(polyline_edges(&[], true).is_empty());
|
||||||
assert!(polyline_edges(&[Vec2::new(0.0, 0.0)], true).is_empty());
|
assert!(polyline_edges(&[Vec2::new(0.0, 0.0)], true).is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn offset_segment_left_positive() {
|
||||||
|
// Strecke (0,0)->(1,0), Offset +0.5 → linke Normale (0,1) → y=0.5.
|
||||||
|
let (a, b) = offset_segment(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), 0.5);
|
||||||
|
assert!((a.x).abs() < T && (a.y - 0.5).abs() < T);
|
||||||
|
assert!((b.x - 1.0).abs() < T && (b.y - 0.5).abs() < T);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn offset_polyline_right_angle_miter() {
|
||||||
|
// L-Ecke (0,0)->(1,0)->(1,1), offen, Offset +0.5 (nach innen/links).
|
||||||
|
// Innerer Gehrungspunkt = Schnitt der beiden verschobenen Kanten bei (0.5,0.5).
|
||||||
|
let l = [
|
||||||
|
Vec2::new(0.0, 0.0),
|
||||||
|
Vec2::new(1.0, 0.0),
|
||||||
|
Vec2::new(1.0, 1.0),
|
||||||
|
];
|
||||||
|
let out = offset_polyline(&l, 0.5, false);
|
||||||
|
assert_eq!(out.len(), 3);
|
||||||
|
assert!((out[1].x - 0.5).abs() < 1e-9 && (out[1].y - 0.5).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fillet_right_angle() {
|
||||||
|
// Rechter Winkel bei (0,0), Schenkel entlang +x und +y, r=1.
|
||||||
|
// setback = r/tan(45°) = 1; center auf Winkelhalbierender bei (1,1).
|
||||||
|
let f = fillet_corner(
|
||||||
|
Vec2::new(0.0, 0.0),
|
||||||
|
Vec2::new(5.0, 0.0),
|
||||||
|
Vec2::new(0.0, 5.0),
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!((f.tangent_a.x - 1.0).abs() < 1e-9 && f.tangent_a.y.abs() < 1e-9);
|
||||||
|
assert!(f.tangent_b.x.abs() < 1e-9 && (f.tangent_b.y - 1.0).abs() < 1e-9);
|
||||||
|
assert!((f.center.x - 1.0).abs() < 1e-9 && (f.center.y - 1.0).abs() < 1e-9);
|
||||||
|
assert!((f.radius - 1.0).abs() < T);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fillet_collinear_is_none() {
|
||||||
|
// Gestreckt (180°) → kollinear → None.
|
||||||
|
assert!(fillet_corner(
|
||||||
|
Vec2::new(0.0, 0.0),
|
||||||
|
Vec2::new(1.0, 0.0),
|
||||||
|
Vec2::new(-1.0, 0.0),
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
.is_none());
|
||||||
|
// Zu grosser Radius für die Schenkellänge → None.
|
||||||
|
assert!(fillet_corner(
|
||||||
|
Vec2::new(0.0, 0.0),
|
||||||
|
Vec2::new(0.1, 0.0),
|
||||||
|
Vec2::new(0.0, 0.1),
|
||||||
|
10.0,
|
||||||
|
)
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,19 @@ import type { Vec2 } from "../model/types";
|
|||||||
import {
|
import {
|
||||||
circleCircleIntersect,
|
circleCircleIntersect,
|
||||||
closestPointOnSegment,
|
closestPointOnSegment,
|
||||||
|
filletCorner,
|
||||||
isCCW,
|
isCCW,
|
||||||
lineCircleIntersect,
|
lineCircleIntersect,
|
||||||
lineSegmentIntersect,
|
lineSegmentIntersect,
|
||||||
|
offsetPolyline,
|
||||||
|
offsetSegment,
|
||||||
pointSegmentDistance,
|
pointSegmentDistance,
|
||||||
projectParam,
|
projectParam,
|
||||||
segmentCircleIntersect,
|
segmentCircleIntersect,
|
||||||
segmentIntersect,
|
segmentIntersect,
|
||||||
segmentPolylineHits,
|
segmentPolylineHits,
|
||||||
signedArea,
|
signedArea,
|
||||||
|
type Fillet,
|
||||||
type Hit,
|
type Hit,
|
||||||
} from "./kernel2d";
|
} from "./kernel2d";
|
||||||
|
|
||||||
@@ -215,6 +219,74 @@ describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Zufall", () => {
|
|||||||
expect(wCcw[i], `ccw#${i}`).toBe(isCCW(p));
|
expect(wCcw[i], `ccw#${i}`).toBe(isCCW(p));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("offsetSegment / offsetPolyline (Miter, Struktur + Werte rel 1e-9)", () => {
|
||||||
|
const rng = mulberry32(7);
|
||||||
|
const segs = Array.from({ length: N }, () => ({
|
||||||
|
a: v(rng),
|
||||||
|
b: v(rng),
|
||||||
|
d: (rng() * 2 - 1) * 5,
|
||||||
|
}));
|
||||||
|
const wSeg = JSON.parse(K.offset_segment_batch_json(JSON.stringify(segs))) as [Vec2, Vec2][];
|
||||||
|
segs.forEach((q, i) => {
|
||||||
|
const [a, b] = offsetSegment(q.a, q.b, q.d);
|
||||||
|
expect(closeVec(wSeg[i][0], a) && closeVec(wSeg[i][1], b), `offSeg#${i}`).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const polys = Array.from({ length: N }, () => {
|
||||||
|
const n = 3 + Math.floor(rng() * 10);
|
||||||
|
const pts = Array.from({ length: n }, () => v(rng));
|
||||||
|
return { pts, d: (rng() * 2 - 1) * 5, closed: rng() < 0.5 };
|
||||||
|
});
|
||||||
|
const wPoly = JSON.parse(K.offset_polyline_batch_json(JSON.stringify(polys))) as Vec2[][];
|
||||||
|
polys.forEach((q, i) => {
|
||||||
|
const t = offsetPolyline(q.pts, q.d, q.closed);
|
||||||
|
expect(wPoly[i].length, `offPoly-len#${i}`).toBe(t.length);
|
||||||
|
t.forEach((p, j) => expect(closeVec(wPoly[i][j], p), `offPoly#${i}.${j}`).toBe(true));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filletCorner (Struktur exakt + Werte, Winkel abs 1e-7)", () => {
|
||||||
|
const rng = mulberry32(8);
|
||||||
|
// Halb kontrolliert (garantiert gueltige Verrundung), halb Zufall (None-Paritaet).
|
||||||
|
const qs = Array.from({ length: N }, (_, i) => {
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
const corner = v(rng);
|
||||||
|
const a0 = rng() * Math.PI * 2;
|
||||||
|
const half = 0.3 + rng() * 0.9; // theta = 2*half ∈ [0.6, 2.4] rad (weg von 0/π)
|
||||||
|
const legLen = 3 + rng() * 7;
|
||||||
|
const rMax = legLen * Math.tan(half);
|
||||||
|
const r = 0.1 + rng() * 0.8 * rMax;
|
||||||
|
const dir = (ang: number) => ({ x: Math.cos(ang), y: Math.sin(ang) });
|
||||||
|
const d1 = dir(a0 + half);
|
||||||
|
const d2 = dir(a0 - half);
|
||||||
|
return {
|
||||||
|
corner,
|
||||||
|
p1: { x: corner.x + d1.x * legLen, y: corner.y + d1.y * legLen },
|
||||||
|
p2: { x: corner.x + d2.x * legLen, y: corner.y + d2.y * legLen },
|
||||||
|
r,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { corner: v(rng), p1: v(rng), p2: v(rng), r: 0.1 + rng() * 5 };
|
||||||
|
});
|
||||||
|
const w = JSON.parse(K.fillet_corner_batch_json(JSON.stringify(qs))) as (Fillet | null)[];
|
||||||
|
let valid = 0;
|
||||||
|
qs.forEach((q, i) => {
|
||||||
|
const t = filletCorner(q.corner, q.p1, q.p2, q.r);
|
||||||
|
expect(w[i] === null, `fil-null#${i}`).toBe(t === null);
|
||||||
|
if (t && w[i]) {
|
||||||
|
const f = w[i]!;
|
||||||
|
expect(closeVec(f.center, t.center), `fil-center#${i}`).toBe(true);
|
||||||
|
expect(closeVec(f.tangentA, t.tangentA), `fil-tA#${i}`).toBe(true);
|
||||||
|
expect(closeVec(f.tangentB, t.tangentB), `fil-tB#${i}`).toBe(true);
|
||||||
|
expect(closeNum(f.radius, t.radius), `fil-r#${i}`).toBe(true);
|
||||||
|
expect(Math.abs(f.startAngle - t.startAngle) <= 1e-7, `fil-sa#${i}`).toBe(true);
|
||||||
|
expect(Math.abs(f.endAngle - t.endAngle) <= 1e-7, `fil-ea#${i}`).toBe(true);
|
||||||
|
valid++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(valid, "keine gueltige Verrundung — Test waere aussagelos").toBeGreaterThan(50);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Golden (Grenzfaelle)", () => {
|
describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Golden (Grenzfaelle)", () => {
|
||||||
@@ -259,4 +331,27 @@ describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Golden (Grenzfae
|
|||||||
expect(Math.abs(wArea[0])).toBeLessThan(1e-12);
|
expect(Math.abs(wArea[0])).toBeLessThan(1e-12);
|
||||||
expect(wCcw[0]).toBe(isCCW(polys[0]));
|
expect(wCcw[0]).toBe(isCCW(polys[0]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Offset L-Ecke (Gehrung) und Fillet rechter Winkel / kollinear→null / zu gross→null", () => {
|
||||||
|
const off = [{ pts: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }], d: 0.5, closed: false }];
|
||||||
|
const wOff = JSON.parse(K.offset_polyline_batch_json(JSON.stringify(off))) as Vec2[][];
|
||||||
|
const tOff = offsetPolyline(off[0].pts, off[0].d, off[0].closed);
|
||||||
|
expect(wOff[0].length).toBe(tOff.length);
|
||||||
|
tOff.forEach((p, j) => expect(closeVec(wOff[0][j], p)).toBe(true));
|
||||||
|
|
||||||
|
const fil = [
|
||||||
|
{ corner: { x: 0, y: 0 }, p1: { x: 5, y: 0 }, p2: { x: 0, y: 5 }, r: 1 }, // rechter Winkel
|
||||||
|
{ corner: { x: 0, y: 0 }, p1: { x: 1, y: 0 }, p2: { x: -1, y: 0 }, r: 0.5 }, // kollinear → null
|
||||||
|
{ corner: { x: 0, y: 0 }, p1: { x: 0.1, y: 0 }, p2: { x: 0, y: 0.1 }, r: 10 }, // zu gross → null
|
||||||
|
];
|
||||||
|
const wFil = JSON.parse(K.fillet_corner_batch_json(JSON.stringify(fil))) as (Fillet | null)[];
|
||||||
|
fil.forEach((q, i) => {
|
||||||
|
const t = filletCorner(q.corner, q.p1, q.p2, q.r);
|
||||||
|
expect(wFil[i] === null, `golden-fil#${i}`).toBe(t === null);
|
||||||
|
if (t && wFil[i]) {
|
||||||
|
expect(closeVec(wFil[i]!.center, t.center)).toBe(true);
|
||||||
|
expect(closeVec(wFil[i]!.tangentA, t.tangentA)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user