fix(render): scale solid tessellation

Derive one drawing-space chordal tolerance so large curves stay smooth while small solids avoid excessive detail.
This commit is contained in:
Hakan Seven 2026-08-12 12:46:17 +03:00
commit ca331fff48
7 changed files with 138 additions and 32 deletions

2
Cargo.lock generated
View file

@ -890,7 +890,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]] [[package]]
name = "cadkernel" name = "cadkernel"
version = "0.1.0" 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 = [ dependencies = [
"acadrust", "acadrust",
"cavalier_contours", "cavalier_contours",

View file

@ -32,7 +32,7 @@ rfd = "0.17"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
env_logger = "0.11" env_logger = "0.11"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "74c5fe9", features = ["serde"] } 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 } acadifc = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "47b5603", optional = true }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1" flate2 = "1"

View file

@ -747,6 +747,33 @@ use crate::scene::model::mesh_model::MeshLodSet;
use crate::scene::convert::solid3d_tess; use crate::scene::convert::solid3d_tess;
use acadrust::{types::Vector3, EntityType}; 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<f64> {
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. /// `point_of_reference` of an ACIS-backed volume entity, if applicable.
pub fn point_of_reference(e: &EntityType) -> Option<&Vector3> { pub fn point_of_reference(e: &EntityType) -> Option<&Vector3> {
match e { match e {
@ -764,13 +791,38 @@ pub fn tessellate_volume(
e: &EntityType, e: &EntityType,
color: [f32; 4], color: [f32; 4],
facet_res: f64, facet_res: f64,
chordal_deflection: Option<f64>,
isolines: usize, isolines: usize,
) -> Option<MeshLodSet> { ) -> Option<MeshLodSet> {
match e { match e {
EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(s, color, facet_res, isolines), EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(
EntityType::Region(r) => solid3d_tess::tessellate_region(r, color, facet_res, isolines), s,
EntityType::Body(b) => solid3d_tess::tessellate_body(b, color, facet_res, isolines), color,
EntityType::Surface(s) => solid3d_tess::tessellate_surface(s, color, facet_res, isolines), 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(_) => { EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) => {
crate::entities::mesh::tessellate_shaded_mesh(e, color) crate::entities::mesh::tessellate_shaded_mesh(e, color)
} }

View file

@ -23,6 +23,7 @@ pub fn tessellate_sat(
name: String, name: String,
color: [f32; 4], color: [f32; 4],
facet_res: f64, facet_res: f64,
chordal_deflection: Option<f64>,
isolines: usize, isolines: usize,
) -> Option<MeshLodSet> { ) -> Option<MeshLodSet> {
let (bodies, loss) = lift(document); let (bodies, loss) = lift(document);
@ -53,7 +54,10 @@ pub fn tessellate_sat(
} else { } else {
1.0 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 // Positions stay f64 until `finalize_mesh` splits them into the coarse
// and fine pair, so a solid at survey coordinates keeps its millimetres. // and fine pair, so a solid at survey coordinates keeps its millimetres.
@ -106,11 +110,14 @@ pub fn tessellate_sat(
DEFAULT_FIT_TOLERANCE DEFAULT_FIT_TOLERANCE
}; };
for (body, placement_scale) in &bodies { for (body, placement_scale) in &bodies {
let tolerance = brep::mesh::TessellationTolerance::new( let mut tolerance = brep::mesh::TessellationTolerance::new(
max_angle, max_angle,
source_fit * placement_scale, source_fit * placement_scale,
) )
.with_isolines(isolines); .with_isolines(isolines);
if let Some(deflection) = chordal_deflection {
tolerance = tolerance.with_chordal_deflection(deflection);
}
let tessellation = brep::mesh::tessellate(body, tolerance); let tessellation = brep::mesh::tessellate(body, tolerance);
undrawn += tessellation.missing_faces.len(); undrawn += tessellation.missing_faces.len();
for face in &tessellation.triangle_faces { for face in &tessellation.triangle_faces {
@ -244,15 +251,4 @@ mod tests {
assert_eq!(moved, [10.0, 20.0, 30.0]); 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));
}
} }

View file

@ -8,9 +8,17 @@ fn tessellate_acis(
name: String, name: String,
color: [f32; 4], color: [f32; 4],
facet_res: f64, facet_res: f64,
chordal_deflection: Option<f64>,
isolines: usize, isolines: usize,
) -> Option<MeshLodSet> { ) -> Option<MeshLodSet> {
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( pub(crate) fn body_transform(
@ -167,10 +175,18 @@ fn finish(
name: String, name: String,
color: [f32; 4], color: [f32; 4],
facet_res: f64, facet_res: f64,
chordal_deflection: Option<f64>,
isolines: usize, isolines: usize,
acis: &acadrust::entities::AcisData, acis: &acadrust::entities::AcisData,
) -> Option<MeshLodSet> { ) -> Option<MeshLodSet> {
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); remap_acis_material_bindings(&mut set, acis);
Some(set) Some(set)
} }
@ -179,6 +195,7 @@ pub fn tessellate_region(
region: &Region, region: &Region,
color: [f32; 4], color: [f32; 4],
facet_res: f64, facet_res: f64,
chordal_deflection: Option<f64>,
isolines: usize, isolines: usize,
) -> Option<MeshLodSet> { ) -> Option<MeshLodSet> {
let sat = parse_acis( let sat = parse_acis(
@ -191,6 +208,7 @@ pub fn tessellate_region(
region.common.handle.value().to_string(), region.common.handle.value().to_string(),
color, color,
facet_res, facet_res,
chordal_deflection,
isolines, isolines,
&region.acis_data, &region.acis_data,
) )
@ -200,6 +218,7 @@ pub fn tessellate_body(
body: &Body, body: &Body,
color: [f32; 4], color: [f32; 4],
facet_res: f64, facet_res: f64,
chordal_deflection: Option<f64>,
isolines: usize, isolines: usize,
) -> Option<MeshLodSet> { ) -> Option<MeshLodSet> {
let sat = parse_acis( let sat = parse_acis(
@ -212,6 +231,7 @@ pub fn tessellate_body(
body.common.handle.value().to_string(), body.common.handle.value().to_string(),
color, color,
facet_res, facet_res,
chordal_deflection,
isolines, isolines,
&body.acis_data, &body.acis_data,
) )
@ -221,6 +241,7 @@ pub fn tessellate_surface(
surface: &acadrust::entities::Surface, surface: &acadrust::entities::Surface,
color: [f32; 4], color: [f32; 4],
facet_res: f64, facet_res: f64,
chordal_deflection: Option<f64>,
isolines: usize, isolines: usize,
) -> Option<MeshLodSet> { ) -> Option<MeshLodSet> {
let sat = parse_acis( let sat = parse_acis(
@ -233,6 +254,7 @@ pub fn tessellate_surface(
surface.common.handle.value().to_string(), surface.common.handle.value().to_string(),
color, color,
facet_res, facet_res,
chordal_deflection,
isolines, isolines,
&surface.acis_data, &surface.acis_data,
) )
@ -242,6 +264,7 @@ pub fn tessellate_solid3d(
solid: &Solid3D, solid: &Solid3D,
color: [f32; 4], color: [f32; 4],
facet_res: f64, facet_res: f64,
chordal_deflection: Option<f64>,
isolines: usize, isolines: usize,
) -> Option<MeshLodSet> { ) -> Option<MeshLodSet> {
let sat = parse_acis( let sat = parse_acis(
@ -254,6 +277,7 @@ pub fn tessellate_solid3d(
solid.common.handle.value().to_string(), solid.common.handle.value().to_string(),
color, color,
facet_res, facet_res,
chordal_deflection,
isolines, isolines,
&solid.acis_data, &solid.acis_data,
) )

View file

@ -136,6 +136,8 @@ impl Scene {
}; };
let image_seed = self.image_seed_for(&entity); let image_seed = self.image_seed_for(&entity);
let facet_res = self.document.header.facet_resolution; 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 isolines = self.document.header.isolines.max(0) as usize;
let mesh_seed = if matches!( let mesh_seed = if matches!(
&entity, &entity,
@ -148,8 +150,14 @@ impl Scene {
| EntityType::PolyfaceMesh(_) | EntityType::PolyfaceMesh(_)
) { ) {
let color = self.render_style(&entity).0; let color = self.render_style(&entity).0;
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines) crate::entities::solid3d::tessellate_volume(
.map(|m| offset_mesh_lod_set(m)) &entity,
color,
facet_res,
chordal_deflection,
isolines,
)
.map(offset_mesh_lod_set)
} else { } else {
None None
}; };
@ -357,6 +365,8 @@ impl Scene {
}; };
let image_seed = self.image_seed_for(&entity); let image_seed = self.image_seed_for(&entity);
let facet_res = self.document.header.facet_resolution; 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 isolines = self.document.header.isolines.max(0) as usize;
let mesh_seed = if matches!( let mesh_seed = if matches!(
&entity, &entity,
@ -369,8 +379,14 @@ impl Scene {
| EntityType::PolyfaceMesh(_) | EntityType::PolyfaceMesh(_)
) { ) {
let color = self.render_style(&entity).0; let color = self.render_style(&entity).0;
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines) crate::entities::solid3d::tessellate_volume(
.map(|m| offset_mesh_lod_set(m)) &entity,
color,
facet_res,
chordal_deflection,
isolines,
)
.map(offset_mesh_lod_set)
} else { } else {
None None
}; };
@ -538,6 +554,8 @@ impl Scene {
}) })
.collect(); .collect();
let facet_res = self.document.header.facet_resolution; 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 isolines = self.document.header.isolines.max(0) as usize;
use crate::par::prelude::*; use crate::par::prelude::*;
let built: Vec<(Handle, MeshLodSet, bool)> = entries let built: Vec<(Handle, MeshLodSet, bool)> = entries
@ -547,6 +565,7 @@ impl Scene {
entity.as_ref(), entity.as_ref(),
color, color,
facet_res, facet_res,
chordal_deflection,
isolines, isolines,
) )
.map(|mut mesh| { .map(|mut mesh| {
@ -1892,6 +1911,8 @@ impl Scene {
use crate::par::prelude::*; use crate::par::prelude::*;
let facet_res = self.document.header.facet_resolution; 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 isolines = self.document.header.isolines.max(0) as usize;
// Top-level solids: offset into the render frame, drawn flat. // Top-level solids: offset into the render frame, drawn flat.
// Block-definition solids: keep block-local coords for per-INSERT // Block-definition solids: keep block-local coords for per-INSERT
@ -1899,7 +1920,14 @@ impl Scene {
let built: Vec<(Handle, MeshLodSet, bool)> = entries let built: Vec<(Handle, MeshLodSet, bool)> = entries
.into_par_iter() .into_par_iter()
.filter_map(|(handle, entity, color, top_level)| { .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( let material = crate::scene::model::material_model::resolve_material_with_base(
&self.document, &self.document,
&entity, &entity,

View file

@ -763,12 +763,12 @@ fn build_derived_caches_impl(
}) })
.collect(); .collect();
// meshes (parallel tessellation). FACETRES (header.facet_resolution) // FACETRES biases one shared chordal tolerance for all solids.
// scales the per-LOD segment counts so users with finer drawings get
// smoother solids; clamped to AutoCAD's [0.01, 10.0] range inside.
// Top-level (layout-owned) solids are offset into the render frame; block // Top-level (layout-owned) solids are offset into the render frame; block
// definition solids keep block-local coords for per-INSERT instancing. (#123) // definition solids keep block-local coords for per-INSERT instancing. (#123)
let facet_res = doc.header.facet_resolution; 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; let isolines = doc.header.isolines.max(0) as usize;
// Real layout blocks come from the Layout objects' block_record handles — // Real layout blocks come from the Layout objects' block_record handles —
// `BlockRecord::is_layout()` is unreliable here (it flags ordinary blocks). // `BlockRecord::is_layout()` is unreliable here (it flags ordinary blocks).
@ -796,8 +796,14 @@ fn build_derived_caches_impl(
material_base_dir, material_base_dir,
); );
let top_level = layout_blocks.contains(&e.common().owner_handle); let top_level = layout_blocks.contains(&e.common().owner_handle);
let result = crate::entities::solid3d::tessellate_volume(e, color, facet_res, isolines) let result = crate::entities::solid3d::tessellate_volume(
.map(|mut mesh| { e,
color,
facet_res,
chordal_deflection,
isolines,
)
.map(|mut mesh| {
material.apply_to_with_face_overrides( material.apply_to_with_face_overrides(
&mut mesh, &mut mesh,
doc, doc,
@ -810,7 +816,7 @@ fn build_derived_caches_impl(
); );
let mesh = if top_level { offset_mesh_lod_set(mesh) } else { mesh }; let mesh = if top_level { offset_mesh_lod_set(mesh) } else { mesh };
(handle, mesh, top_level) (handle, mesh, top_level)
}); });
let done = detail_done.fetch_add(1, Ordering::Relaxed) + 1; let done = detail_done.fetch_add(1, Ordering::Relaxed) + 1;
if done & 0xff == 0 || done == detail_total { if done & 0xff == 0 || done == detail_total {
report_detail(done); report_detail(done);