// Grippable + PropertyEditable for Solid3D, Region, Body. // // Shared grips and properties for modeler entities. use codec::entities::{Body, Region, Solid3D, Surface}; use codec::xdata::{ExtendedDataRecord, XDataValue}; use kernel::space::polygon; use crate::t; use crate::command::EntityTransform; use crate::entities::common::{ center_grip, edit_prop as edit, format_area, format_length, parse_f64, ro_prop as ro, }; use crate::entities::traits::{Grippable, PropertyEditable, Transformable}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; /// Shared transform for the ACIS volume entities. Translate / rotate / scale /// delegate to opencadcodec (which composes the move into the solid's ACIS /// placement), and mirror delegates via a reflection transform. Without this /// the entity dispatcher treated solids as non-transformable, so a moved or /// pasted solid stayed at its original ACIS placement. macro_rules! impl_acis_transformable { ($ty:ty) => { impl Transformable for $ty { fn apply_transform(&mut self, t: &EntityTransform) { crate::scene::view::transform::apply_standard_entity_transform(self, t, |e, p1, p2| { let m = crate::scene::view::transform::reflection_about_xy_line(p1, p2); codec::Entity::apply_transform(e, &m); }); } } }; } impl_acis_transformable!(Solid3D); impl_acis_transformable!(Region); impl_acis_transformable!(Body); impl_acis_transformable!(Surface); // ── shared helpers ──────────────────────────────────────────────────────────── fn dvec3(v: &codec::types::Vector3) -> glam::DVec3 { glam::DVec3::new(v.x, v.y, v.z) } fn translate_acis_entity(entity: &mut T, d: glam::DVec3) { codec::Entity::translate( entity, codec::types::Vector3::new(d.x, d.y, d.z), ); } fn yes_no(value: bool) -> &'static str { if value { "Yes" } else { "No" } } pub(crate) const SURFACE_PROPERTIES_APP: &str = "OCS_SURFACE_PROPERTIES"; #[derive(Clone, Copy)] pub(crate) struct SurfacePropertyState { pub isolines: bool, pub maintain_associativity: bool, pub show_associativity: bool, } pub(crate) fn surface_property_state(surface: &Surface) -> SurfacePropertyState { let defaults = SurfacePropertyState { isolines: true, maintain_associativity: surface.history_handle.is_some(), show_associativity: false, }; let Some(record) = surface .common .extended_data .get_record(SURFACE_PROPERTIES_APP) else { return defaults; }; let mut values = record.values.iter().filter_map(|value| match value { XDataValue::Integer16(value) => Some(*value), _ => None, }); let Some(version) = values.next() else { return defaults; }; if version != 1 { return defaults; } SurfacePropertyState { isolines: values.next().map_or(defaults.isolines, |value| value == 0), maintain_associativity: values .next() .map_or(defaults.maintain_associativity, |value| value != 0), show_associativity: values .next() .map_or(defaults.show_associativity, |value| value != 0), } } pub(crate) fn surface_property_xdata_values(state: SurfacePropertyState) -> Vec { vec![ XDataValue::Integer16(1), XDataValue::Integer16(if state.isolines { 0 } else { 1 }), XDataValue::Integer16(state.maintain_associativity as i16), XDataValue::Integer16(state.show_associativity as i16), ] } fn write_surface_property_state(surface: &mut Surface, state: SurfacePropertyState) { let mut record = ExtendedDataRecord::new(SURFACE_PROPERTIES_APP); record.values = surface_property_xdata_values(state); surface.common.extended_data.upsert_record(record); } pub(crate) fn surface_isoline_counts(surface: &Surface) -> [usize; 2] { [ surface.u_isolines.max(0) as usize, surface.v_isolines.max(0) as usize, ] } fn handle_text(handle: Option) -> String { handle .filter(|handle| handle.is_valid()) .map(|handle| format!("{:X}", handle.value())) .unwrap_or_else(|| "None".to_string()) } fn acis_sections( acis: &codec::entities::AcisData, wires: &[codec::entities::Wire], silhouettes: &[codec::entities::Silhouette], history: Option, ) -> Vec { let mut wire_types = [0usize; 5]; let mut transformed = 0usize; let mut points = 0usize; for wire in wires { let index = match wire.wire_type { codec::entities::WireType::Silhouette => 1, codec::entities::WireType::VisibleEdge => 2, codec::entities::WireType::HiddenEdge => 3, codec::entities::WireType::Isoline => 4, _ => 0, }; wire_types[index] += 1; transformed += wire.has_transform as usize; points += wire.points.len(); } let revision = if acis.revision.has_guid { format!( "{}-{}-{}-{:02X?}; end {}", acis.revision.major, acis.revision.minor1, acis.revision.minor2, acis.revision.bytes, acis.revision.end_marker ) } else { "None".to_string() }; let bindings = if acis.materials.is_empty() { "None".to_string() } else { acis.materials .iter() .map(|binding| { format!( "{}:{}→{}", binding.array_index, binding.absolute_reference, handle_text(binding.material_handle) ) }) .collect::>() .join(", ") }; let extra = acis.extra_acis_data.as_ref().map_or_else( || "None".to_string(), |data| { format!( "{}; {} bytes", if data.is_binary { "SAB" } else { "SAT" }, data.size() ) }, ); vec![ PropSection { title: t!("Modeler Geometry").into_owned(), props: vec![ ro(t!("Format").as_ref(), "acis_format", if acis.is_binary { "SAB" } else { "SAT" }, ), ro(t!("Version").as_ref(), "acis_version", format!("{:?}", acis.version)), ro(t!("Data Size").as_ref(), "acis_size", format!("{} bytes", acis.size())), ro(t!("Revision").as_ref(), "acis_revision", revision), ro(t!("Material Bindings").as_ref(), "acis_materials", bindings), ro(t!("Extra Modeler Data").as_ref(), "acis_extra", extra), ro(t!("History").as_ref(), "acis_history", handle_text(history)), ], }, PropSection { title: t!("Modeler Display Data").into_owned(), props: vec![ ro(t!("Wireframe Cache").as_ref(), "acis_wireframe", yes_no(acis.wireframe_data_present), ), ro(t!("Reference Point").as_ref(), "acis_wireframe_point", yes_no(acis.wireframe_point_present), ), ro(t!("Isoline List").as_ref(), "acis_wireframe_isolines", yes_no(acis.wireframe_isoline_present), ), ro(t!("Isolines").as_ref(), "acis_isolines", acis.wireframe_isolines.to_string(), ), ro(t!("Wires").as_ref(), "acis_wires", format!( "{} (unknown {}, silhouette {}, visible {}, hidden {}, isoline {}; {} points; {} transformed)", wires.len(), wire_types[0], wire_types[1], wire_types[2], wire_types[3], wire_types[4], points, transformed ), ), ro(t!("View Silhouettes").as_ref(), "acis_silhouettes", format!( "{} views; {} wires", silhouettes.len(), silhouettes.iter().map(|silhouette| silhouette.wires.len()).sum::() ), ), ], }, ] } fn position_section(prefix: &str, p: &codec::types::Vector3) -> PropSection { let fields = match prefix { "rgn" => ["rgn_px", "rgn_py", "rgn_pz"], "bdy" => ["bdy_px", "bdy_py", "bdy_pz"], "srf" => ["srf_px", "srf_py", "srf_pz"], _ => ["s3d_px", "s3d_py", "s3d_pz"], }; PropSection { title: t!("Geometry").into_owned(), props: vec![ edit(t!("Position X").as_ref(), fields[0], p.x), edit(t!("Position Y").as_ref(), fields[1], p.y), edit(t!("Position Z").as_ref(), fields[2], p.z), ], } } /// 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 = kernel::space::Plane::orthonormal(plane.origin, plane.x_axis, plane.normal()?)?; let transform = kernel::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 { let ring: Vec<[f64; 3]> = wire.points.iter().map(|p| [p.x, p.y, p.z]).collect(); // Wires arrive as open chains that the region closes between them, so // the length is the chain's and the area vectors are summed before // being measured — one wire's contribution is not an area on its own. perimeter += polygon::chain_length(&ring); let piece = polygon::area_vector(&ring); for axis in 0..3 { area_vector[axis] += piece[axis]; } } let area = (area_vector[0].powi(2) + area_vector[1].powi(2) + area_vector[2].powi(2)).sqrt(); (area, perimeter) } // ── Solid3D ─────────────────────────────────────────────────────────────────── impl Grippable for Solid3D { fn grips(&self) -> Vec { vec![center_grip(0, dvec3(&self.point_of_reference))] } fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { if grip_id != 0 { return; } if let GripApply::Translate(d) = apply { translate_acis_entity(self, d); } } } impl PropertyEditable for Solid3D { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { let mut sections = acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle); sections[0] .props .insert(0, ro(t!("UID").as_ref(), "s3d_uid", self.uid.clone())); sections.push(position_section("s3d", &self.point_of_reference)); sections } fn apply_geom_prop(&mut self, field: &str, value: &str) { let Some(v) = parse_f64(value) else { return; }; let delta = match field { "s3d_px" => codec::types::Vector3::new(v - self.point_of_reference.x, 0.0, 0.0), "s3d_py" => codec::types::Vector3::new(0.0, v - self.point_of_reference.y, 0.0), "s3d_pz" => codec::types::Vector3::new(0.0, 0.0, v - self.point_of_reference.z), _ => return, }; if delta != codec::types::Vector3::ZERO { codec::Entity::translate(self, delta); } } } // ── Region ──────────────────────────────────────────────────────────────────── impl Grippable for Region { fn grips(&self) -> Vec { vec![center_grip(0, dvec3(&self.point_of_reference))] } fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { if grip_id != 0 { return; } if let GripApply::Translate(d) = apply { translate_acis_entity(self, d); } } } impl PropertyEditable for Region { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { let (area, perimeter) = region_area_perimeter(self); vec![PropSection { title: t!("Geometry").into_owned(), props: vec![ ro(t!("Area").as_ref(), "rgn_area", format_area(area)), ro( t!("Perimeter").as_ref(), "rgn_perimeter", format_length(perimeter), ), ], }] } fn apply_geom_prop(&mut self, _field: &str, _value: &str) {} } // ── Body ────────────────────────────────────────────────────────────────────── impl Grippable for Body { fn grips(&self) -> Vec { vec![center_grip(0, dvec3(&self.point_of_reference))] } fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { if grip_id != 0 { return; } if let GripApply::Translate(d) = apply { translate_acis_entity(self, d); } } } impl PropertyEditable for Body { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { let mut sections = acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle); sections[0] .props .insert(0, ro(t!("UID").as_ref(), "bdy_uid", self.uid.clone())); sections.push(position_section("bdy", &self.point_of_reference)); sections } fn apply_geom_prop(&mut self, field: &str, value: &str) { let Some(v) = parse_f64(value) else { return; }; let delta = match field { "bdy_px" => codec::types::Vector3::new(v - self.point_of_reference.x, 0.0, 0.0), "bdy_py" => codec::types::Vector3::new(0.0, v - self.point_of_reference.y, 0.0), "bdy_pz" => codec::types::Vector3::new(0.0, 0.0, v - self.point_of_reference.z), _ => return, }; if delta != codec::types::Vector3::ZERO { codec::Entity::translate(self, delta); } } } fn embedded_name(entity: Option<&codec::entities::EmbeddedEntity>) -> &'static str { match entity { Some(codec::entities::EmbeddedEntity::Point(_)) => "Point", Some(codec::entities::EmbeddedEntity::Line(_)) => "Line", Some(codec::entities::EmbeddedEntity::Arc(_)) => "Arc", Some(codec::entities::EmbeddedEntity::Circle(_)) => "Circle", Some(codec::entities::EmbeddedEntity::Ellipse(_)) => "Ellipse", Some(codec::entities::EmbeddedEntity::Spline(_)) => "Spline", Some(codec::entities::EmbeddedEntity::LwPolyline(_)) => "Polyline", Some(codec::entities::EmbeddedEntity::Region(_)) => "Region", Some(codec::entities::EmbeddedEntity::Ray(_)) => "Ray", Some(codec::entities::EmbeddedEntity::XLine(_)) => "XLine", Some(codec::entities::EmbeddedEntity::Unknown { .. }) => "Unknown", None => "None", } } fn vector_text(vector: &codec::types::Vector3) -> String { format!("{:.6}, {:.6}, {:.6}", vector.x, vector.y, vector.z) } fn matrix_text(matrix: &[f64; 16]) -> String { matrix .chunks_exact(4) .map(|row| { format!( "[{:.4}, {:.4}, {:.4}, {:.4}]", row[0], row[1], row[2], row[3] ) }) .collect::>() .join(" ") } fn sweep_options_text(options: &codec::entities::SurfaceSweepOptions) -> String { format!( "draft {:.6} ({:.6}→{:.6}); twist {:.6}; scale {:.6}; align {:.6}; solid {}; flags {}/{}; align-start {}; bank {}; base {}; sweep-xform {}; path-xform {}; ref {}", options.draft_angle, options.draft_start_distance, options.draft_end_distance, options.twist_angle, options.scale_factor, options.align_angle, yes_no(options.is_solid), options.sweep_alignment_flags, options.path_flags, yes_no(options.align_start), yes_no(options.bank), yes_no(options.base_point_set), yes_no(options.sweep_entity_transform_computed), yes_no(options.path_entity_transform_computed), vector_text(&options.reference_vector) ) } fn surface_construction_section(surface: &Surface) -> PropSection { use codec::entities::SurfaceData; let mut props = vec![ ro(t!("Kind").as_ref(), "srf_kind", format!("{:?}", surface.kind)), ro(t!("Modeler Format").as_ref(), "srf_modeler_version", surface.modeler_format_version.to_string(), ), edit(t!("U Isolines").as_ref(), "srf_u_isolines", surface.u_isolines as f64, ), edit(t!("V Isolines").as_ref(), "srf_v_isolines", surface.v_isolines as f64, ), ]; match &surface.surface_data { SurfaceData::Generic => {} SurfaceData::Plane { class_version } => { props.push(ro(t!("Class Version").as_ref(), "srf_class_version", class_version.to_string(), )); } SurfaceData::Extruded { sweep_entity, options, sweep_vector, sweep_transform, } => { props.extend([ ro(t!("Sweep Entity").as_ref(), "srf_sweep_entity", embedded_name(sweep_entity.as_ref()), ), ro(t!("Sweep Vector").as_ref(), "srf_sweep_vector", vector_text(sweep_vector)), ro(t!("Sweep Options").as_ref(), "srf_sweep_options", sweep_options_text(options)), ro(t!("Sweep Transform").as_ref(), "srf_sweep_transform", matrix_text(sweep_transform), ), ro(t!("Sweep Entity Transform").as_ref(), "srf_sweep_entity_transform", matrix_text(&options.sweep_entity_transform), ), ro(t!("Path Entity Transform").as_ref(), "srf_path_entity_transform", matrix_text(&options.path_entity_transform), ), ]); } SurfaceData::Lofted { loft_transform, cross_section_entities, guide_entities, path_entity, plane_normal_lofting_type, start_draft_angle, end_draft_angle, start_draft_magnitude, end_draft_magnitude, arc_length_parameterization, no_twist, align_direction, simple_surfaces, closed_surfaces, solid, ruled_surface, virtual_guide, cross_sections, guide_curves, path_curve, } => { props.extend([ ro(t!("Embedded Curves").as_ref(), "srf_loft_embedded", format!( "{} cross sections; {} guides; path {}", cross_section_entities.len(), guide_entities.len(), embedded_name(path_entity.as_ref()) ), ), ro(t!("Database Curves").as_ref(), "srf_loft_handles", format!( "{} cross sections; {} guides; path {}", cross_sections.len(), guide_curves.len(), handle_text(*path_curve) ), ), ro(t!("Draft").as_ref(), "srf_loft_draft", format!( "type {}; angle {:.6}→{:.6}; magnitude {:.6}→{:.6}", plane_normal_lofting_type, start_draft_angle, end_draft_angle, start_draft_magnitude, end_draft_magnitude ), ), ro(t!("Loft Flags").as_ref(), "srf_loft_flags", format!( "arc-length {}; no-twist {}; align {}; simple {}; closed {}; solid {}; ruled {}; virtual-guide {}", yes_no(*arc_length_parameterization), yes_no(*no_twist), yes_no(*align_direction), yes_no(*simple_surfaces), yes_no(*closed_surfaces), yes_no(*solid), yes_no(*ruled_surface), yes_no(*virtual_guide) ), ), ro(t!("Loft Transform").as_ref(), "srf_loft_transform", matrix_text(loft_transform), ), ]); } SurfaceData::Revolved { revolve_entity, class_version, entity_id, axis_point, axis_vector, revolve_angle, start_angle, entity_transform, draft_angle, draft_start_distance, draft_end_distance, twist_angle, solid, close_to_axis, } => { props.extend([ ro(t!("Revolve Entity").as_ref(), "srf_revolve_entity", embedded_name(revolve_entity.as_ref()), ), ro(t!("Class / Entity").as_ref(), "srf_revolve_ids", format!("{class_version} / {entity_id}"), ), ro(t!("Axis Point").as_ref(), "srf_axis_point", vector_text(axis_point)), ro(t!("Axis Vector").as_ref(), "srf_axis_vector", vector_text(axis_vector)), ro(t!("Angles").as_ref(), "srf_revolve_angles", format!( "start {:.6}; revolve {:.6}; draft {:.6}; twist {:.6}", start_angle, revolve_angle, draft_angle, twist_angle ), ), ro(t!("Draft Distances").as_ref(), "srf_revolve_draft_distances", format!("{draft_start_distance:.6}→{draft_end_distance:.6}"), ), ro(t!("Revolve Flags").as_ref(), "srf_revolve_flags", format!( "solid {}; close-to-axis {}", yes_no(*solid), yes_no(*close_to_axis) ), ), ro(t!("Entity Transform").as_ref(), "srf_revolve_transform", matrix_text(entity_transform), ), ]); } SurfaceData::Swept { class_version, sweep_entity, path_entity, sweep_transform, path_transform, options, } => { props.extend([ ro(t!("Class Version").as_ref(), "srf_class_version", class_version.to_string(), ), ro(t!("Sweep / Path").as_ref(), "srf_swept_entities", format!( "{} / {}", embedded_name(sweep_entity.as_ref()), embedded_name(path_entity.as_ref()) ), ), ro(t!("Sweep Options").as_ref(), "srf_sweep_options", sweep_options_text(options)), ro(t!("Sweep Transform").as_ref(), "srf_sweep_transform", matrix_text(sweep_transform), ), ro(t!("Path Transform").as_ref(), "srf_path_transform", matrix_text(path_transform), ), ro(t!("Sweep Entity Transform").as_ref(), "srf_sweep_entity_transform", matrix_text(&options.sweep_entity_transform), ), ro(t!("Path Entity Transform").as_ref(), "srf_path_entity_transform", matrix_text(&options.path_entity_transform), ), ]); } SurfaceData::Nurb { short_170, cv_hull_display, u_vector1, v_vector1, u_vector2, v_vector2, } => { props.extend([ ro(t!("NURB Version").as_ref(), "srf_nurb_version", short_170.to_string(), ), ro(t!("CV Hull").as_ref(), "srf_nurb_cv_hull", yes_no(*cv_hull_display), ), ro(t!("U Vector 1").as_ref(), "srf_nurb_u1", vector_text(u_vector1)), ro(t!("V Vector 1").as_ref(), "srf_nurb_v1", vector_text(v_vector1)), ro(t!("U Vector 2").as_ref(), "srf_nurb_u2", vector_text(u_vector2)), ro(t!("V Vector 2").as_ref(), "srf_nurb_v2", vector_text(v_vector2)), ]); } } PropSection { title: t!("Surface Construction").into_owned(), props, } } fn translate_surface(surface: &mut Surface, delta: codec::types::Vector3) { let before = surface.point_of_reference; codec::Entity::translate(surface, delta); if surface.point_of_reference == before { surface.point_of_reference = before + delta; } } impl Grippable for Surface { fn grips(&self) -> Vec { vec![center_grip(0, dvec3(&self.point_of_reference))] } fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { if grip_id != 0 { return; } if let GripApply::Translate(delta) = apply { translate_surface( self, codec::types::Vector3::new(delta.x, delta.y, delta.z), ); } } } impl PropertyEditable for Surface { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { let mut sections = acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle); sections.push(surface_construction_section(self)); sections.push(position_section("srf", &self.point_of_reference)); sections } fn apply_geom_prop(&mut self, field: &str, value: &str) { match field { "srf_wireframe_type" => { let mut state = surface_property_state(self); state.isolines = !value.eq_ignore_ascii_case("Isoparms"); write_surface_property_state(self, state); } "srf_maintain_associativity" => { let mut state = surface_property_state(self); state.maintain_associativity = value.eq_ignore_ascii_case("Yes"); write_surface_property_state(self, state); } "srf_show_associativity" => { let mut state = surface_property_state(self); state.show_associativity = value.eq_ignore_ascii_case("Yes"); write_surface_property_state(self, state); } "srf_u_isolines" => { if let Some(value) = parse_f64(value) { self.u_isolines = (value.round() as i16).max(0); } } "srf_v_isolines" => { if let Some(value) = parse_f64(value) { self.v_isolines = (value.round() as i16).max(0); } } _ => { let Some(value) = parse_f64(value) else { return; }; let delta = match field { "srf_px" => codec::types::Vector3::new( value - self.point_of_reference.x, 0.0, 0.0, ), "srf_py" => codec::types::Vector3::new( 0.0, value - self.point_of_reference.y, 0.0, ), "srf_pz" => codec::types::Vector3::new( 0.0, 0.0, value - self.point_of_reference.z, ), _ => return, }; if delta != codec::types::Vector3::ZERO { translate_surface(self, delta); } } } } } // ── Accessors for the Solid3D / Region / Body trio ───────────────────────── // // These entity types share ACIS data and a point of reference. use crate::scene::model::mesh_model::MeshLodSet; use crate::scene::convert::solid3d_tess; use codec::{types::Vector3, EntityType}; const DISPLAY_DEFLECTION_COEFFICIENT: f64 = 2.5e-4; /// Shared world-space chord tolerance for solid display. pub fn display_deflection( header: &codec::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 { EntityType::Solid3D(s) => Some(&s.point_of_reference), EntityType::Region(r) => Some(&r.point_of_reference), EntityType::Body(b) => Some(&b.point_of_reference), EntityType::Surface(s) => Some(&s.point_of_reference), _ => None, } } /// Build material-aware shaded geometry for every standard 3-D solid/surface /// and mesh family, returning `None` when decoded geometry is unusable. 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, 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, surface_isoline_counts(s), surface_property_state(s).isolines, ), EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) => { crate::entities::mesh::tessellate_shaded_mesh(e, color) } _ => None, } } #[cfg(test)] mod tests { use super::*; use kernel::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]); } }