diff --git a/crates/kernel/src/native.rs b/crates/kernel/src/native.rs index f7d06860df..d491d19eb2 100644 --- a/crates/kernel/src/native.rs +++ b/crates/kernel/src/native.rs @@ -156,9 +156,22 @@ pub(crate) fn smlib_record_ordered( /// Where SMLib's faces and edges of `brep` (imported from `record`) are in the record's order: /// `None` where SMLib's export does not number them as the record does. +/// +/// SMLib's export is in canonical order, and `order` says where SMLib's own face and edge `i` +/// went in it. Where the canonical record numbers them as `record` does, `order` maps SMLib's +/// to the record's. Otherwise SMLib may still keep the record's own order (an import lists +/// SMLib's faces and edges in the record's order, and canonical order can list them otherwise, +/// a tube's seam edges among them): the export put back in SMLib's order then numbers them as +/// `record` does, and SMLib's face and edge `i` are the record's `i`. fn numbering(record: &solidcraft_geom::brep::Brep, brep: &solidcraft_smlib::Brep) -> Option { match brep.to_native_ordered() { - Ok((back, order)) => same_numbering(record, &back).then_some(order), + Ok((back, order)) => { + if same_numbering(record, &back) { + return Some(order); + } + let in_smlib_order = in_smlib_order(&back, &order)?; + same_numbering(record, &in_smlib_order).then(solidcraft_smlib::history::Order::default) + } Err(e) => { log::debug!(target: "smlib", "no order for an operand: {e}"); None @@ -166,6 +179,50 @@ fn numbering(record: &solidcraft_geom::brep::Brep, brep: &solidcraft_smlib::Brep } } +/// The canonical record `back` with its faces and edges in SMLib's own order (`order`: where +/// SMLib's face and edge `i` are in `back`), each loop naming its edges by SMLib's numbers. +/// `None` where `order` is not a renumbering of `back`'s faces and edges (an edge split into +/// pieces where solids touch along it is more than one record edge). +fn in_smlib_order(back: &solidcraft_geom::brep::Brep, order: &solidcraft_smlib::history::Order) -> Option { + use solidcraft_geom::brep::Loop; + let permutation = |to: &[usize], n: usize| -> Option> { + if to.is_empty() { + return Some((0..n).collect()); + } + let mut from = vec![usize::MAX; n]; + for (i, &k) in to.iter().enumerate() { + let slot = from.get_mut(k)?; + if *slot != usize::MAX { + return None; + } + *slot = i; + } + (to.len() == n).then_some(from) + }; + if !order.pieces.is_empty() { + return None; + } + // `faces_from[k]`: SMLib's number of `back`'s face k; likewise for edges. + let faces_from = permutation(&order.faces, back.faces.len())?; + let edges_from = permutation(&order.edges, back.edges.len())?; + let mut out = back.clone(); + for (k, face) in back.faces.iter().enumerate() { + let mut face = face.clone(); + for l in &mut face.loops { + if let Loop::Edges { edges } = l { + for e in edges.iter_mut() { + e.edge = u32::try_from(*edges_from.get(usize::try_from(e.edge).ok()?)?).ok()?; + } + } + } + *out.faces.get_mut(*faces_from.get(k)?)? = face; + } + for (k, edge) in back.edges.iter().enumerate() { + *out.edges.get_mut(*edges_from.get(k)?)? = edge.clone(); + } + Some(out) +} + /// Whether two records of one body number its faces, edges and vertices alike: the same loops of /// the same edges, the same edge ends, and each vertex where the other's is. fn same_numbering(a: &solidcraft_geom::brep::Brep, b: &solidcraft_geom::brep::Brep) -> bool { diff --git a/crates/kernel/src/step_in/tests.rs b/crates/kernel/src/step_in/tests.rs index ee8aff7d14..99049a2c40 100644 --- a/crates/kernel/src/step_in/tests.rs +++ b/crates/kernel/src/step_in/tests.rs @@ -1759,9 +1759,9 @@ fn periodic_knot_tube(first: u64, x: f64, r: f64, h: f64) -> (String, u64) { /// Two such tubes far apart in one body: the body's size sets a chord tolerance coarse beside /// the tubes, and meshing a face at it divided the surface's whole parameter box, whose /// corners lay outside the domain. The division never converged there (#1374's recursion), -/// and the import grew by gigabytes without end. SMLib does not give the sheet's faces back in -/// the file's order, so the import refuses it with that reason (it once came in as a mesh of -/// its faces); the faces as read are meshed at the body's display tolerance, the coarse one. +/// and the import grew by gigabytes without end. The sheet comes in as SMLib's B-rep of its two +/// faces, each the file's face (SMLib lists the seam edges in another canonical order than the +/// file, and keeps the file's own order), each a cylinder's side. #[test] fn periodic_knot_tube_faces_import_and_mesh() { let t0 = std::time::Instant::now(); @@ -1771,13 +1771,18 @@ fn periodic_knot_tube_faces_import_and_mesh() { let text = format!( "{HEAD}{a}{b}#1=OPEN_SHELL('',(#{fa},#{fb}));\n#2=SHELL_BASED_SURFACE_MODEL('Tubes',(#1));\n#3=SHAPE_REPRESENTATION('',(#2),$);\n{TAIL}" ); - let why = step_import(&text).map(|imp| imp.warnings).unwrap_err().to_string(); - assert!(why.contains("solid 'Tubes': not imported: ") && why.contains("renumbered"), "{why}"); - let ex = p21::parse(&text).unwrap(); - let sheet = Body::from_record(solidcraft_step::topology::read_solid(&ex, 2, Units::default()).unwrap().brep).unwrap(); - let mesh = sheet.mesh(&crate::MeshTolerance::display_of(sheet.size())).unwrap(); + let imp = step_import(&text).unwrap(); + assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings); + let tubes = &imp.bodies[0]; + assert!(tubes.refused.is_none() && !tubes.body.is_mesh() && tubes.body.is_surface(), "{:?} {:?}", tubes.refused, imp.warnings); + assert_eq!(tubes.face_origin, vec![fa, fb], "{:?}", imp.warnings); + // Each face is the file's quadratic circle of radius r (corner weights √2/2 to 12 digits, + // a radius off by under 1e-12 of r) moved straight along its axis by h: a cylinder's side, + // 2π r h. SMLib integrates to 1e-8. + let m = measure(&tubes.body).unwrap(); let area = 2.0 * std::f64::consts::TAU * r * h; - assert!(rel(mesh.measure().area, area) < 0.02, "area {} (expected {area})", mesh.measure().area); + assert!(rel(m.area, area) < 1e-6, "area {} (expected {area})", m.area); + assert_eq!(tubes.body.unmeshed_faces(), 0); assert!(t0.elapsed().as_secs_f64() < 60.0, "took {:?}", t0.elapsed()); }