mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
kernel: booleans through SMLib behind SOLIDCRAFT_BOOLEANS, with SMLib's history
With the `smlib` feature, SOLIDCRAFT_BOOLEANS=smlib (read once per process, default truck) sends every kernel::boolean through SMLib: each operand's schema 4 record imported and tagged as operand 0 or 1, SmBrep's boolean, SMLib's check at every level, the result exported and decoded (with its geometry check). A result either check refuses is an error. The setting naming SMLib in a build without it, or anything unknown, fails every boolean instead of quietly running truck, so a comparison never compares truck with itself. The history is SMLib's tags renumbered into Body::history's index spaces. The export now reports where each SMLib face and edge lands in canonical order (its arrays follow SmBrep::GetFaces/GetEdges, the order the tags use). The result's order is checked against the decoded solid by face and edge identity, the operands' by re-exporting the imported body and comparing its numbering with the record's; where either differs the result has no history rather than a wrong one. The result gets the operands' regularization protection and planar roles as truck's does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
//! Which kernel runs [`crate::boolean`]: truck (the default) or SMLib.
|
||||
//!
|
||||
//! The setting is the environment variable `SOLIDCRAFT_BOOLEANS`, read once per process:
|
||||
//! `truck` (or unset) or `smlib`. SMLib needs a build with the kernel's `smlib` feature; set
|
||||
//! without it, or set to anything else, every boolean fails with an error that says so rather
|
||||
//! than quietly running truck, so a comparison run can never compare truck with itself.
|
||||
//!
|
||||
//! Through SMLib a boolean is: each operand's schema 4 record built in SMLib and tagged as
|
||||
//! operand 0 or 1, `SmBrep` boolean, the result exported to schema 4 and decoded as a kernel
|
||||
//! body. The result's history is SMLib's own (its tags carried through the operation),
|
||||
//! renumbered from SMLib's face and edge order into the operands' and the result's ([`History`]'s
|
||||
//! contract). A result SMLib's own check or the record's geometry check refuses is an error,
|
||||
//! never a body.
|
||||
|
||||
use crate::{KernelError, Result};
|
||||
|
||||
/// The environment variable that picks the boolean kernel.
|
||||
pub const BOOLEANS_ENV: &str = "SOLIDCRAFT_BOOLEANS";
|
||||
|
||||
/// A kernel that runs booleans.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum BooleanBackend {
|
||||
Truck,
|
||||
Smlib,
|
||||
}
|
||||
|
||||
impl BooleanBackend {
|
||||
/// The backend a setting names; `None` (unset) is truck.
|
||||
pub fn parse(setting: Option<&str>) -> std::result::Result<BooleanBackend, String> {
|
||||
match setting.map(str::trim) {
|
||||
None | Some("") | Some("truck") => Ok(BooleanBackend::Truck),
|
||||
Some("smlib") if cfg!(feature = "smlib") => Ok(BooleanBackend::Smlib),
|
||||
Some("smlib") => Err(format!("{BOOLEANS_ENV}=smlib: not supported yet: this build has no SMLib (feature `smlib`)")),
|
||||
Some(other) => Err(format!("{BOOLEANS_ENV}={other}: expected `truck` or `smlib`")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The boolean kernel this process uses ([`BOOLEANS_ENV`], read on first use).
|
||||
pub fn boolean_backend() -> Result<BooleanBackend> {
|
||||
static SETTING: std::sync::OnceLock<std::result::Result<BooleanBackend, String>> = std::sync::OnceLock::new();
|
||||
SETTING.get_or_init(|| BooleanBackend::parse(std::env::var(BOOLEANS_ENV).ok().as_deref())).clone().map_err(KernelError::Invalid)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smlib")]
|
||||
pub(crate) use smlib::boolean as smlib_boolean;
|
||||
|
||||
#[cfg(feature = "smlib")]
|
||||
mod smlib {
|
||||
use solidcraft_smlib::{BooleanOp, CheckLevel};
|
||||
|
||||
use crate::body::Body;
|
||||
use crate::history::History;
|
||||
use crate::native::{body_from_smlib_ordered, smlib_body_ordered};
|
||||
use crate::ops::BoolOp;
|
||||
use crate::{KernelError, Result, record};
|
||||
|
||||
fn failed(what: &str, e: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("SMLib boolean: {what}: {e}"))
|
||||
}
|
||||
|
||||
/// `a` and `b` combined by `op` in SMLib, with the history SMLib's tags give (none where a
|
||||
/// body's face and edge order cannot be matched to SMLib's). `None`: the result is empty.
|
||||
pub(crate) fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
a.require_brep("a boolean")?;
|
||||
b.require_brep("a boolean")?;
|
||||
let (sa, oa) = smlib_body_ordered(a)?;
|
||||
let (sb, ob) = smlib_body_ordered(b)?;
|
||||
let ta = sa.tagged(0).map_err(|e| failed("tag", e))?;
|
||||
let tb = sb.tagged(1).map_err(|e| failed("tag", e))?;
|
||||
let kind = match op {
|
||||
BoolOp::Union => BooleanOp::Union,
|
||||
BoolOp::Cut => BooleanOp::Subtract,
|
||||
BoolOp::Intersect => BooleanOp::Intersect,
|
||||
};
|
||||
let Some(r) = ta.boolean(&tb, kind).map_err(|e| failed("operation", e))? else { return Ok(None) };
|
||||
let check = r.check(CheckLevel::All).map_err(|e| failed("check", e))?;
|
||||
if !check.is_valid() {
|
||||
return Err(failed("SMLib's check refuses the result", check));
|
||||
}
|
||||
let (body, or) = body_from_smlib_ordered(&r)?;
|
||||
let history = match (oa, ob, or) {
|
||||
(Some(oa), Some(ob), Some(or)) => r.history().map_err(|e| failed("history", e))?.history(&[oa, ob], &or).map(Some).unwrap_or_else(|e| {
|
||||
log::debug!(target: "smlib", "boolean history: {e}");
|
||||
None
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
Ok(Some(with_history(body, history, a, b)))
|
||||
}
|
||||
|
||||
/// `body` with `history` where it describes `body` made from `a` and `b`.
|
||||
fn with_history(body: Body, history: Option<History>, a: &Body, b: &Body) -> Body {
|
||||
body.with_checked_history(history, &record::counts(&[a, b]))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_setting_names_a_backend_or_is_refused() {
|
||||
assert_eq!(BooleanBackend::parse(None), Ok(BooleanBackend::Truck));
|
||||
assert_eq!(BooleanBackend::parse(Some("truck")), Ok(BooleanBackend::Truck));
|
||||
assert_eq!(BooleanBackend::parse(Some(" ")), Ok(BooleanBackend::Truck));
|
||||
assert_eq!(BooleanBackend::parse(Some("smlib")).is_ok(), cfg!(feature = "smlib"));
|
||||
assert!(BooleanBackend::parse(Some("occt")).is_err());
|
||||
}
|
||||
}
|
||||
@@ -525,3 +525,50 @@ fn stacked_pieces_of_one_cylinder_keep_their_walls() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The same booleans through SMLib: its tags, renumbered into the operands' and the result's
|
||||
/// face and edge order, give a history that holds against the geometry.
|
||||
#[cfg(feature = "smlib")]
|
||||
mod smlib {
|
||||
use super::*;
|
||||
use crate::boolean_backend::smlib_boolean;
|
||||
|
||||
fn through_smlib(a: &Body, b: &Body, op: BoolOp) -> Body {
|
||||
smlib_boolean(a, b, op).unwrap().expect("a result")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operands_number_as_their_records() {
|
||||
let bodies = [boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0), crate::cylinder(Vec3::new(5.0, 5.0, -1.0), Vec3::Z, 2.0, 12.0).unwrap()];
|
||||
for (i, b) in bodies.iter().enumerate() {
|
||||
let (_, order) = crate::native::smlib_body_ordered(b).unwrap();
|
||||
assert!(order.is_some(), "body {i}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn box_minus_cylinder() {
|
||||
let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
|
||||
let b = crate::cylinder(Vec3::new(5.0, 5.0, -1.0), Vec3::Z, 2.0, 12.0).unwrap();
|
||||
let r = through_smlib(&a, &b, BoolOp::Cut);
|
||||
let h = verified(&r, &[&a, &b], 0.01);
|
||||
assert!(from(h, face(0, 0)).len() + from(h, face(0, 1)).len() >= 1, "{}", show(h));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn box_union_box() {
|
||||
let a = boxed(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
|
||||
let b = boxed(5.0, 2.0, 2.0, 15.0, 8.0, 8.0);
|
||||
let r = through_smlib(&a, &b, BoolOp::Union);
|
||||
let h = verified(&r, &[&a, &b], 0.02);
|
||||
assert_eq!(h.faces.len(), 11, "{}", show(h));
|
||||
assert_eq!(meets(h).len(), 4, "{}", show(h));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disjoint_intersection_is_empty() {
|
||||
let a = boxed(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
|
||||
let b = boxed(5.0, 5.0, 5.0, 6.0, 6.0, 6.0);
|
||||
assert!(smlib_boolean(&a, &b, BoolOp::Intersect).unwrap().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ mod blend_vertex;
|
||||
#[cfg(test)]
|
||||
mod blend_wrong_way_tests;
|
||||
mod body;
|
||||
mod boolean_backend;
|
||||
mod boolean_diagnostics;
|
||||
mod boolean_guard;
|
||||
mod boolean_record;
|
||||
@@ -160,6 +161,7 @@ pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet, fillet_each, fillet
|
||||
pub use blend_corner::fillet_radii;
|
||||
pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fillet_variable, fillet_variable_points, fillet_variable_spline, taper};
|
||||
pub use body::{Body, CapSignature, DISPLAY_ANGLE, DisplayShare, EdgeInfo, FaceInfo, FacePaint, FaceSource, Paint};
|
||||
pub use boolean_backend::{BOOLEANS_ENV, BooleanBackend, boolean_backend};
|
||||
pub use boolean_diagnostics::{BooleanDiagnostics, BooleanFallback, BooleanStrategy, capture_boolean_diagnostics};
|
||||
pub use boundary::{Boundary, boundary_patch};
|
||||
pub use boundary_network::{BoundaryCurve, CurveSource, MAX_CHAIN_CURVES, MAX_EDGE_PICKS, boundary_join, boundary_surface};
|
||||
|
||||
@@ -157,8 +157,65 @@ pub fn decode_native_body(bytes: &[u8]) -> Result<Body> {
|
||||
/// says (`Body::oriented`), never by meshing it.
|
||||
#[cfg(feature = "smlib")]
|
||||
pub fn body_from_smlib(brep: &solidcraft_smlib::Brep) -> Result<Body> {
|
||||
let record = brep.to_native().map_err(|e| KernelError::Failed(e.to_string()))?;
|
||||
guard("decode SMLib B-rep", || Body::oriented(adapter::decode_from(record, adapter::Producer::Foreign)?))
|
||||
body_from_smlib_ordered(brep).map(|(body, _)| body)
|
||||
}
|
||||
|
||||
/// [`body_from_smlib`], with where each of SMLib's faces and edges is in the body's face and
|
||||
/// edge order ([`crate::history`]): `None` where the body did not keep the record's order (a
|
||||
/// rebuild that walked a face's edges the other way).
|
||||
#[cfg(feature = "smlib")]
|
||||
pub(crate) fn body_from_smlib_ordered(brep: &solidcraft_smlib::Brep) -> Result<(Body, Option<solidcraft_smlib::history::Order>)> {
|
||||
let (record, order) = brep.to_native_ordered().map_err(|e| KernelError::Failed(e.to_string()))?;
|
||||
guard("decode SMLib B-rep", || {
|
||||
let (solid, numbering) = adapter::decode_numbered(record, adapter::Producer::Foreign)?;
|
||||
let body = Body::oriented(solid)?;
|
||||
let faces_kept = body.solid.face_iter().map(|f| f.id()).eq(numbering.faces.iter().copied());
|
||||
let edges_kept = crate::body::edge_order(body.solid.face_iter()).0.iter().map(|e| e.id()).eq(numbering.edges.iter().copied());
|
||||
Ok((body, (faces_kept && edges_kept).then_some(order)))
|
||||
})
|
||||
}
|
||||
|
||||
/// `body` in SMLib ([`smlib_body`]), with where each of SMLib's faces and edges is in the body's
|
||||
/// face and edge order: SMLib's export of the imported body, checked to number every face,
|
||||
/// edge and vertex as the body's record does. `None` where it does not.
|
||||
#[cfg(feature = "smlib")]
|
||||
pub(crate) fn smlib_body_ordered(body: &Body) -> Result<(solidcraft_smlib::Brep, Option<solidcraft_smlib::history::Order>)> {
|
||||
let record = brep_record(body)?;
|
||||
let brep = solidcraft_smlib::Brep::from_record(&record, true).map_err(|e| KernelError::Failed(format!("SMLib: {e}")))?;
|
||||
let order = match brep.to_native_ordered() {
|
||||
Ok((back, order)) => same_numbering(&record, &back).then_some(order),
|
||||
Err(e) => {
|
||||
log::debug!(target: "smlib", "no order for an operand: {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
Ok((brep, order))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[cfg(feature = "smlib")]
|
||||
fn same_numbering(a: &solidcraft_geom::brep::Brep, b: &solidcraft_geom::brep::Brep) -> bool {
|
||||
use solidcraft_geom::brep::Loop;
|
||||
if a.faces.len() != b.faces.len() || a.edges.len() != b.edges.len() || a.vertices.len() != b.vertices.len() {
|
||||
return false;
|
||||
}
|
||||
let loops_alike = a.faces.iter().zip(&b.faces).all(|(fa, fb)| {
|
||||
fa.loops.len() == fb.loops.len()
|
||||
&& fa.loops.iter().zip(&fb.loops).all(|pair| match pair {
|
||||
(Loop::Edges { edges: x }, Loop::Edges { edges: y }) => {
|
||||
x.len() == y.len() && x.iter().zip(y).all(|(u, w)| u.edge == w.edge && u.forward == w.forward)
|
||||
}
|
||||
(Loop::Vertex { vertex: x }, Loop::Vertex { vertex: y }) => x == y,
|
||||
_ => false,
|
||||
})
|
||||
});
|
||||
let ends_alike = a.edges.iter().zip(&b.edges).all(|(x, y)| x.vertices == y.vertices);
|
||||
let at_same_place = a.vertices.iter().zip(&b.vertices).all(|(x, y)| {
|
||||
let d = (0..3).map(|k| (x.point[k] - y.point[k]).powi(2)).sum::<f64>().sqrt();
|
||||
d <= x.tolerance + y.tolerance + a.tolerance + b.tolerance
|
||||
});
|
||||
loops_alike && ends_alike && at_same_place
|
||||
}
|
||||
|
||||
fn validate_topology(saved: &SavedBody) -> Result<()> {
|
||||
|
||||
@@ -330,6 +330,20 @@ pub(super) fn decode(body: b::Brep) -> Result<mt::Solid> {
|
||||
}
|
||||
|
||||
pub(super) fn decode_from(body: b::Brep, producer: Producer) -> Result<mt::Solid> {
|
||||
decode_numbered(body, producer).map(|(solid, _)| solid)
|
||||
}
|
||||
|
||||
/// The identities of a decoded record's faces and edges, by record index.
|
||||
#[cfg_attr(not(feature = "smlib"), allow(dead_code))]
|
||||
pub(super) struct Numbering {
|
||||
pub faces: Vec<mt::FaceID>,
|
||||
pub edges: Vec<mt::EdgeID>,
|
||||
}
|
||||
|
||||
/// [`decode_from`], with each record face's and edge's identity in the solid: a caller holding
|
||||
/// indices into the record (an SMLib history) checks with them that the solid's face and edge
|
||||
/// order is the record's.
|
||||
pub(super) fn decode_numbered(body: b::Brep, producer: Producer) -> Result<(mt::Solid, Numbering)> {
|
||||
body.validate().map_err(invalid)?;
|
||||
body.check_geometry().map_err(invalid)?;
|
||||
compatible(&body, producer)?;
|
||||
@@ -340,6 +354,7 @@ pub(super) fn decode_from(body: b::Brep, producer: Producer) -> Result<mt::Solid
|
||||
.into_iter()
|
||||
.map(|e| Ok(mt::Edge::new_unchecked(vertex(e.vertices[0])?, vertex(e.vertices[1])?, load_edge_curve(e.curve, e.interval)?)))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let mut face_ids: Vec<Option<mt::FaceID>> = vec![None; faces.len()];
|
||||
let mut faces: Vec<Option<b::Face>> = faces.into_iter().map(Some).collect();
|
||||
let shells = shells
|
||||
.into_iter()
|
||||
@@ -349,7 +364,11 @@ pub(super) fn decode_from(body: b::Brep, producer: Producer) -> Result<mt::Solid
|
||||
.map(|u| {
|
||||
// A face in two shells (both its sides used) is refused here: truck has one.
|
||||
let f = faces.get_mut(u.face as usize).and_then(Option::take).ok_or_else(|| unsupported("a face in two shells"))?;
|
||||
face(f, u.forward, &edges)
|
||||
let made = face(f, u.forward, &edges)?;
|
||||
if let Some(slot) = face_ids.get_mut(u.face as usize) {
|
||||
*slot = Some(made.id());
|
||||
}
|
||||
Ok(made)
|
||||
})
|
||||
.collect::<Result<mt::Shell>>(),
|
||||
b::Shell::Faces { .. } => Err(unsupported("wire edges embedded in a shell")),
|
||||
@@ -359,7 +378,9 @@ pub(super) fn decode_from(body: b::Brep, producer: Producer) -> Result<mt::Solid
|
||||
if shells.iter().any(|s| s.shell_condition() == mt::ShellCondition::Irregular) {
|
||||
return Err(invalid("non-manifold shell"));
|
||||
}
|
||||
Ok(mt::Solid::new_unchecked(shells))
|
||||
let faces = face_ids.into_iter().collect::<Option<Vec<_>>>().ok_or_else(|| invalid("a face no shell uses"))?;
|
||||
let numbering = Numbering { faces, edges: edges.iter().map(mt::Edge::id).collect() };
|
||||
Ok((mt::Solid::new_unchecked(shells), numbering))
|
||||
}
|
||||
/// A face's topology as recorded, references already checked: built as the kernel built it,
|
||||
/// without truck's checks for wires that touch themselves (a seam edge used twice, a closed edge
|
||||
|
||||
@@ -917,8 +917,38 @@ fn take_attempt() -> bool {
|
||||
///
|
||||
/// The result carries its exact history ([`Body::history`]) over the operands `[a, b]`,
|
||||
/// whichever way it was made, or none where a step it took does not report one.
|
||||
///
|
||||
/// Truck runs it unless [`crate::BOOLEANS_ENV`] picks SMLib ([`crate::boolean_backend`]).
|
||||
pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, || boolean_untimed(a, b, op))
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, || match crate::boolean_backend()? {
|
||||
crate::BooleanBackend::Truck => boolean_untimed(a, b, op),
|
||||
crate::BooleanBackend::Smlib => smlib_boolean(a, b, op),
|
||||
})
|
||||
}
|
||||
|
||||
/// [`boolean`] through SMLib, its result given the operands' roles as truck's is.
|
||||
#[cfg(feature = "smlib")]
|
||||
fn smlib_boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
let r = guard("SMLib boolean", || crate::boolean_backend::smlib_boolean(a, b, op));
|
||||
with_operand_roles(a, b, op, r)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "smlib"))]
|
||||
fn smlib_boolean(_: &Body, _: &Body, _: BoolOp) -> Result<Option<Body>> {
|
||||
Err(KernelError::Invalid("not supported yet: this build has no SMLib".into()))
|
||||
}
|
||||
|
||||
/// A boolean's result with what its operands' faces stood for carried over: regularization
|
||||
/// protection and the planar construction and shell roles on supports it kept.
|
||||
fn with_operand_roles(a: &Body, b: &Body, op: BoolOp, r: Result<Option<Body>>) -> Result<Option<Body>> {
|
||||
let r = r.map(|result| result.map(|body| body.inherit_regularization_protection(a).inherit_regularization_protection(b)));
|
||||
match op {
|
||||
BoolOp::Cut => r.and_then(|result| result.map(|body| body.with_preserved_boolean_planar_sources(a, b, true)).transpose()),
|
||||
BoolOp::Union => r.and_then(|result| result.map(|body| body.with_preserved_union_planar_sources(a, b)).transpose()),
|
||||
// Every face of an intersection lies on one operand's face, as a union's does: the same
|
||||
// roles carry over (#565).
|
||||
BoolOp::Intersect => r.and_then(|result| result.map(|body| body.with_preserved_union_planar_sources(a, b)).transpose()),
|
||||
}
|
||||
}
|
||||
|
||||
fn boolean_untimed(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
@@ -1012,7 +1042,7 @@ fn boolean_via(a: &Body, b: &Body, op: BoolOp, whole: &dyn Fn(&Body, &Body, Bool
|
||||
)),
|
||||
};
|
||||
}
|
||||
r = finished(a, b, op, r);
|
||||
r = with_operand_roles(a, b, op, r);
|
||||
// A retry's result is held to what the operation can do at all; one that fails is refused
|
||||
// rather than returned wrong (issue #616).
|
||||
if (!diagnostics.events.is_empty() || diagnostics.truncated)
|
||||
|
||||
@@ -34,6 +34,7 @@ use solidcraft_geom::Vec3;
|
||||
use solidcraft_geom::brep as b;
|
||||
|
||||
use crate::flat::{self, *};
|
||||
use crate::history::Order;
|
||||
use crate::import::Arrays;
|
||||
use crate::{Brep, Result, error, ffi, init};
|
||||
|
||||
@@ -86,6 +87,15 @@ impl Brep {
|
||||
let arrays = self.arrays().map_err(ExportError::Smlib)?;
|
||||
native(&arrays)
|
||||
}
|
||||
|
||||
/// [`Brep::to_native`], with where each of SMLib's faces and edges went in the record's
|
||||
/// canonical order: what [`crate::history::Raw::history`] renumbers this body's history with.
|
||||
/// SMLib's export lists faces and edges in the body's own order (`SmBrep::GetFaces`,
|
||||
/// `GetEdges`), the order its history uses.
|
||||
pub fn to_native_ordered(&self) -> std::result::Result<(b::Brep, Order), ExportError> {
|
||||
let arrays = self.arrays().map_err(ExportError::Smlib)?;
|
||||
native_ordered(&arrays).map(|(brep, _, order)| (brep, order))
|
||||
}
|
||||
}
|
||||
|
||||
/// What the mapping could not put in its own kind, or turned.
|
||||
@@ -576,6 +586,11 @@ struct Edge {
|
||||
|
||||
/// The schema 4 B-rep of SMLib's flat arrays: mapped, renumbered canonically and validated.
|
||||
pub fn native(arrays: &Arrays) -> Res<(b::Brep, Report)> {
|
||||
native_ordered(arrays).map(|(brep, report, _)| (brep, report))
|
||||
}
|
||||
|
||||
/// [`native`], with the canonical place of each array face and edge.
|
||||
fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> {
|
||||
let flat = View(arrays);
|
||||
let mut report = Report::default();
|
||||
if arrays.ints.values().map(Vec::len).chain(arrays.reals.values().map(Vec::len)).any(|n| n > flat::MAX_LEN) {
|
||||
@@ -787,7 +802,7 @@ pub fn native(arrays: &Arrays) -> Res<(b::Brep, Report)> {
|
||||
}
|
||||
let shells: Vec<Vec<(u32, bool)>> = order.iter().filter_map(|s| shells.get(*s as usize).cloned()).collect();
|
||||
|
||||
canonical(Parts { faces, edges, shells, regions }, n_vertices, flat, tolerance).map(|brep| (brep, report))
|
||||
canonical(Parts { faces, edges, shells, regions }, n_vertices, flat, tolerance).map(|(brep, order)| (brep, report, order))
|
||||
}
|
||||
|
||||
/// The mapped records before renumbering.
|
||||
@@ -800,8 +815,9 @@ struct Parts {
|
||||
|
||||
/// The record in canonical order: faces in first appearance through the shells, edges through
|
||||
/// the faces (a face whose first oriented face is reversed walked backwards), vertices through
|
||||
/// the edges and then the vertex loops, with each face's pcurves; validated.
|
||||
fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerance: f64) -> Res<b::Brep> {
|
||||
/// the edges and then the vertex loops, with each face's pcurves; validated. With it, the
|
||||
/// canonical index of each array face and edge.
|
||||
fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerance: f64) -> Res<(b::Brep, Order)> {
|
||||
let Parts { faces, edges, shells, regions } = parts;
|
||||
let mut face_new: Vec<Option<u32>> = vec![None; faces.len()];
|
||||
let (mut face_order, mut direction) = (Vec::new(), Vec::new());
|
||||
@@ -933,7 +949,11 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerance: f64) -> Res
|
||||
.collect::<Res<Vec<_>>>()?;
|
||||
let brep = b::Brep { version: b::VERSION, tolerance, regions, shells, faces: face_records, edges: edge_records, vertices };
|
||||
brep.validate().map_err(|e| invalid(format!("the mapped B-rep does not validate: {e}")))?;
|
||||
Ok(brep)
|
||||
let placed = |new: &[Option<u32>]| {
|
||||
new.iter().map(|i| i.map(|i| i as usize).ok_or_else(|| invalid("an entity outside the canonical walk"))).collect::<Res<Vec<_>>>()
|
||||
};
|
||||
let order = Order { faces: placed(&face_new)?, edges: placed(&edge_new)? };
|
||||
Ok((brep, order))
|
||||
}
|
||||
|
||||
/// A face's pcurves, edge loop by edge loop, from the plain-record routine on the face's
|
||||
|
||||
@@ -58,6 +58,29 @@ mod native_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A tagged body's own history, renumbered with its export's order on both sides, is the
|
||||
/// identity in canonical order: the order says where each SMLib face and edge went.
|
||||
#[test]
|
||||
fn the_export_order_renumbers_a_history_into_the_record() {
|
||||
for (name, s, _) in primitives() {
|
||||
let t = s.tagged(0).unwrap();
|
||||
let (record, order) = t.to_native_ordered().unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!((order.faces.len(), order.edges.len()), (record.faces.len(), record.edges.len()), "{name}");
|
||||
let h = t.history().unwrap().history(std::slice::from_ref(&order), &order).unwrap();
|
||||
for (i, f) in h.faces.iter().enumerate() {
|
||||
assert_eq!(f.origins, vec![solidcraft_geom::brep::history::FaceOrigin::Face { operand: 0, face: i }], "{name}: face {i}");
|
||||
}
|
||||
for (i, e) in h.edges.iter().enumerate() {
|
||||
assert_eq!(e.origins, vec![solidcraft_geom::brep::history::EdgeOrigin::Edge { operand: 0, edge: i }], "{name}: edge {i}");
|
||||
// The faces it bounds, as the record's uses say.
|
||||
let mut faces: Vec<usize> = record.edges[i].uses.iter().map(|u| u.face as usize).collect();
|
||||
faces.sort();
|
||||
faces.dedup();
|
||||
assert_eq!(e.faces, faces, "{name}: edge {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognised_edges_are_on_smlibs_curves() {
|
||||
let cyl = Brep::make_cylinder([0.0; 3], 5.0, 10.0).unwrap().to_native().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user