diff --git a/Cargo.lock b/Cargo.lock index e60125ac4..c4d17a78c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,7 +6,7 @@ version = 4 name = "OpenCADStudio" version = "0.9.4" dependencies = [ - "acadifc 0.5.0 (git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=6059bae)", + "acadifc", "ashpd", "bincode", "bytemuck", @@ -70,25 +70,11 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadifc" version = "0.5.0" -source = "git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=6059bae#6059baed781c6399d8259aa85c19be803ba95205" +source = "git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=c15cbf0#c15cbf0ac5c61202f1d9b1a5d344a03bb1e65871" dependencies = [ - "acadrust 0.4.1 (git+https://github.com/HakanSeven12/cadcodec.git?rev=36e841f)", + "acadrust", "base64", - "cadkernel 0.1.0 (git+https://github.com/HakanSeven12/cadkernel.git?rev=6f34deb)", - "serde", - "serde_json", - "sha2 0.10.9", - "thiserror 1.0.69", -] - -[[package]] -name = "acadifc" -version = "0.5.0" -source = "git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=c65d396#c65d396abc3defcf96b1d71a3f812c8ad993e77c" -dependencies = [ - "acadrust 0.4.1 (git+https://github.com/HakanSeven12/cadcodec.git?rev=d645c7f)", - "base64", - "cadkernel 0.1.0 (git+https://github.com/HakanSeven12/cadkernel.git)", + "cadkernel", "serde", "serde_json", "sha2 0.10.9", @@ -118,29 +104,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "acadrust" -version = "0.4.1" -source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=d645c7f#d645c7f526c199c02fcab7272d6b6f2d1c6b91ae" -dependencies = [ - "ahash 0.8.12", - "anyhow", - "bitflags 2.13.1", - "byteorder", - "encoding_rs", - "flate2", - "indexmap", - "itoa", - "memmap2", - "nom 7.1.3", - "once_cell", - "rayon", - "ryu", - "serde", - "thiserror 1.0.69", - "web-time", -] - [[package]] name = "adler2" version = "2.0.1" @@ -927,16 +890,11 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6f34deb#6f34deb57eedf0dc5908bf225917f19c9707682d" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=691c477#691c47756f30bb32fad2342afeec14d5985783ac" dependencies = [ "cavalier_contours", ] -[[package]] -name = "cadkernel" -version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git#860b9df7e5fc4a495334a6329560a4cde33659db" - [[package]] name = "calloop" version = "0.13.0" @@ -3788,7 +3746,7 @@ dependencies = [ name = "ocs_plugin_api" version = "0.1.0" dependencies = [ - "acadifc 0.5.0 (git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=6059bae)", + "acadifc", "bincode", "getrandom 0.2.17", "interprocess", @@ -3803,7 +3761,7 @@ dependencies = [ name = "ocs_web_worker" version = "0.1.0" dependencies = [ - "acadifc 0.5.0 (git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=c65d396)", + "acadifc", "bincode", "console_error_panic_hook", "getrandom 0.3.4", diff --git a/Cargo.toml b/Cargo.toml index ef60849ea..d3382c904 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ env_logger = "0.11" # The CAD stack is reached through acadifc, which re-exports the codec and # the geometry kernel. Aliased to `acadrust` so existing `use acadrust::…` # paths keep resolving. -acadrust = { package = "acadifc", git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "6059bae", features = ["serde", "offset"] } +acadrust = { package = "acadifc", git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "c15cbf0", features = ["serde", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index a2d3b544e..f58c08c7b 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -9,7 +9,7 @@ license = "GPL-3.0-only" # Pulled in only by the `host` feature, which adds the `acadrust`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -acadrust = { package = "acadifc", git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "6059bae", optional = true, features = ["serde"] } +acadrust = { package = "acadifc", git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "c15cbf0", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index 03dda7e9f..7c366ab85 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -acadrust = { package = "acadifc", git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "c65d396", features = ["serde"] } +acadrust = { package = "acadifc", git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "c15cbf0", features = ["serde"] } bincode = "1.3" console_error_panic_hook = "0.1" getrandom = { version = "0.3", features = ["wasm_js"] } diff --git a/src/entities/common.rs b/src/entities/common.rs index 1a7339c31..f17bbe8b2 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -670,14 +670,7 @@ pub(crate) fn thick_band_tube( (fill_tris, lines) } -/// Build a continuous WCS point list + a per-point FULL band width for a -/// tapered wide polyline, so the wire shader can interpolate each segment's two -/// endpoint widths. Each `verts` entry is `(location_xy, bulge_to_next, -/// start_width, end_width)` — the effective full widths at that vertex's segment -/// start and end (already resolved against the polyline's constant width). Arcs -/// are sampled in 16 steps with the width interpolated linearly along the arc. -/// A shared vertex is emitted once (carrying the previous segment's end width), -/// which is exact for the usual continuous taper. +/// Build a continuous WCS point list and full width for a tapered polyline. pub(crate) fn tapered_band_points( verts: &[([f64; 2], f64, f64, f64)], is_closed: bool, @@ -701,9 +694,10 @@ pub(crate) fn tapered_band_points( if bulge.abs() < 1e-9 { push(p1[0], p1[1], ew0 as f32); } else if let Some(arc) = BulgeArc::from_bulge(p0, p1, bulge) { - for j in 1..=16usize { - let t = j as f64 / 16.0; - let s = arc.sample(t); + let samples = arc.tessellate_angle(acadrust::kernel::tessellation::DEFAULT_ANGLE); + let segments = samples.len().saturating_sub(1).max(1); + for (index, s) in samples.into_iter().enumerate().skip(1) { + let t = index as f64 / segments as f64; push(s[0], s[1], (sw0 + (ew0 - sw0) * t) as f32); } } @@ -792,4 +786,3 @@ pub(crate) fn polyline_segment_fill( Some(boundary) } } - diff --git a/src/entities/curve.rs b/src/entities/curve.rs index 7ae3801f3..ec3f877f5 100644 --- a/src/entities/curve.rs +++ b/src/entities/curve.rs @@ -331,53 +331,9 @@ pub fn entity_curve_xy(entity: &EntityType) -> Option { }) } -/// World-space wire points for a curve, cut to the render pass's current -/// chord tolerance. -/// -/// The density comes from how far a chord may sag from the curve, not from a -/// segment count: an arc a metre across and one a kilometre across need -/// different numbers of points to look equally round, and the tolerance the -/// render pass carries is already zoom-adaptive. -/// -/// Everything stays `f64` to the end. The narrowing belongs at the GPU -/// boundary, where the residual is kept as the low half of a double-single -/// pair; casting local coordinates on the way in throws away precision the -/// shader was built to reconstruct. +/// World-space wire points sampled by the kernel's angular policy. pub fn curve_points(curve: &PlanarCurve) -> Vec<[f64; 3]> { - let tolerance = crate::scene::convert::curve_tol::curve_tol_for(curve_size(&curve.curve)); - curve.tessellate_within(tolerance) -} - -/// How big a curve is, for deciding how finely to sample it. -/// -/// A radius where there is one, since that is what the chord-height relation -/// is written in terms of. `0` for the kinds whose sampling density is not a -/// question of curvature — a straight run needs its two ends and nothing in -/// between — which leaves those on the frame's own tolerance. -fn curve_size(curve: &Curve) -> f64 { - match curve { - Curve::Circle(circle) => circle.radius, - Curve::Arc(arc) => arc.radius, - Curve::Ellipse(arc) => arc.ellipse.major_radius, - // A polyline's bulges and a spline's bends are the same question asked - // of a chain, and its own extent is the nearest thing to a radius. - Curve::Polyline(_) | Curve::Nurbs(_) => { - let sampled = curve.tessellate(64.0); - let (mut low, mut high) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]); - for point in &sampled { - for axis in 0..2 { - low[axis] = low[axis].min(point[axis]); - high[axis] = high[axis].max(point[axis]); - } - } - if low[0] > high[0] { - 0.0 - } else { - (high[0] - low[0]).max(high[1] - low[1]) * 0.5 - } - } - Curve::Line(_) | Curve::Ray(_) | Curve::XLine(_) => 0.0, - } + curve.tessellate_angle(acadrust::kernel::tessellation::DEFAULT_ANGLE) } /// The snap candidates an entity's curve offers, in the two channels the diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index aca22680d..36016e359 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -24,11 +24,7 @@ fn to_render(ell: &Ellipse) -> RenderEntity { .map(crate::entities::curve::snap_from) .unwrap_or_default(); - // The points come from the entity's own curve, cut to the render pass's - // chord tolerance. The two fixed counts this replaced — sixteen per half - // for a full ellipse, thirty-two for an arc — faceted visibly on - // anything large, and the local coordinates were built in `f32`, which - // threw away precision the double-single vertex path exists to keep. + // The points come from the entity's own kernel curve and angular policy. // // What does not change is the shape of the object. EXTRUDE, REVOLVE and // SWEEP read their profile out of `Contour` / `Curve` and have no arm for diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index 022fc3f9c..b53513dd2 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -11,7 +11,7 @@ use crate::command::EntityTransform; use crate::entities::common::{center_grip, circle_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro}; use crate::entities::traits::{FallbackTess, Grippable, PropertyEditable, Transformable}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; -use crate::scene::convert::tess_util::{arc_segments, arc_signed_span, wire_chord_tol, FallbackGeometry}; +use crate::scene::convert::tess_util::FallbackGeometry; use crate::scene::model::wire_model::SnapHint; /// The area the hatch's boundary paths enclose. @@ -49,19 +49,20 @@ fn boundary_area(h: &Hatch) -> f64 { } /// A hatch boundary edge as a kernel curve, in the hatch's own OCS. -fn edge_curve(edge: &BoundaryEdge) -> Option { +pub(crate) fn edge_curve(edge: &BoundaryEdge) -> Option { Some(match edge { BoundaryEdge::Line(l) => KernelCurve::Line(KernelLine { start: [l.start.x, l.start.y], end: [l.end.x, l.end.y], }), BoundaryEdge::CircularArc(a) => { - // A clockwise edge is the same arc walked the other way, so the - // stored angles swap rather than the sweep going negative. let (start, end) = if a.counter_clockwise { (a.start_angle, a.end_angle) } else { - (a.end_angle, a.start_angle) + ( + std::f64::consts::TAU - a.end_angle, + std::f64::consts::TAU - a.start_angle, + ) }; KernelCurve::Arc(KernelArc { centre: [a.center.x, a.center.y], @@ -79,7 +80,10 @@ fn edge_curve(edge: &BoundaryEdge) -> Option { let (start, end) = if e.counter_clockwise { (e.start_angle, e.end_angle) } else { - (e.end_angle, e.start_angle) + ( + std::f64::consts::TAU - e.end_angle, + std::f64::consts::TAU - e.start_angle, + ) }; KernelCurve::Ellipse(KernelEllipseArc { ellipse: KernelEllipse { @@ -991,146 +995,41 @@ impl FallbackTess for Hatch { let mut snap_pts: Vec<(Vec3, SnapHint)> = Vec::new(); for path in &self.paths { for edge in &path.edges { + let Some(curve) = edge_curve(edge) else { + continue; + }; + let local = curve + .tessellate_angle(acadrust::kernel::tessellation::DEFAULT_ANGLE); + if local.len() < 2 { + continue; + } + if !pts.is_empty() { + pts.push([f64::NAN; 3]); + } + let world: Vec<[f64; 3]> = local + .into_iter() + .map(|point| to_wcs(point[0], point[1])) + .collect(); match edge { - BoundaryEdge::Polyline(poly) => { - // Hatch-boundary polyline vertices encode bulge in - // `Vector3.z`; straight segments emit just the - // start vertex, bulged segments tessellate the arc - // between v0 → v1. - let verts = &poly.vertices; - let count = verts.len(); - if count == 0 { - continue; - } - // Break the wire between this polyline and whatever - // preceded it — without the separator the renderer - // draws a ghost segment from the previous edge / path - // straight to this polyline's first vertex, which - // shows up as a stray boundary line between hatch - // regions. - if !pts.is_empty() { - pts.push([f64::NAN; 3]); - } - let start_idx = pts.len(); - 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; - 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 { - let p = to_wcs(v0.x, v0.y); - pts.push(p); - key_verts.push(p); - continue; - }; - let segs = arc_segments( - arc.radius, - arc.sweep.abs(), - wire_chord_tol(arc.radius), - ); - for j in 0..segs { - let s = arc.sample(j as f64 / segs as f64); - let p = to_wcs(s[0], s[1]); - pts.push(p); - if j == 0 { - key_verts.push(p); - } - } - } - // Close the loop visually for closed polylines by - // returning to the first emitted point. - if poly.is_closed { - if let Some(first) = pts.get(start_idx).cloned() { - if first[0].is_finite() { - pts.push(first); - } - } - } else if let Some(last) = verts.last() { - let p = to_wcs(last.x, last.y); - pts.push(p); - key_verts.push(p); - } - } - BoundaryEdge::Line(ln) => { - let p0 = to_wcs(ln.start.x, ln.start.y); - let p1 = to_wcs(ln.end.x, ln.end.y); - if !pts.is_empty() { - pts.push([f64::NAN; 3]); - } - pts.push(p0); - pts.push(p1); - key_verts.push(p0); - key_verts.push(p1); - } - BoundaryEdge::CircularArc(arc) => { - let (sa, span) = - arc_signed_span(arc.start_angle, arc.end_angle, arc.counter_clockwise); - let segs = arc_segments(arc.radius, span.abs(), wire_chord_tol(arc.radius)); - if !pts.is_empty() { - pts.push([f64::NAN; 3]); - } - for i in 0..=segs { - let t = sa + span * (i as f64 / segs as f64); - let p = to_wcs( - arc.center.x + arc.radius * t.cos(), - arc.center.y + arc.radius * t.sin(), - ); - pts.push(p); - if i == 0 || i == segs { - key_verts.push(p); - } - } - snap_pts.push(( - snap_at(to_wcs(arc.center.x, arc.center.y)), - SnapHint::Center, - )); - } - 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) = - arc_signed_span(ell.start_angle, ell.end_angle, ell.counter_clockwise); - let segs = arc_segments(r_maj, span.abs(), wire_chord_tol(r_maj)); - if !pts.is_empty() { - pts.push([f64::NAN; 3]); - } - let (cr, sr) = (rot.cos(), rot.sin()); - 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(); - let p = to_wcs( - ell.center.x + lx * cr - ly * sr, - ell.center.y + lx * sr + ly * cr, - ); - pts.push(p); - if i == 0 || i == segs { - key_verts.push(p); - } - } - snap_pts.push(( - snap_at(to_wcs(ell.center.x, ell.center.y)), - SnapHint::Center, - )); - } + BoundaryEdge::Polyline(poly) => key_verts.extend( + poly.vertices + .iter() + .map(|point| to_wcs(point.x, point.y)), + ), + _ => key_verts.extend([world[0], *world.last().unwrap()]), + } + match edge { + BoundaryEdge::CircularArc(arc) => snap_pts.push(( + snap_at(to_wcs(arc.center.x, arc.center.y)), + SnapHint::Center, + )), + BoundaryEdge::EllipticArc(ellipse) => snap_pts.push(( + snap_at(to_wcs(ellipse.center.x, ellipse.center.y)), + SnapHint::Center, + )), _ => {} } + pts.extend(world); } } if pts.is_empty() { diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index f3c1e12ed..be0d5664e 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -308,8 +308,11 @@ fn to_render(pline: &LwPolyline) -> RenderEntity { center: [wcx as f32, wcy as f32, wcz as f32], radius: arc.radius as f32, }); - for j in 1..=16usize { - let s = arc.sample(j as f64 / 16.0); + for s in arc + .tessellate_angle(acadrust::kernel::tessellation::DEFAULT_ANGLE) + .into_iter() + .skip(1) + { let (wx, wy, wz) = to_wcs(s[0], s[1]); path.push([wx, wy, wz]); } @@ -406,8 +409,11 @@ fn to_render(pline: &LwPolyline) -> RenderEntity { } else if let Some(arc) = crate::entities::common::BulgeArc::from_bulge([ox0, oy0], [ox1, oy1], bulge) { - for j in 1..=16usize { - let s = arc.sample(j as f64 / 16.0); + for s in arc + .tessellate_angle(acadrust::kernel::tessellation::DEFAULT_ANGLE) + .into_iter() + .skip(1) + { let (wx, wy, wz) = to_wcs(s[0], s[1]); pts.push([wx, wy, wz]); } diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index 87d912e40..258651407 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -258,8 +258,11 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> RenderEntity { center: [wcx as f32, wcy as f32, wcz as f32], radius: arc.radius as f32, }); - for j in 1..=16usize { - let s = arc.sample(j as f64 / 16.0); + for s in arc + .tessellate_angle(acadrust::kernel::tessellation::DEFAULT_ANGLE) + .into_iter() + .skip(1) + { let (wx, wy, wz) = to_wcs(s[0], s[1]); path.push([wx, wy, wz]); } diff --git a/src/entities/spline.rs b/src/entities/spline.rs index fe95b27c5..ac8fa7893 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -157,25 +157,7 @@ pub(crate) fn measurement_polyline(spl: &Spline) -> Vec<[f64; 3]> { if degree == 0 || degree >= count { return spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect(); } - let mut min = [f64::INFINITY; 3]; - let mut max = [f64::NEG_INFINITY; 3]; - for point in &spl.control_points { - min[0] = min[0].min(point.x); - min[1] = min[1].min(point.y); - min[2] = min[2].min(point.z); - max[0] = max[0].max(point.x); - max[1] = max[1].max(point.y); - max[2] = max[2].max(point.z); - } - let diagonal = ((max[0] - min[0]).powi(2) - + (max[1] - min[1]).powi(2) - + (max[2] - min[2]).powi(2)) - .sqrt(); - let tolerance = crate::scene::convert::tess_util::fill_chord_tol(diagonal.max(1.0)); - - // The kernel's space curve holds the rational and the polynomial case - // alike — weights absent means polynomial — and samples to a chord - // tolerance, refined where the curve bends rather than evenly. + // The kernel's space curve holds rational and polynomial curves alike. let controls: Vec<[f64; 3]> = spl .control_points .iter() @@ -188,7 +170,9 @@ pub(crate) fn measurement_polyline(spl: &Spline) -> Vec<[f64; 3]> { .collect() }); match NurbsCurve3::new(degree, controls, spl.knots.clone(), weights) { - Some(curve) => curve.tessellate_within(tolerance), + Some(curve) => { + curve.tessellate_angle(acadrust::kernel::tessellation::DEFAULT_ANGLE) + } None => spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect(), } } @@ -197,7 +181,6 @@ pub(crate) fn measurement_polyline(spl: &Spline) -> Vec<[f64; 3]> { /// passes through every input point; open ends use reflected phantom points so /// they don't kink, closed curves wrap around. fn catmull_rom_polyline(pts: &[acadrust::types::Vector3], closed: bool) -> Vec<[f64; 3]> { - const STEPS: usize = 16; let n = pts.len(); if n < 2 { return pts.iter().map(|p| [p.x, p.y, p.z]).collect(); @@ -212,7 +195,7 @@ fn catmull_rom_polyline(pts: &[acadrust::types::Vector3], closed: bool) -> Vec<[ [pts[j].x, pts[j].y, pts[j].z] }; let seg_count = if closed { n } else { n - 1 }; - let mut out: Vec<[f64; 3]> = Vec::with_capacity(seg_count * STEPS + 1); + let mut out = Vec::new(); for seg in 0..seg_count { let p1 = get(seg as isize); let p2 = get(seg as isize + 1); @@ -235,15 +218,7 @@ fn catmull_rom_polyline(pts: &[acadrust::types::Vector3], closed: bool) -> Vec<[ } else { get(seg as isize + 2) }; - // Emit t in [0, 1); the final segment also emits t = 1 so the curve - // closes onto the last point (no duplicate shared vertices otherwise). - let last = if seg == seg_count - 1 { - STEPS - } else { - STEPS - 1 - }; - for s in 0..=last { - let t = s as f64 / STEPS as f64; + let point_at = |t: f64| { let (t2, t3) = (t * t, t * t * t); let mut q = [0.0f64; 3]; for k in 0..3 { @@ -253,8 +228,24 @@ fn catmull_rom_polyline(pts: &[acadrust::types::Vector3], closed: bool) -> Vec<[ + (2.0 * p0[k] - 5.0 * p1[k] + 4.0 * p2[k] - p3[k]) * t2 + (-p0[k] + 3.0 * p1[k] - 3.0 * p2[k] + p3[k]) * t3); } - out.push(q); - } + q + }; + let tangent_at = |t: f64| { + let mut q = [0.0; 3]; + for k in 0..3 { + let a = -p0[k] + p2[k]; + let b = 2.0 * p0[k] - 5.0 * p1[k] + 4.0 * p2[k] - p3[k]; + let c = -p0[k] + 3.0 * p1[k] - 3.0 * p2[k] + p3[k]; + q[k] = 0.5 * (a + 2.0 * b * t + 3.0 * c * t * t); + } + q + }; + let sampled = acadrust::kernel::tessellation::sample_curve3_angle( + point_at, + tangent_at, + acadrust::kernel::tessellation::DEFAULT_ANGLE, + ); + out.extend(sampled.into_iter().skip(usize::from(seg > 0))); } out } @@ -340,12 +331,9 @@ fn fit_spline_polyline(spl: &Spline) -> Vec<[f64; 3]> { } // Evaluate each segment as a cubic Hermite (dP/du = slope · h_i). - const STEPS: usize = 32; - let mut out = Vec::with_capacity((n - 1) * STEPS + 1); + let mut out = Vec::new(); for i in 0..n - 1 { - let last = if i == n - 2 { STEPS } else { STEPS - 1 }; - for s in 0..=last { - let u = s as f64 / STEPS as f64; + let point_at = |u: f64| { let (u2, u3) = (u * u, u * u * u); let h00 = 2.0 * u3 - 3.0 * u2 + 1.0; let h10 = u3 - 2.0 * u2 + u; @@ -357,8 +345,30 @@ fn fit_spline_polyline(spl: &Spline) -> Vec<[f64; 3]> { let m1 = slopes[k][i + 1] * h[i]; q[k] = h00 * p[i][k] + h10 * m0 + h01 * p[i + 1][k] + h11 * m1; } - out.push(q); - } + q + }; + let tangent_at = |u: f64| { + let u2 = u * u; + let (h00, h10, h01, h11) = ( + 6.0 * u2 - 6.0 * u, + 3.0 * u2 - 4.0 * u + 1.0, + -6.0 * u2 + 6.0 * u, + 3.0 * u2 - 2.0 * u, + ); + let mut q = [0.0; 3]; + for k in 0..3 { + let m0 = slopes[k][i] * h[i]; + let m1 = slopes[k][i + 1] * h[i]; + q[k] = h00 * p[i][k] + h10 * m0 + h01 * p[i + 1][k] + h11 * m1; + } + q + }; + let sampled = acadrust::kernel::tessellation::sample_curve3_angle( + point_at, + tangent_at, + acadrust::kernel::tessellation::DEFAULT_ANGLE, + ); + out.extend(sampled.into_iter().skip(usize::from(i > 0))); } out } diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 34024dc05..67aa9d110 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -1961,17 +1961,11 @@ fn transform_tangent( } } -/// Radius / coordinate cap above which adaptive curve tessellation will -/// allocate hundreds of millions of points. `parameter_division` samples -/// to a fixed chord tolerance, so a Circle of radius 1e10 already produces -/// tens of millions of points. +/// Coordinate cap for invalid or impractical extents. const SANE_EXTENT: f64 = 1.0e8; fn is_unreasonable_extent(e: &EntityType) -> bool { - // Adaptive curve tessellation also explodes on degenerate primitives - // (radius = 0, axes of length 0): `parameter_division` allocates - // proportional to range/tolerance, which underflows when the curve - // collapses to a point. Drop both ends of the spectrum. + // Drop degenerate primitives and impractical coordinate ranges. match e { EntityType::Circle(c) => c.radius.abs() < 1.0e-9 || c.radius.abs() > SANE_EXTENT, EntityType::Arc(a) => a.radius.abs() < 1.0e-9 || a.radius.abs() > SANE_EXTENT, diff --git a/src/scene/convert/acis_kernel.rs b/src/scene/convert/acis_kernel.rs index 16c12a9cc..2dedcf9c5 100644 --- a/src/scene/convert/acis_kernel.rs +++ b/src/scene/convert/acis_kernel.rs @@ -15,9 +15,6 @@ use acadrust::kernel::brep; use crate::scene::convert::solid3d_tess::{body_transform, finalize_mesh}; use crate::scene::model::mesh_model::{CurvedGen, MeshLodSet}; -/// Relative chord tolerance, resolved once per body. -const CHORD_FRAC: f64 = 0.002; - /// ACIS topology fit tolerance. const TOL: f64 = 1e-6; @@ -56,7 +53,7 @@ pub fn tessellate_sat( } else { 1.0 }; - let frac = CHORD_FRAC / resolution; + let max_angle = acadrust::kernel::tessellation::DEFAULT_ANGLE / resolution; // Positions stay f64 until `finalize_mesh` splits them into the coarse // and fine pair, so a solid at survey coordinates keeps its millimetres. @@ -101,7 +98,7 @@ pub fn tessellate_sat( // parameters leaves a hole, the same as one that never lifted — so both // are counted before calling the mesh whole. let mut undrawn = 0usize; - let tolerance = brep::mesh::TessellationTolerance::relative(frac, TOL) + let tolerance = brep::mesh::TessellationTolerance::new(max_angle, TOL) .with_isolines(isolines); for body in &bodies { let tessellation = brep::mesh::tessellate(body, tolerance); @@ -237,23 +234,15 @@ mod tests { assert_eq!(moved, [10.0, 20.0, 30.0]); } - /// How many sides a circle of `radius` gets at a sag of `frac × radius`. - fn sides(frac: f64) -> f64 { - let step = 2.0 * (1.0 - frac).clamp(-1.0, 1.0).acos(); - std::f64::consts::TAU / step + fn sides(max_angle: f64) -> f64 { + std::f64::consts::TAU / max_angle } #[test] fn a_round_surface_gets_the_same_sides_whatever_its_size() { - // The fault: `facet_res` is a resolution multiplier and was used as a - // sag in world units. At the default that asked for a whole unit of - // departure, so nothing smaller than a metre subdivided at all and a - // pipe came out as coarse as its own rim. - // - // A fraction of the radius carries no unit, so a bolt and a pipeline - // are sampled alike. - assert!(sides(CHORD_FRAC) > 24.0, "{}", sides(CHORD_FRAC)); - assert!(sides(CHORD_FRAC) < 96.0, "{}", sides(CHORD_FRAC)); + let angle = acadrust::kernel::tessellation::DEFAULT_ANGLE; + assert!(sides(angle) > 24.0, "{}", sides(angle)); + assert!(sides(angle) < 96.0, "{}", sides(angle)); } } diff --git a/src/scene/convert/curve_tol.rs b/src/scene/convert/curve_tol.rs deleted file mode 100644 index 250c0912b..000000000 --- a/src/scene/convert/curve_tol.rs +++ /dev/null @@ -1,140 +0,0 @@ -// How finely a curve is sampled, for the whole of one render frame. -// -// Every curve in a drawing is tessellated to a chord height — how far the -// polyline drawn for it may sit from the curve itself — and the right value -// depends on the zoom. At a metre per pixel a millimetre of error is -// invisible; zoomed to a millimetre per pixel it is the whole picture. -// -// The Scene sets this once per frame from `world_per_pixel`, targeting about -// half a pixel. Every tessellation inside that frame reads the same atomic, -// including the ones running on rayon workers, which is why it is a global -// rather than a parameter: threading a tolerance through every entity -// converter's signature would touch every one of them to say the same thing. -// -// Zero means "no frame is being drawn" — a load, a snap, a hit test — and the -// floor is used instead. That is what `BlockCache::build` expects. - -use std::sync::atomic::{AtomicU64, Ordering}; - -/// The finest a curve is ever sampled, in world units, and the value used -/// when no frame has set one. -const CURVE_TOL: f64 = 0.005; - -static CURVE_TOL_BITS: AtomicU64 = AtomicU64::new(0); - -/// Sets the per-frame curve tolerance. `None` — or anything not finite and -/// positive — reverts to the floor. -pub fn set_curve_tol_override(tol: Option) { - let bits = match tol { - Some(value) if value > 0.0 && value.is_finite() => value.to_bits(), - _ => 0, - }; - CURVE_TOL_BITS.store(bits, Ordering::Relaxed); -} - -/// The tolerance to sample a curve of a given size at. -/// -/// A chord tolerance is an absolute length, so a fixed floor carries an -/// assumption about the drawing's units. Five thousandths is a fine sampling -/// for a part measured in millimetres and a coarse one for a building -/// measured in metres — it turns a ten-centimetre pipe into a decagon and a -/// five-centimetre bolt into a heptagon, and zooming in cannot recover them, -/// because the floor is what stops the frame asking for better. -/// -/// Bounded against the curve's own size instead, which carries no unit. The -/// two ends are worth stating as segment counts, since that is what they -/// really are: a circle gets at least about fifty sides and never more than -/// about a hundred and sixty, wherever the camera is and whatever the drawing -/// is measured in. -pub(crate) fn curve_tol_for(size: f64) -> f64 { - if !(size > 0.0) || !size.is_finite() { - return current_curve_tol(); - } - // The frame's own request, before the absolute floor — that floor is the - // very thing being replaced here, and letting it through first would pin - // every curve to it however close the camera came. - let bits = CURVE_TOL_BITS.load(Ordering::Relaxed); - let asked = if bits == 0 { - // Nothing is being drawn — a load, a snap, a hit test. Ask for the - // middle of the range, which is smooth without being extravagant. - size / 1_500.0 - } else { - f64::from_bits(bits) - }; - asked.clamp(size / 5_000.0, size / 500.0) -} - -/// The tolerance to sample at, never below the floor — so zooming a long way -/// in cannot ask for a sampling finer than the baseline quality. -pub(crate) fn current_curve_tol() -> f64 { - let bits = CURVE_TOL_BITS.load(Ordering::Relaxed); - if bits == 0 { - CURVE_TOL - } else { - f64::from_bits(bits).max(CURVE_TOL) - } -} - -/// `Some(tol)` only while a frame's override is in force — that is, while -/// something is being drawn rather than loaded, snapped or hit-tested. Hatch -/// boundaries use it to decide whether zoom-adaptive sampling applies at all. -pub(crate) fn active_curve_tol() -> Option { - let bits = CURVE_TOL_BITS.load(Ordering::Relaxed); - (bits != 0).then(|| f64::from_bits(bits).max(CURVE_TOL)) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// How many chords a circle of `radius` needs at chord height `tol`. - /// - /// From the sagitta: a chord subtending `θ` departs from the arc by - /// `r(1 − cos(θ/2))` in the middle. - fn sides(radius: f64, tol: f64) -> f64 { - let step = 2.0 * (1.0 - tol / radius).clamp(-1.0, 1.0).acos(); - std::f64::consts::TAU / step - } - - #[test] - fn a_circle_gets_the_same_number_of_sides_whatever_it_is_measured_in() { - // The fault: the floor was an absolute length, so the same circle drawn - // in millimetres and in metres came out smooth and faceted. Ten - // centimetres of pipe had ten sides. - set_curve_tol_override(None); - let in_millimetres = sides(100.0, curve_tol_for(100.0)); - let in_metres = sides(0.1, curve_tol_for(0.1)); - assert!( - (in_millimetres - in_metres).abs() < 1.0, - "{in_millimetres} vs {in_metres}" - ); - assert!(in_metres > 40.0, "a circle should not read as a decagon: {in_metres}"); - } - - #[test] - fn zooming_in_buys_detail_and_zooming_out_does_not_cost_it() { - // Between the two bounds the frame decides, so a curve fills in as the - // camera closes on it. - set_curve_tol_override(Some(0.01)); - let far = sides(1.0, curve_tol_for(1.0)); - set_curve_tol_override(Some(0.0001)); - let near = sides(1.0, curve_tol_for(1.0)); - assert!(near > far, "{near} vs {far}"); - // And neither end runs away: a circle is never a polygon and never a - // thousand-sided one that costs more than it shows. - set_curve_tol_override(Some(1e-12)); - assert!(sides(1.0, curve_tol_for(1.0)) < 200.0); - set_curve_tol_override(Some(1e9)); - assert!(sides(1.0, curve_tol_for(1.0)) > 40.0); - set_curve_tol_override(None); - } - - #[test] - fn a_curve_with_no_size_is_left_to_the_frame() { - // A straight run needs its two ends; there is no curvature to sample. - set_curve_tol_override(Some(0.02)); - assert_eq!(curve_tol_for(0.0), current_curve_tol()); - assert_eq!(curve_tol_for(f64::NAN), current_curve_tol()); - set_curve_tol_override(None); - } -} diff --git a/src/scene/convert/mod.rs b/src/scene/convert/mod.rs index 2a4bb17ee..61ba2f246 100644 --- a/src/scene/convert/mod.rs +++ b/src/scene/convert/mod.rs @@ -2,7 +2,6 @@ pub mod acad_to_render; pub mod acis_export; pub mod acis_kernel; pub mod dgn_linestyle; -pub mod curve_tol; pub mod tessellate; pub(crate) mod tess; pub mod proxy_graphics; diff --git a/src/scene/convert/tess_util.rs b/src/scene/convert/tess_util.rs index e60d87fc0..7c8492bb0 100644 --- a/src/scene/convert/tess_util.rs +++ b/src/scene/convert/tess_util.rs @@ -1,9 +1,7 @@ // Shared helpers used by per-entity tessellation impls in `crate::entities` // and the dispatcher in `crate::scene::convert::tessellate`. // -// Anything cross-entity (colour resolution, arc sampling, chord tolerance) -// lives here. Entity-specific helpers (dim format strings, mleader path -// types, etc.) stay with their entity file. +// Cross-entity rendering helpers live here. use acadrust::types::Color as AcadColor; use glam::Vec3; @@ -28,81 +26,6 @@ pub type FallbackGeometry = ( Vec<[f64; 3]>, ); -// ── Arc tessellation helpers ───────────────────────────────────────────── - -/// Convert hatch-boundary arc `(start, end, ccw)` into a -/// `(start, signed_span)` ready for the sampling loop. Matches the -/// legacy `(TAU - sa, TAU - ea)` flip used here for years — direction -/// semantics are preserved on real files. (Wrap-through-2π is a known -/// edge case in that convention; do not "fix" it without a wider audit -/// of how upstream writers emit CW boundary arcs.) -/// -/// VERIFIED against real AutoCAD output (KSR-039 DWG, CW arc edges with -/// line neighbours): the stored angles of a `ccw = false` arc edge are -/// MIRRORED — the true point is `center + r·(cos(TAU-θ), sin(TAU-θ))`. -/// Sampling the mirrored parameter directly (this function's flip) gives -/// exact endpoint continuity with the adjacent edges (Δ = 0.000000), -/// while interpreting them as true angles lands tens of units away. Any -/// code that *produces* CW boundary arcs (mirror transforms, explode) -/// must therefore store mirrored angles, not geometric ones. -pub fn arc_signed_span(start: f64, end: f64, ccw: bool) -> (f64, f64) { - const TAU: f64 = std::f64::consts::TAU; - let (sa, ea) = if ccw { - (start, end) - } else { - (TAU - start, TAU - end) - }; - (sa, ea - sa) -} - -/// Segment count for an arc, targeting `chord_tol_world` chord-height -/// error in world units. Floor 8, cap 512. -/// -/// Two production callers: -/// - hatch fill polygon (built at load / on edit) passes a radius- -/// relative tol via [`fill_chord_tol`] — ~0.1% radius, zoom-free so -/// the polygon stays sharp at extreme zoom-in without re-tessellation. -/// - hatch wire outline (re-tessellated every frame inside the render -/// scope) passes a zoom-adaptive tol via [`wire_chord_tol`] — pulls -/// from the per-frame override set by `Scene::wires_for_block` so -/// far-out arcs collapse to a handful of segments. -pub fn arc_segments(radius: f64, span_abs: f64, chord_tol_world: f64) -> u32 { - arc_segments_floored(radius, span_abs, chord_tol_world, 8) -} - -/// Like [`arc_segments`] but with a caller-chosen segment floor. The 2-D wire -/// wants floor 8 so a circle reads as round; a curved-surface grid patch or a -/// short arc wants a lower floor so a small span isn't forced to 8 steps. Cap -/// stays 512. -pub fn arc_segments_floored(radius: f64, span_abs: f64, chord_tol_world: f64, floor: u32) -> u32 { - if span_abs < 1e-9 || radius < 1e-9 { - return floor.max(1); - } - let tol = chord_tol_world.max(1e-9).min(radius * 0.99); - // θ where r * (1 - cos(θ/2)) = tol → θ = 2 * acos(1 - tol/r). - let max_step = (2.0 * (1.0 - tol / radius).acos()).max(1e-6); - ((span_abs / max_step).ceil() as u32).clamp(floor, 512) -} - -/// Chord tolerance for the load-time fill polygon: 0.1% of radius, -/// floor 1 µm so degenerate radii still produce a workable count. -pub fn fill_chord_tol(radius: f64) -> f64 { - (radius * 0.001).max(1e-6) -} - -/// Chord tolerance for the per-frame wire outline: pulls the active -/// `curve_tol::set_curve_tol_override` value (Scene sets it to -/// `world_per_pixel × 0.5` so curves stay at ~half-pixel chord error at -/// the current zoom). When no override is active (snap / hit-test -/// passes, load-time builds), falls back to [`fill_chord_tol`] so we -/// never under-sample. -pub fn wire_chord_tol(radius: f64) -> f64 { - match crate::scene::convert::curve_tol::active_curve_tol() { - Some(t) => t.min(fill_chord_tol(radius)).max(1e-9), - None => fill_chord_tol(radius), - } -} - // ── Colour helper ────────────────────────────────────────────────────────── /// Convert an acadrust Color (ACI index or true-color) to a GPU RGBA value. diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 4bc0e2542..31c4f87ef 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -4,7 +4,7 @@ // EntityType // ↓ acad_to_render::convert() // RenderEntity { object: RenderObject, snap_pts, tangent_geoms, key_vertices } -// ↓ curve_tol::tessellate_*() +// ↓ // RenderObject::Lines → WireModel // RenderObject::Dot → WireModel (a dot sized in pixels) // RenderObject::Text → WireModel (glyph strokes) + SDF quads diff --git a/src/scene/entity.rs b/src/scene/entity.rs index e1f5bccd7..ae1d71c4e 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1398,195 +1398,14 @@ impl Scene { let mut edge_polys: Vec> = 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)); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index b61a30da7..ce62bf1ec 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -8207,21 +8207,6 @@ impl Scene { let paper = anno_scale_override.is_some(); let blk_cache = self.block_cache_arc_for(annotation_scale_handle, all_visible, style_viewport); let blk_ref: &cache::block_cache::BlockCache = &blk_cache; - // Zoom-adaptive curve sampling for top-level Edge tessellation. Target - // ~0.5 px chord height — far-out arcs that used to emit hundreds of - // segments now collapse to a handful. The guard clears the override - // when this scope exits so off-render tessellation (snap previews, - // hit-test, block_cache rebuild) sees the default. - struct CurveTolGuard; - impl Drop for CurveTolGuard { - fn drop(&mut self) { - crate::scene::convert::curve_tol::set_curve_tol_override(None); - } - } - let _tol_guard = wpp.map(|w| { - crate::scene::convert::curve_tol::set_curve_tol_override(Some((w * 0.5) as f64)); - CurveTolGuard - }); // Per-entity tessellation memo. Same classify/tessellate logic, two // SEPARATE stores so they can't thrash each other: // * culled path (`view_aabb == Some`) → `tess_memo`, guard keyed on the diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index 8e56333cc..e578df458 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -17,16 +17,17 @@ use acadrust::kernel::brep::{self, Body}; use crate::scene::model::mesh_model::{MeshLodSet, MeshModel}; -/// How far a triangle may sit from the surface it lies on. A drawing is -/// measured in millimetres, and a twentieth of one is past what a screen -/// resolves at any sane zoom. -const SAG: f64 = 0.05; - /// What counts as the same point when the kernel checks a body over. const TOL: f64 = 1e-9; fn tessellation(body: &Body) -> brep::mesh::BodyMesh { - brep::mesh::tessellate(body, brep::mesh::TessellationTolerance::new(SAG, TOL)) + brep::mesh::tessellate( + body, + brep::mesh::TessellationTolerance::new( + acadrust::kernel::tessellation::DEFAULT_ANGLE, + TOL, + ), + ) } /// Axis-aligned box from its center and full extents. diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs index ef25826bb..3ddda1860 100644 --- a/src/scene/model/sweep_model.rs +++ b/src/scene/model/sweep_model.rs @@ -112,26 +112,26 @@ use crate::scene::model::mesh_model::{MeshLodSet, MeshModel}; /// SWEEP through the kernel's tolerance-driven mesh API. pub fn swept(profile: &EntityType, path: &EntityType, color: [f32; 4]) -> Option { - let tolerance = crate::scene::convert::curve_tol::current_curve_tol(); + let max_angle = acadrust::kernel::tessellation::DEFAULT_ANGLE; let surface = brep::mesh::sweep_surface( &entity_curve(profile)?, &entity_curve(path)?, - tolerance, + max_angle, )?; - mesh_set(surface, color, tolerance) + mesh_set(surface, color, 1e-9) } /// LOFT through the kernel's tolerance-driven mesh API. pub fn lofted(profiles: &[EntityType], color: [f32; 4]) -> Option { let curves: Vec = profiles.iter().filter_map(entity_curve).collect(); - let tolerance = crate::scene::convert::curve_tol::current_curve_tol(); - mesh_set(brep::mesh::loft_surface(&curves, tolerance)?, color, tolerance) + let max_angle = acadrust::kernel::tessellation::DEFAULT_ANGLE; + mesh_set(brep::mesh::loft_surface(&curves, max_angle)?, color, 1e-9) } fn mesh_set( surface: brep::mesh::SurfaceMesh, color: [f32; 4], - tolerance: f64, + precision: f64, ) -> Option { if surface.mesh.is_empty() { return None; @@ -141,7 +141,7 @@ fn mesh_set( for point in &surface.mesh.positions { push_point(&mut verts, &mut verts_low, *point); } - let silhouette = surface.silhouette_source(tolerance); + let silhouette = surface.silhouette_source(precision); let mut set = MeshLodSet::from_single(MeshModel { name: String::new(), verts,