diff --git a/src/app/automation.rs b/src/app/automation.rs index 13584bd87..64968ab11 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -213,14 +213,8 @@ fn entity_json(e: &acadrust::EntityType, detail: &str) -> Value { _ => {} } if detail == "full" { - let bounds = e.as_entity().bounding_box(); - map.insert( - "bounds".into(), - json!({ - "min":[bounds.min.x,bounds.min.y,bounds.min.z], - "max":[bounds.max.x,bounds.max.y,bounds.max.z] - }), - ); + let (min, max) = crate::scene::convert::tess::entity_bounds(e); + map.insert("bounds".into(), json!({ "min": min, "max": max })); if let Ok(Value::Object(wrapper)) = serde_json::to_value(e) { if let Some((_, properties)) = wrapper.into_iter().next() { map.insert("properties".into(), properties); @@ -654,11 +648,11 @@ impl OpenCADStudio { continue; } if let Some(bounds) = bounds { - let entity_bounds = e.as_entity().bounding_box(); - if entity_bounds.max.x < bounds[0] - || entity_bounds.max.y < bounds[1] - || entity_bounds.min.x > bounds[2] - || entity_bounds.min.y > bounds[3] + let (min, max) = crate::scene::convert::tess::entity_bounds(e); + if max[0] < bounds[0] + || max[1] < bounds[1] + || min[0] > bounds[2] + || min[1] > bounds[3] { continue; } diff --git a/src/app/control/mod.rs b/src/app/control/mod.rs index d6b67e7f2..797f40365 100644 --- a/src/app/control/mod.rs +++ b/src/app/control/mod.rs @@ -968,7 +968,7 @@ impl OpenCADStudio { let Some(entity) = tab.scene.document.get_entity(handle) else { continue; }; - let bb = entity.as_entity().bounding_box(); + let (min, max) = crate::scene::convert::tess::entity_bounds(entity); let metrics = tab .scene .meshes @@ -994,7 +994,7 @@ impl OpenCADStudio { "area":planar.curve.is_closed().then(|| planar.curve.enclosed_area().abs()) }) }); - out.push(json!({"handle":format!("{:X}",handle.value()),"type":crate::entities::names::ui_name(entity),"bounds":{"min":[bb.min.x,bb.min.y,bb.min.z],"max":[bb.max.x,bb.max.y,bb.max.z]},"curve":curve,"mesh":metrics.map(|m|json!({"vertices":m.metrics.vertices,"triangles":m.metrics.triangles,"surface_area":m.metrics.surface_area,"volume":m.metrics.volume,"centroid":m.metrics.centroid,"moment_of_inertia":m.metrics.moment_of_inertia,"principal_directions":m.metrics.principal_directions,"principal_moments":m.metrics.principal_moments,"product_of_inertia":m.metrics.product_of_inertia,"radii_of_gyration":m.metrics.radii_of_gyration}))})); + out.push(json!({"handle":format!("{:X}",handle.value()),"type":crate::entities::names::ui_name(entity),"bounds":{"min":min,"max":max},"curve":curve,"mesh":metrics.map(|m|json!({"vertices":m.metrics.vertices,"triangles":m.metrics.triangles,"surface_area":m.metrics.surface_area,"volume":m.metrics.volume,"centroid":m.metrics.centroid,"moment_of_inertia":m.metrics.moment_of_inertia,"principal_directions":m.metrics.principal_directions,"principal_moments":m.metrics.principal_moments,"product_of_inertia":m.metrics.product_of_inertia,"radii_of_gyration":m.metrics.radii_of_gyration}))})); } json!({"ok":true,"document_id":tab.id,"geometry_revision":tab.scene.geometry_epoch,"measurements":out}) } diff --git a/src/entities/solid3d.rs b/src/entities/solid3d.rs index 8cf4f1924..139906102 100644 --- a/src/entities/solid3d.rs +++ b/src/entities/solid3d.rs @@ -260,17 +260,26 @@ fn position_section(prefix: &str, p: &acadrust::types::Vector3) -> PropSection { } } -/// Approximate a region's enclosed area and boundary perimeter from its -/// wireframe loops. Perimeter is the total edge length across every wire. -/// Area accumulates the Newell area vector of each loop (opposite-wound -/// holes subtract) and halves its magnitude — exact for a single planar -/// loop, approximate for multi-loop or curved regions. Returns zeros when -/// there is nothing to measure. -/// The area and perimeter of a region's boundary wires. -/// -/// Measured in space rather than in projection: a region need not lie in a -/// coordinate plane, and flattening it to XY first would report its shadow. -fn region_area_perimeter(wires: &[acadrust::entities::Wire]) -> (f64, f64) { +/// Measure decoded planar curves in a unit frame, retaining legacy wire-only input. +fn region_area_perimeter(region: &Region) -> (f64, f64) { + let exact = (|| { + let (plane, loops, true) = crate::scene::model::presspull_model::profile_geometry( + &EntityType::Region(region.clone()), + )? else { return None; }; + let unit = cadkernel::space::Plane::orthonormal(plane.origin, plane.x_axis, plane.normal()?)?; + let transform = cadkernel::geom2d::Transform { + origin: unit.project(plane.origin)?.into(), + x_axis: unit.project_vector(plane.x_axis)?.into(), + y_axis: unit.project_vector(plane.y_axis)?.into(), + }; + let curves = loops.iter().flatten().map(|curve| curve.transformed(&transform)) + .collect::>>()?; + let area = curves.iter().map(|curve| curve.enclosed_area()).sum::().abs(); + let perimeter = curves.iter().map(|curve| curve.length()).sum::(); + (area.is_finite() && perimeter.is_finite()).then_some((area, perimeter)) + })(); + if let Some(measurements) = exact { return measurements; } + let wires = ®ion.wires; let mut area_vector = [0.0f64; 3]; let mut perimeter = 0.0; for wire in wires { @@ -351,7 +360,7 @@ impl Grippable for Region { impl PropertyEditable for Region { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - let (area, perimeter) = region_area_perimeter(&self.wires); + let (area, perimeter) = region_area_perimeter(self); let mut sections = acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle); sections[0] @@ -912,3 +921,36 @@ pub fn tessellate_volume( _ => None, } } + +#[cfg(test)] +mod tests { + use super::*; + use cadkernel::geom2d::{Curve, Line}; + + fn ring(points: &[[f64; 2]]) -> Vec { + points.iter().copied().zip(points.iter().copied().cycle().skip(1)) + .take(points.len()).map(|(start, end)| Curve::Line(Line { start, end })).collect() + } + + #[test] + fn decoded_region_reports_exact_hole_area_perimeter_and_bounds() { + let loops = vec![ + ring(&[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]), + ring(&[[2.0, 2.0], [2.0, 4.0], [4.0, 4.0], [4.0, 2.0]]), + ]; + let EntityType::Region(mut region) = + crate::scene::model::presspull_model::region_from_loops( + &loops, + crate::command::WorkingPlane::default(), + ).unwrap() + else { unreachable!() }; + region.wires.clear(); + + let (area, perimeter) = region_area_perimeter(®ion); + assert!((area - 96.0).abs() < 1e-8, "{area}"); + assert!((perimeter - 48.0).abs() < 1e-8, "{perimeter}"); + let (min, max) = crate::scene::convert::tess::entity_bounds(&EntityType::Region(region)); + assert_eq!(min, [0.0, 0.0, 0.0]); + assert_eq!(max, [10.0, 10.0, 0.0]); + } +} diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index db78e6930..10ad57487 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -1718,7 +1718,16 @@ pub(crate) fn set_wire_aabb(w: &mut WireModel, entity_box: [f32; 4]) { w.aabb = out; } -fn entity_bounds(e: &acadrust::EntityType) -> ([f64; 3], [f64; 3]) { +pub(crate) fn entity_bounds(e: &acadrust::EntityType) -> ([f64; 3], [f64; 3]) { + if let acadrust::EntityType::Region(region) = e { + if region.wires.is_empty() { + if let Some(bounds) = super::solid3d_tess::kernel_region_body(region) + .and_then(|body| cadkernel::brep::body_bounds(&body)) + { + return (bounds.min, bounds.max); + } + } + } if let acadrust::EntityType::Line(line) = e { if let Some(association) = acadrust::entities::CenterMarkAssociation::read( &line.common.extended_data,