Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(kernel): bridge brep geometry to & from step

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (May 30, 2026, 11:45 PM +0300) commit f3567e03 parent 4359e457 change-id nvqsuuqw
+279
+206
crates/bone-kernel/src/brep/step.rs
··· 1 + use std::collections::{HashMap, HashSet}; 2 + 3 + use bone_types::{ 4 + EdgeLabel, EdgeRole, FaceLabel, FaceRole, FeatureId, ImportOrdinal, SolidKey, StepEntityKind, 5 + VertexLabel, VertexRole, 6 + }; 7 + use truck_modeling::{Curve, Shell, Solid, Surface}; 8 + use truck_stepio::r#in::Table; 9 + use truck_stepio::r#in::alias as step_in; 10 + use truck_stepio::r#in::ruststep::ast::DataSection; 11 + use truck_stepio::r#in::ruststep::parser::exchange::data_section; 12 + use truck_stepio::out::StepModels; 13 + use truck_topology::compress::{CompressedEdge, CompressedFace, CompressedShell}; 14 + 15 + use super::build::{SolidLabeling, assemble}; 16 + use super::persist::{ 17 + BrepReattach, content_key, ordered_edges, ordered_faces, ordered_vertices, to_labeling, 18 + }; 19 + use super::{BrepError, BrepSolid}; 20 + 21 + impl BrepSolid { 22 + pub fn to_step_body(&self) -> Result<String, BrepError> { 23 + let compressed = self.arena.solid().compress(); 24 + Ok(StepModels::from_iter([&compressed]).to_string()) 25 + } 26 + 27 + pub fn from_step( 28 + text: &str, 29 + feature: FeatureId, 30 + expected: Option<(SolidKey, &BrepReattach)>, 31 + ) -> Result<BrepSolid, BrepError> { 32 + let solid = parse_step_solid(text)?; 33 + let key = content_key(&solid); 34 + let labeling = match expected { 35 + Some((expected_key, reattach)) if expected_key == key => { 36 + to_labeling(&solid, reattach).unwrap_or_else(|_| imported_labeling(&solid, feature)) 37 + } 38 + _ => imported_labeling(&solid, feature), 39 + }; 40 + assemble(solid, &labeling) 41 + } 42 + } 43 + 44 + fn parse_step_solid(text: &str) -> Result<Solid, BrepError> { 45 + let data = first_data_section(text)?; 46 + let table = Table::from_data_section(&data); 47 + let mut shell_keys: Vec<u64> = table.shell.keys().copied().collect(); 48 + shell_keys.sort_unstable(); 49 + let shells = shell_keys 50 + .iter() 51 + .map(|key| { 52 + let holder = &table.shell[key]; 53 + let compressed = table 54 + .to_compressed_shell(holder) 55 + .map_err(|_| BrepError::StepShellMalformed)?; 56 + let bridged = bridge_shell(compressed)?; 57 + Shell::extract(bridged).map_err(|_| BrepError::StepShellMalformed) 58 + }) 59 + .collect::<Result<Vec<Shell>, BrepError>>()?; 60 + match shells.len() { 61 + 0 => Err(BrepError::StepEmpty), 62 + 1 => Ok(Solid::new_unchecked(shells)), 63 + count => Err(BrepError::StepMultipleShells { count }), 64 + } 65 + } 66 + 67 + fn first_data_section(text: &str) -> Result<DataSection, BrepError> { 68 + text.match_indices("DATA") 69 + .find_map(|(at, _)| data_section(&text[at..]).map(|(_, section)| section).ok()) 70 + .ok_or_else(|| { 71 + if text.contains("DATA") { 72 + BrepError::StepSyntax 73 + } else { 74 + BrepError::StepNoData 75 + } 76 + }) 77 + } 78 + 79 + type StepShell = CompressedShell<truck_modeling::Point3, step_in::Curve3D, step_in::Surface>; 80 + type ModelShell = CompressedShell<truck_modeling::Point3, Curve, Surface>; 81 + 82 + fn bridge_shell(shell: StepShell) -> Result<ModelShell, BrepError> { 83 + let edges = shell 84 + .edges 85 + .into_iter() 86 + .map(|edge| { 87 + Ok(CompressedEdge { 88 + vertices: edge.vertices, 89 + curve: bridge_curve(edge.curve)?, 90 + }) 91 + }) 92 + .collect::<Result<Vec<_>, BrepError>>()?; 93 + let faces = shell 94 + .faces 95 + .into_iter() 96 + .map(|face| { 97 + Ok(CompressedFace { 98 + boundaries: face.boundaries, 99 + orientation: face.orientation, 100 + surface: bridge_surface(face.surface)?, 101 + }) 102 + }) 103 + .collect::<Result<Vec<_>, BrepError>>()?; 104 + Ok(CompressedShell { 105 + vertices: shell.vertices, 106 + edges, 107 + faces, 108 + }) 109 + } 110 + 111 + fn bridge_curve(curve: step_in::Curve3D) -> Result<Curve, BrepError> { 112 + match curve { 113 + step_in::Curve3D::Line(line) => Ok(Curve::Line(line)), 114 + step_in::Curve3D::BSplineCurve(spline) => Ok(Curve::BSplineCurve(spline)), 115 + step_in::Curve3D::NurbsCurve(nurbs) => Ok(Curve::NurbsCurve(nurbs)), 116 + step_in::Curve3D::Conic(_) => Err(unsupported(StepEntityKind::ConicCurve)), 117 + step_in::Curve3D::Polyline(_) => Err(unsupported(StepEntityKind::PolylineCurve)), 118 + step_in::Curve3D::PCurve(_) => Err(unsupported(StepEntityKind::ParametricCurve)), 119 + } 120 + } 121 + 122 + fn bridge_surface(surface: step_in::Surface) -> Result<Surface, BrepError> { 123 + match surface { 124 + step_in::Surface::ElementarySurface(elementary) => match *elementary { 125 + step_in::ElementarySurface::Plane(plane) => Ok(Surface::Plane(plane)), 126 + step_in::ElementarySurface::Sphere(_) => { 127 + Err(unsupported(StepEntityKind::SphericalSurface)) 128 + } 129 + step_in::ElementarySurface::CylindricalSurface(_) => { 130 + Err(unsupported(StepEntityKind::CylindricalSurface)) 131 + } 132 + step_in::ElementarySurface::ToroidalSurface(_) => { 133 + Err(unsupported(StepEntityKind::ToroidalSurface)) 134 + } 135 + step_in::ElementarySurface::ConicalSurface(_) => { 136 + Err(unsupported(StepEntityKind::ConicalSurface)) 137 + } 138 + }, 139 + step_in::Surface::BSplineSurface(spline) => Ok(Surface::BSplineSurface(*spline)), 140 + step_in::Surface::NurbsSurface(nurbs) => Ok(Surface::NurbsSurface(*nurbs)), 141 + step_in::Surface::SweptCurve(_) => Err(unsupported(StepEntityKind::SweptSurface)), 142 + } 143 + } 144 + 145 + fn unsupported(kind: StepEntityKind) -> BrepError { 146 + BrepError::StepUnsupported { kind } 147 + } 148 + 149 + fn imported_labeling(solid: &Solid, feature: FeatureId) -> SolidLabeling { 150 + let faces = ordered_faces(solid) 151 + .into_iter() 152 + .enumerate() 153 + .map(|(index, id)| { 154 + ( 155 + id, 156 + FaceLabel { 157 + feature, 158 + role: FaceRole::Imported { 159 + ordinal: ordinal(index), 160 + }, 161 + }, 162 + ) 163 + }) 164 + .collect(); 165 + let edges = ordered_edges(solid) 166 + .into_iter() 167 + .enumerate() 168 + .map(|(index, id)| { 169 + ( 170 + id, 171 + EdgeLabel { 172 + feature, 173 + role: EdgeRole::Imported { 174 + ordinal: ordinal(index), 175 + }, 176 + }, 177 + ) 178 + }) 179 + .collect(); 180 + let vertices = ordered_vertices(solid) 181 + .into_iter() 182 + .enumerate() 183 + .map(|(index, id)| { 184 + ( 185 + id, 186 + VertexLabel { 187 + feature, 188 + role: VertexRole::Imported { 189 + ordinal: ordinal(index), 190 + }, 191 + }, 192 + ) 193 + }) 194 + .collect(); 195 + SolidLabeling { 196 + faces, 197 + edges, 198 + vertices, 199 + closed_curves: HashSet::new(), 200 + edge_curves: HashMap::new(), 201 + } 202 + } 203 + 204 + fn ordinal(index: usize) -> ImportOrdinal { 205 + ImportOrdinal::new(u32::try_from(index).unwrap_or(u32::MAX)) 206 + }
+73
crates/bone-kernel/tests/step_geometry.rs
··· 1 + use bone_kernel::{BrepError, BrepSolid}; 2 + use bone_types::{FeatureId, StepEntityKind}; 3 + use slotmap::SlotMap; 4 + use truck_modeling::{Point3, Rad, Solid, Vector3, builder}; 5 + use truck_stepio::out::StepModels; 6 + 7 + fn envelope(body: &str) -> String { 8 + format!( 9 + "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\nFILE_NAME('','1970-01-01T00:00:00',(''),(''),'','','');\nFILE_SCHEMA(('AUTOMOTIVE_DESIGN {{ 1 0 10303 214 1 1 1 1 }}'));\nENDSEC;\nDATA;\n{body}ENDSEC;\nEND-ISO-10303-21;\n" 10 + ) 11 + } 12 + 13 + fn feature() -> FeatureId { 14 + let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key(); 15 + features.insert(()) 16 + } 17 + 18 + #[test] 19 + fn analytic_revolved_surface_is_typed_unsupported() { 20 + let v0 = builder::vertex(Point3::new(2.0, 0.0, 0.0)); 21 + let v1 = builder::vertex(Point3::new(3.0, 0.0, 0.0)); 22 + let edge = builder::line(&v0, &v1); 23 + let face = builder::tsweep(&edge, Vector3::new(0.0, 0.0, 1.0)); 24 + let revolved: Solid = builder::rsweep( 25 + &face, 26 + Point3::new(0.0, 0.0, 0.0), 27 + Vector3::unit_z(), 28 + Rad(2.0 * std::f64::consts::PI), 29 + ); 30 + let doc = envelope(&StepModels::from_iter([&revolved.compress()]).to_string()); 31 + assert!(matches!( 32 + BrepSolid::from_step(&doc, feature(), None), 33 + Err(BrepError::StepUnsupported { 34 + kind: StepEntityKind::SweptSurface 35 + }) 36 + )); 37 + } 38 + 39 + #[test] 40 + fn text_without_a_data_section_reports_no_data() { 41 + let doc = "ISO-10303-21;\nHEADER;\nENDSEC;\nEND-ISO-10303-21;\n"; 42 + assert!(matches!( 43 + BrepSolid::from_step(doc, feature(), None), 44 + Err(BrepError::StepNoData) 45 + )); 46 + } 47 + 48 + #[test] 49 + fn unparseable_data_section_reports_syntax() { 50 + let doc = "ISO-10303-21;\nDATA;\nthis is not a record\nENDSEC;\n"; 51 + assert!(matches!( 52 + BrepSolid::from_step(doc, feature(), None), 53 + Err(BrepError::StepSyntax) 54 + )); 55 + } 56 + 57 + #[test] 58 + fn multiple_bodies_are_rejected_not_merged() { 59 + let truck_cube = |origin_x: f64| { 60 + let corner = builder::vertex(Point3::new(origin_x, 0.0, 0.0)); 61 + let edge = builder::tsweep(&corner, Vector3::new(1.0, 0.0, 0.0)); 62 + let face = builder::tsweep(&edge, Vector3::new(0.0, 1.0, 0.0)); 63 + let solid: Solid = builder::tsweep(&face, Vector3::new(0.0, 0.0, 1.0)); 64 + solid.compress() 65 + }; 66 + let first = truck_cube(0.0); 67 + let second = truck_cube(5.0); 68 + let doc = envelope(&StepModels::from_iter([&first, &second]).to_string()); 69 + assert!(matches!( 70 + BrepSolid::from_step(&doc, feature(), None), 71 + Err(BrepError::StepMultipleShells { count: 2 }) 72 + )); 73 + }