feat(tess): use angular sampling

Route curve, hatch, sweep, loft, surface, edge, and isoline tessellation through the shared kernel policy.
This commit is contained in:
Hakan Seven committed 2026-08-10 12:12:16 +03:00
1 parent f607422c9d
commit 96be8214f9
21 files changed
+158 -767

No files matched your search

+8 -189
View File
@@ -1398,195 +1398,14 @@ impl Scene {
let mut edge_polys: Vec<Vec<[f64; 2]>> = Vec::new();
for edge in &path.edges {
match edge {
BoundaryEdge::Polyline(poly) => {
let verts = &poly.vertices;
let count = verts.len();
if count == 0 {
continue;
}
let seg_count = if poly.is_closed {
count
} else {
count.saturating_sub(1)
};
for i in 0..seg_count {
let v0 = &verts[i];
let v1 = &verts[(i + 1) % count];
let bulge = v0.z;
// Tess in f64 to preserve ~1 cm precision at
// UTM-scale WCS (the f32 path used to produce
// visibly wavy hatch arcs at 1e5+ magnitude).
let arc = if bulge.abs() < 1e-9 {
None
} else {
crate::entities::common::BulgeArc::from_bulge(
[v0.x, v0.y],
[v1.x, v1.y],
bulge,
)
};
let Some(arc) = arc else {
boundary.push(to_xy(v0.x, v0.y));
continue;
};
let segs = convert::tess_util::arc_segments(
arc.radius,
arc.sweep.abs(),
convert::tess_util::fill_chord_tol(arc.radius),
);
for j in 0..segs {
let s = arc.sample(j as f64 / segs as f64);
boundary.push(to_xy(s[0], s[1]));
}
}
if poly.is_closed {
if let Some(&first) = boundary.get(path_start) {
boundary.push(first);
}
}
}
BoundaryEdge::Line(line) => {
edge_polys.push(vec![
to_xy(line.start.x, line.start.y),
to_xy(line.end.x, line.end.y),
]);
}
BoundaryEdge::CircularArc(arc) => {
let (sa, span) = convert::tess_util::arc_signed_span(
arc.start_angle,
arc.end_angle,
arc.counter_clockwise,
);
let segs = convert::tess_util::arc_segments(
arc.radius,
span.abs(),
convert::tess_util::fill_chord_tol(arc.radius),
);
let mut pts = Vec::with_capacity(segs as usize + 1);
for i in 0..=segs {
let t = sa + span * (i as f64 / segs as f64);
pts.push(to_xy(
arc.center.x + arc.radius * t.cos(),
arc.center.y + arc.radius * t.sin(),
));
}
edge_polys.push(pts);
}
BoundaryEdge::EllipticArc(ell) => {
let r_maj = (ell.major_axis_endpoint.x * ell.major_axis_endpoint.x
+ ell.major_axis_endpoint.y * ell.major_axis_endpoint.y)
.sqrt();
let r_min = r_maj * ell.minor_axis_ratio;
let rot = ell
.major_axis_endpoint
.y
.atan2(ell.major_axis_endpoint.x);
let (sa, span) = convert::tess_util::arc_signed_span(
ell.start_angle,
ell.end_angle,
ell.counter_clockwise,
);
let segs = convert::tess_util::arc_segments(
r_maj,
span.abs(),
convert::tess_util::fill_chord_tol(r_maj),
);
let (cr, sr) = (rot.cos(), rot.sin());
let mut pts = Vec::with_capacity(segs as usize + 1);
for i in 0..=segs {
let t = sa + span * (i as f64 / segs as f64);
let lx = r_maj * t.cos();
let ly = r_min * t.sin();
pts.push(to_xy(
ell.center.x + lx * cr - ly * sr,
ell.center.y + lx * sr + ly * cr,
));
}
edge_polys.push(pts);
}
BoundaryEdge::Spline(spline) => {
// DXF spline control_points pack (x, y, weight) into
// a Vector3 — the z field is the rational weight, NOT
// a Z coordinate. The legacy code dropped weight and
// sampled with a fixed 16 segments; both bugs
// produced visibly wrong fill regions for spline-
// bounded hatches (especially block-internal ones,
// where boundaries are often spline curves with
// rational weights and short cubic segments).
//
// The kernel's plane NURBS holds the rational and
// the polynomial case alike — weights absent means
// polynomial — and tessellates to a chord tolerance,
// the same one the fill polygon uses for arcs.
let degree = spline.degree.max(0) as usize;
// Rough chord-tolerance: 0.1% of the control-poly
// diagonal so adaptive sampling produces enough
// points to follow the curve without exploding on
// huge splines.
let (mut sp_min_x, mut sp_min_y) = (f64::INFINITY, f64::INFINITY);
let (mut sp_max_x, mut sp_max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
for cp in &spline.control_points {
sp_min_x = sp_min_x.min(cp.x);
sp_min_y = sp_min_y.min(cp.y);
sp_max_x = sp_max_x.max(cp.x);
sp_max_y = sp_max_y.max(cp.y);
}
let diag = ((sp_max_x - sp_min_x).powi(2)
+ (sp_max_y - sp_min_y).powi(2))
.sqrt();
let tol = convert::tess_util::fill_chord_tol(diag.max(1.0));
let mut epts: Vec<[f64; 2]> = Vec::new();
let mut sampled = false;
let points: Vec<[f64; 2]> = spline
.control_points
.iter()
.map(|p| [p.x, p.y])
.collect();
let weights = spline.rational.then(|| {
spline
.control_points
.iter()
.map(|p| if p.z.abs() > 1e-12 { p.z } else { 1.0 })
.collect()
});
if let Some(curve) = acadrust::kernel::geom2d::NurbsCurve::new(
degree,
points,
spline.knots.clone(),
weights,
) {
for point in
acadrust::kernel::geom2d::Curve::Nurbs(curve).tessellate_within(tol)
{
epts.push(to_xy(point[0], point[1]));
}
sampled = epts.len() >= 2;
}
if !sampled {
// Fallback: prefer fit_points (which lie on the
// curve) over control_points (which usually
// don't). A control-point polyline would draw
// the convex-hull silhouette — visibly wrong.
let pts: &[_] = if !spline.fit_points.is_empty() {
&spline.fit_points
} else {
&[]
};
if !pts.is_empty() {
for p in pts {
epts.push(to_xy(p.x, p.y));
}
} else {
for cp in &spline.control_points {
epts.push(to_xy(cp.x, cp.y));
}
}
}
edge_polys.push(epts);
}
if let Some(curve) = crate::entities::hatch::edge_curve(edge) {
edge_polys.push(
curve
.tessellate_angle(acadrust::kernel::tessellation::DEFAULT_ANGLE)
.into_iter()
.map(|point| to_xy(point[0], point[1]))
.collect(),
);
}
}
boundary.extend(chain_path_edges(edge_polys));