mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
kernel: a body SMLib keeps in the record's order is mapped, whatever order its export lists
An imported solid's faces were mapped to the file's only when SMLib's export numbered them as the record did (native.rs, numbering). The export is in canonical order, which can list edges otherwise: the two periodic-knot tubes of #1374 came back with their seam and rim edges listed [2, 0, 1, 5, 3, 4], so the sheet, a valid B-rep, was refused as "SMLib renumbered the solid's faces". The export's order says where SMLib's own face and edge i went; put back in SMLib's order it numbers them as the record does, and SMLib's i is the record's i (an import lists them in the record's order). numbering now takes that too, checked the same way; an edge split into pieces is no renumbering and stays unmapped. The tube test asserts what it should: one surface body, not a mesh, its faces the file's two faces in order, every face meshed, in under a minute, and the area exact: each face is the file's quadratic circle (corner weights sqrt(2)/2 to 12 digits) moved straight along its axis, a cylinder's side, so 2 * 2 pi r h, which SMLib's measure meets to 1e-6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<solidcraft_smlib::history::Order> {
|
||||
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<solidcraft_geom::brep::Brep> {
|
||||
use solidcraft_geom::brep::Loop;
|
||||
let permutation = |to: &[usize], n: usize| -> Option<Vec<usize>> {
|
||||
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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user