From ca331fff48ff4fc81998c2a03fb2687b92c76315 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 12 Aug 2026 12:46:17 +0300 Subject: [PATCH] fix(render): scale solid tessellation Derive one drawing-space chordal tolerance so large curves stay smooth while small solids avoid excessive detail. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/entities/solid3d.rs | 60 ++++++++++++++++++++++++++++--- src/scene/convert/acis_kernel.rs | 22 +++++------- src/scene/convert/solid3d_tess.rs | 28 +++++++++++++-- src/scene/entity.rs | 38 +++++++++++++++++--- src/scene/mod.rs | 18 ++++++---- 7 files changed, 138 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b56f5fdd..f4f4422a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -890,7 +890,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=574cf13#574cf13345c266cce01f4413c2ba54dff936f8ab" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=07f0739#07f0739549892f2ba8b7556932a0f831c4b64052" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index d0dc099a..46bf8594 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "74c5fe9", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "574cf13", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "07f0739", features = ["acis", "offset"] } acadifc = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "47b5603", optional = true } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" diff --git a/src/entities/solid3d.rs b/src/entities/solid3d.rs index 92fa3b4d..e713a61b 100644 --- a/src/entities/solid3d.rs +++ b/src/entities/solid3d.rs @@ -747,6 +747,33 @@ use crate::scene::model::mesh_model::MeshLodSet; use crate::scene::convert::solid3d_tess; use acadrust::{types::Vector3, EntityType}; +const DISPLAY_DEFLECTION_COEFFICIENT: f64 = 2.5e-4; + +/// Shared world-space chord tolerance for solid display. +pub fn display_deflection( + header: &acadrust::document::HeaderVariables, + facet_res: f64, +) -> Option { + let low = header.model_space_extents_min; + let high = header.model_space_extents_max; + let spans = [high.x - low.x, high.y - low.y, high.z - low.z]; + let span = spans + .into_iter() + .filter(|value| value.is_finite() && *value > 0.0) + .fold(0.0, f64::max); + if !span.is_finite() || span <= 0.0 || span > 1.0e16 { + return None; + } + let resolution = if facet_res.is_finite() && facet_res > 0.0 { + facet_res.clamp(0.01, 10.0) + } else { + 1.0 + }; + Some( + (span * DISPLAY_DEFLECTION_COEFFICIENT / resolution.max(1.0).sqrt()).max(1e-9), + ) +} + /// `point_of_reference` of an ACIS-backed volume entity, if applicable. pub fn point_of_reference(e: &EntityType) -> Option<&Vector3> { match e { @@ -764,13 +791,38 @@ pub fn tessellate_volume( e: &EntityType, color: [f32; 4], facet_res: f64, + chordal_deflection: Option, isolines: usize, ) -> Option { match e { - EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(s, color, facet_res, isolines), - EntityType::Region(r) => solid3d_tess::tessellate_region(r, color, facet_res, isolines), - EntityType::Body(b) => solid3d_tess::tessellate_body(b, color, facet_res, isolines), - EntityType::Surface(s) => solid3d_tess::tessellate_surface(s, color, facet_res, isolines), + EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d( + s, + color, + facet_res, + chordal_deflection, + isolines, + ), + EntityType::Region(r) => solid3d_tess::tessellate_region( + r, + color, + facet_res, + chordal_deflection, + isolines, + ), + EntityType::Body(b) => solid3d_tess::tessellate_body( + b, + color, + facet_res, + chordal_deflection, + isolines, + ), + EntityType::Surface(s) => solid3d_tess::tessellate_surface( + s, + color, + facet_res, + chordal_deflection, + isolines, + ), EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) => { crate::entities::mesh::tessellate_shaded_mesh(e, color) } diff --git a/src/scene/convert/acis_kernel.rs b/src/scene/convert/acis_kernel.rs index f4855b2e..5b74c78c 100644 --- a/src/scene/convert/acis_kernel.rs +++ b/src/scene/convert/acis_kernel.rs @@ -23,6 +23,7 @@ pub fn tessellate_sat( name: String, color: [f32; 4], facet_res: f64, + chordal_deflection: Option, isolines: usize, ) -> Option { let (bodies, loss) = lift(document); @@ -53,7 +54,10 @@ pub fn tessellate_sat( } else { 1.0 }; - let max_angle = cadkernel::tessellation::angle_for_resolution(resolution); + let max_angle = chordal_deflection.map_or_else( + || cadkernel::tessellation::angle_for_resolution(resolution), + |_| cadkernel::tessellation::display_angle_for_resolution(resolution), + ); // Positions stay f64 until `finalize_mesh` splits them into the coarse // and fine pair, so a solid at survey coordinates keeps its millimetres. @@ -106,11 +110,14 @@ pub fn tessellate_sat( DEFAULT_FIT_TOLERANCE }; for (body, placement_scale) in &bodies { - let tolerance = brep::mesh::TessellationTolerance::new( + let mut tolerance = brep::mesh::TessellationTolerance::new( max_angle, source_fit * placement_scale, ) .with_isolines(isolines); + if let Some(deflection) = chordal_deflection { + tolerance = tolerance.with_chordal_deflection(deflection); + } let tessellation = brep::mesh::tessellate(body, tolerance); undrawn += tessellation.missing_faces.len(); for face in &tessellation.triangle_faces { @@ -244,15 +251,4 @@ mod tests { assert_eq!(moved, [10.0, 20.0, 30.0]); } - fn sides(max_angle: f64) -> f64 { - std::f64::consts::TAU / max_angle - } - - #[test] - fn a_round_surface_gets_the_same_sides_whatever_its_size() { - let angle = cadkernel::tessellation::DEFAULT_ANGLE; - assert!(sides(angle) > 24.0, "{}", sides(angle)); - assert!(sides(angle) < 96.0, "{}", sides(angle)); - } - } diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index 9098c5cd..d8c24af5 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -8,9 +8,17 @@ fn tessellate_acis( name: String, color: [f32; 4], facet_res: f64, + chordal_deflection: Option, isolines: usize, ) -> Option { - crate::scene::convert::acis_kernel::tessellate_sat(sat, name, color, facet_res, isolines) + crate::scene::convert::acis_kernel::tessellate_sat( + sat, + name, + color, + facet_res, + chordal_deflection, + isolines, + ) } pub(crate) fn body_transform( @@ -167,10 +175,18 @@ fn finish( name: String, color: [f32; 4], facet_res: f64, + chordal_deflection: Option, isolines: usize, acis: &acadrust::entities::AcisData, ) -> Option { - let mut set = tessellate_acis(&sat, name, color, facet_res, isolines)?; + let mut set = tessellate_acis( + &sat, + name, + color, + facet_res, + chordal_deflection, + isolines, + )?; remap_acis_material_bindings(&mut set, acis); Some(set) } @@ -179,6 +195,7 @@ pub fn tessellate_region( region: &Region, color: [f32; 4], facet_res: f64, + chordal_deflection: Option, isolines: usize, ) -> Option { let sat = parse_acis( @@ -191,6 +208,7 @@ pub fn tessellate_region( region.common.handle.value().to_string(), color, facet_res, + chordal_deflection, isolines, ®ion.acis_data, ) @@ -200,6 +218,7 @@ pub fn tessellate_body( body: &Body, color: [f32; 4], facet_res: f64, + chordal_deflection: Option, isolines: usize, ) -> Option { let sat = parse_acis( @@ -212,6 +231,7 @@ pub fn tessellate_body( body.common.handle.value().to_string(), color, facet_res, + chordal_deflection, isolines, &body.acis_data, ) @@ -221,6 +241,7 @@ pub fn tessellate_surface( surface: &acadrust::entities::Surface, color: [f32; 4], facet_res: f64, + chordal_deflection: Option, isolines: usize, ) -> Option { let sat = parse_acis( @@ -233,6 +254,7 @@ pub fn tessellate_surface( surface.common.handle.value().to_string(), color, facet_res, + chordal_deflection, isolines, &surface.acis_data, ) @@ -242,6 +264,7 @@ pub fn tessellate_solid3d( solid: &Solid3D, color: [f32; 4], facet_res: f64, + chordal_deflection: Option, isolines: usize, ) -> Option { let sat = parse_acis( @@ -254,6 +277,7 @@ pub fn tessellate_solid3d( solid.common.handle.value().to_string(), color, facet_res, + chordal_deflection, isolines, &solid.acis_data, ) diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 2e315f17..6ed432eb 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -136,6 +136,8 @@ impl Scene { }; let image_seed = self.image_seed_for(&entity); let facet_res = self.document.header.facet_resolution; + let chordal_deflection = + crate::entities::solid3d::display_deflection(&self.document.header, facet_res); let isolines = self.document.header.isolines.max(0) as usize; let mesh_seed = if matches!( &entity, @@ -148,8 +150,14 @@ impl Scene { | EntityType::PolyfaceMesh(_) ) { let color = self.render_style(&entity).0; - crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines) - .map(|m| offset_mesh_lod_set(m)) + crate::entities::solid3d::tessellate_volume( + &entity, + color, + facet_res, + chordal_deflection, + isolines, + ) + .map(offset_mesh_lod_set) } else { None }; @@ -357,6 +365,8 @@ impl Scene { }; let image_seed = self.image_seed_for(&entity); let facet_res = self.document.header.facet_resolution; + let chordal_deflection = + crate::entities::solid3d::display_deflection(&self.document.header, facet_res); let isolines = self.document.header.isolines.max(0) as usize; let mesh_seed = if matches!( &entity, @@ -369,8 +379,14 @@ impl Scene { | EntityType::PolyfaceMesh(_) ) { let color = self.render_style(&entity).0; - crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines) - .map(|m| offset_mesh_lod_set(m)) + crate::entities::solid3d::tessellate_volume( + &entity, + color, + facet_res, + chordal_deflection, + isolines, + ) + .map(offset_mesh_lod_set) } else { None }; @@ -538,6 +554,8 @@ impl Scene { }) .collect(); let facet_res = self.document.header.facet_resolution; + let chordal_deflection = + crate::entities::solid3d::display_deflection(&self.document.header, facet_res); let isolines = self.document.header.isolines.max(0) as usize; use crate::par::prelude::*; let built: Vec<(Handle, MeshLodSet, bool)> = entries @@ -547,6 +565,7 @@ impl Scene { entity.as_ref(), color, facet_res, + chordal_deflection, isolines, ) .map(|mut mesh| { @@ -1892,6 +1911,8 @@ impl Scene { use crate::par::prelude::*; let facet_res = self.document.header.facet_resolution; + let chordal_deflection = + crate::entities::solid3d::display_deflection(&self.document.header, facet_res); let isolines = self.document.header.isolines.max(0) as usize; // Top-level solids: offset into the render frame, drawn flat. // Block-definition solids: keep block-local coords for per-INSERT @@ -1899,7 +1920,14 @@ impl Scene { let built: Vec<(Handle, MeshLodSet, bool)> = entries .into_par_iter() .filter_map(|(handle, entity, color, top_level)| { - crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines).map(|mut mesh| { + crate::entities::solid3d::tessellate_volume( + &entity, + color, + facet_res, + chordal_deflection, + isolines, + ) + .map(|mut mesh| { let material = crate::scene::model::material_model::resolve_material_with_base( &self.document, &entity, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 966c6cd1..120a10a6 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -763,12 +763,12 @@ fn build_derived_caches_impl( }) .collect(); - // meshes (parallel tessellation). FACETRES (header.facet_resolution) - // scales the per-LOD segment counts so users with finer drawings get - // smoother solids; clamped to AutoCAD's [0.01, 10.0] range inside. + // FACETRES biases one shared chordal tolerance for all solids. // Top-level (layout-owned) solids are offset into the render frame; block // definition solids keep block-local coords for per-INSERT instancing. (#123) let facet_res = doc.header.facet_resolution; + let chordal_deflection = + crate::entities::solid3d::display_deflection(&doc.header, facet_res); let isolines = doc.header.isolines.max(0) as usize; // Real layout blocks come from the Layout objects' block_record handles — // `BlockRecord::is_layout()` is unreliable here (it flags ordinary blocks). @@ -796,8 +796,14 @@ fn build_derived_caches_impl( material_base_dir, ); let top_level = layout_blocks.contains(&e.common().owner_handle); - let result = crate::entities::solid3d::tessellate_volume(e, color, facet_res, isolines) - .map(|mut mesh| { + let result = crate::entities::solid3d::tessellate_volume( + e, + color, + facet_res, + chordal_deflection, + isolines, + ) + .map(|mut mesh| { material.apply_to_with_face_overrides( &mut mesh, doc, @@ -810,7 +816,7 @@ fn build_derived_caches_impl( ); let mesh = if top_level { offset_mesh_lod_set(mesh) } else { mesh }; (handle, mesh, top_level) - }); + }); let done = detail_done.fetch_add(1, Ordering::Relaxed) + 1; if done & 0xff == 0 || done == detail_total { report_detail(done);