diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 9fe4824cfa..be98761e63 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -189,7 +189,9 @@ pub use matchsurf::{match_surface, seam_continuity}; pub use measure::{BodyMeasure, MassMeasure, measure, measure_mass}; pub use mesh_convert::convert_prismatic_mesh; pub use meshbody::{MAX_TRIANGLES, mesh_body}; -pub use native::{decode_native_body, encode_native_body, native_preview_mesh}; +#[cfg(feature = "smlib")] +pub use native::smlib_body; +pub use native::{brep_record, decode_native_body, encode_native_body, native_preview_mesh}; pub use offset::with_same_surface; pub use ops::{ BoolOp, boolean, can_join_all_touching_faces, can_join_touching_faces, compound_disjoint_extrusions, half_space, is_planar, join_apart, diff --git a/crates/kernel/src/native.rs b/crates/kernel/src/native.rs index 47cac0bc87..4608e8622b 100644 --- a/crates/kernel/src/native.rs +++ b/crates/kernel/src/native.rs @@ -82,6 +82,26 @@ pub fn encode_native_body(body: &Body) -> Result> { }) } +/// The schema 4 record of a solid body, in canonical order with its pcurves: what another +/// backend (SMLib) builds the body from. Mesh bodies have none. +pub fn brep_record(body: &Body) -> Result { + guard("B-rep record", || { + if body.mesh.is_some() { + return Err(invalid("mesh bodies are source blobs, not B-rep")); + } + adapter::encode(body) + }) +} + +/// The body in SMLib, built from its schema 4 record. The record is trusted (SMLib's healer is +/// skipped): the kernel's records keep SMLib's rules, and healing moved a cylinder's volume by +/// 9e-7 where the record as given is exact to 1e-7. +#[cfg(feature = "smlib")] +pub fn smlib_body(body: &Body) -> Result { + let record = brep_record(body)?; + solidcraft_smlib::Brep::from_record(&record, true).map_err(|e| KernelError::Failed(format!("SMLib: {e}"))) +} + /// Decode bounded untrusted data. Validate indices before topology extraction. /// Only a caller that has checked who produced the data may treat it as evaluated geometry. pub fn decode_native_body(bytes: &[u8]) -> Result { @@ -194,6 +214,9 @@ fn value_nodes(v: &Value) -> usize { _ => 1, } } +#[cfg(all(test, feature = "smlib"))] +mod smlib_import_tests; + #[cfg(test)] mod tests { use super::*; diff --git a/crates/kernel/src/native/smlib_import_tests.rs b/crates/kernel/src/native/smlib_import_tests.rs new file mode 100644 index 0000000000..ba67214cd3 --- /dev/null +++ b/crates/kernel/src/native/smlib_import_tests.rs @@ -0,0 +1,38 @@ +//! Truck bodies into SMLib through their schema 4 records: the kernel's primitives keep their +//! analytic volume. Truck holds cylinders, spheres and tori as rational B-splines, which SMLib +//! integrates to a few 1e-7 (truck's own mesh measure is 1e-4 off). +use std::f64::consts::PI; + +use solidcraft_geom::Vec3; + +use crate::{Body, box_solid, cylinder, sphere, torus}; + +fn v(x: f64, y: f64, z: f64) -> Vec3 { + Vec3::new(x, y, z) +} + +/// The body's SMLib volume, and whether SMLib finds it a manifold solid. +fn in_smlib(body: &Body) -> Result<(f64, bool), String> { + let s = super::smlib_body(body).map_err(|e| e.to_string())?; + Ok((s.volume(1e-7).map_err(|e| e.to_string())?, s.is_manifold_solid().unwrap_or(false))) +} + +#[test] +fn primitives_keep_their_analytic_volume() { + let (got, solid) = in_smlib(&box_solid(v(0., 0., 0.), v(10., 20., 30.)).unwrap()).unwrap(); + assert!(solid && (got - 6000.0).abs() <= 1e-9 * 6000.0, "box: SMLib volume {got}, manifold {solid}"); + let (got, solid) = in_smlib(&cylinder(v(1., 2., 3.), v(0., 0., 1.), 5.0, 10.0).unwrap()).unwrap(); + assert!(solid && (got - 250.0 * PI).abs() <= 1e-6 * 250.0 * PI, "cylinder: SMLib volume {got}, manifold {solid}"); + let (got, solid) = in_smlib(&cylinder(v(0., 0., 0.), v(1., 1., 1.), 2.0, 7.0).unwrap()).unwrap(); + assert!(solid && (got - 28.0 * PI).abs() <= 1e-6 * 28.0 * PI, "tilted cylinder: SMLib volume {got}, manifold {solid}"); +} + +/// Truck holds spheres and tori as revolutions (`PlacedRevolution`), which the SMLib import does +/// not carry yet: a clear error, never a wrong body. +#[test] +fn revolutions_are_refused_for_now() { + for (name, body) in [("sphere", sphere(v(0., 0., 0.), 5.0).unwrap()), ("torus", torus(v(0., 0., 0.), 10.0, 2.0).unwrap())] { + let e = super::smlib_body(&body).err().map(|e| e.to_string()).unwrap_or_default(); + assert!(e.contains("not supported yet: revolved surfaces"), "{name}: {e}"); + } +} diff --git a/crates/smlib/SMLIB_REV b/crates/smlib/SMLIB_REV index 5897339d20..957eddc8ec 100644 --- a/crates/smlib/SMLIB_REV +++ b/crates/smlib/SMLIB_REV @@ -1 +1 @@ -3e95b7905ed9e2ba355b0ca9954295ac002d2925 +40bb855d15391428697d086fd122409c10e5ffee diff --git a/crates/smlib/src/ffi.rs b/crates/smlib/src/ffi.rs index daa54cd41d..e774d9a12c 100644 --- a/crates/smlib/src/ffi.rs +++ b/crates/smlib/src/ffi.rs @@ -7,7 +7,7 @@ use std::ffi::{c_char, c_void}; /// `SMLIB_ABI_VERSION` this crate was written against. -pub const ABI_VERSION: i32 = 1; +pub const ABI_VERSION: i32 = 2; pub const OK: i32 = 0; pub const ERR_EXCEPTION: i32 = -1000; @@ -180,6 +180,7 @@ mod calls { pub use calls::*; pub mod check; +pub mod flat; pub mod history; pub mod measure; diff --git a/crates/smlib/src/ffi/flat.rs b/crates/smlib/src/ffi/flat.rs new file mode 100644 index 0000000000..8353e3bcd6 --- /dev/null +++ b/crates/smlib/src/ffi/flat.rs @@ -0,0 +1,134 @@ +//! The flat-array block of `capi/include/smlib.h`, declared by hand. Part of the FFI boundary, +//! like its parent: called only through [`crate::flat`]. +#![allow(unsafe_code)] + +use super::Handle; +#[cfg(feature = "native")] +use super::{OK, RawBrep, new_brep}; + +/// `smlib_flat`, opaque. +#[repr(C)] +pub struct RawFlat { + _private: [u8; 0], +} + +#[cfg(feature = "native")] +unsafe extern "C" { + fn smlib_flat_new() -> *mut RawFlat; + fn smlib_flat_free(flat: *mut RawFlat); + fn smlib_brep_export(brep: *const RawBrep, out: *mut *mut RawFlat) -> i32; + fn smlib_brep_import(flat: *const RawFlat, out: *mut *mut RawBrep) -> i32; + fn smlib_flat_get_i64(flat: *const RawFlat, id: i32, data: *mut *const i64, len: *mut i64) -> i32; + fn smlib_flat_get_f64(flat: *const RawFlat, id: i32, data: *mut *const f64, len: *mut i64) -> i32; + fn smlib_flat_set_i64(flat: *mut RawFlat, id: i32, data: *const i64, len: i64) -> i32; + fn smlib_flat_set_f64(flat: *mut RawFlat, id: i32, data: *const f64, len: i64) -> i32; + fn smlib_brep_is_manifold_solid(brep: *const RawBrep, out: *mut i32) -> i32; +} + +/// An owned `smlib_flat`, freed once on drop. +#[cfg_attr(not(feature = "native"), allow(dead_code))] +pub struct FlatHandle(std::ptr::NonNull); + +// Only ever used through the C API, which serialises every call. +unsafe impl Send for FlatHandle {} +unsafe impl Sync for FlatHandle {} + +impl Drop for FlatHandle { + fn drop(&mut self) { + #[cfg(feature = "native")] + unsafe { + smlib_flat_free(self.0.as_ptr()) + }; + } +} + +#[cfg(feature = "native")] +mod calls { + use super::*; + use crate::ffi::{ERR_ARG, ERR_EMPTY, ERR_EXCEPTION}; + + pub fn new() -> Result { + std::ptr::NonNull::new(unsafe { smlib_flat_new() }).map(FlatHandle).ok_or(ERR_EXCEPTION) + } + pub fn export(b: &Handle) -> Result { + let mut out: *mut RawFlat = std::ptr::null_mut(); + match (unsafe { smlib_brep_export(b.raw(), &mut out) }, std::ptr::NonNull::new(out)) { + (OK, Some(p)) => Ok(FlatHandle(p)), + (OK, None) => Err(ERR_EMPTY), + (s, _) => Err(s), + } + } + pub fn import(f: &FlatHandle) -> Result { + new_brep(|out| unsafe { smlib_brep_import(f.0.as_ptr(), out) }) + } + /// Copies the array out: the C side's buffer lives only as long as the arrays are unchanged. + fn copy(s: i32, data: *const T, len: i64) -> Result, i32> { + if s != OK { + return Err(s); + } + let n = usize::try_from(len).map_err(|_| ERR_ARG)?; + if n == 0 || data.is_null() { + return Ok(Vec::new()); + } + Ok(unsafe { std::slice::from_raw_parts(data, n) }.to_vec()) + } + pub fn get_i64(f: &FlatHandle, id: i32) -> Result, i32> { + let (mut data, mut len) = (std::ptr::null(), 0i64); + let s = unsafe { smlib_flat_get_i64(f.0.as_ptr(), id, &mut data, &mut len) }; + copy(s, data, len) + } + pub fn get_f64(f: &FlatHandle, id: i32) -> Result, i32> { + let (mut data, mut len) = (std::ptr::null(), 0i64); + let s = unsafe { smlib_flat_get_f64(f.0.as_ptr(), id, &mut data, &mut len) }; + copy(s, data, len) + } + pub fn set_i64(f: &mut FlatHandle, id: i32, v: &[i64]) -> i32 { + let Ok(n) = i64::try_from(v.len()) else { return ERR_ARG }; + unsafe { smlib_flat_set_i64(f.0.as_ptr(), id, v.as_ptr(), n) } + } + pub fn set_f64(f: &mut FlatHandle, id: i32, v: &[f64]) -> i32 { + let Ok(n) = i64::try_from(v.len()) else { return ERR_ARG }; + unsafe { smlib_flat_set_f64(f.0.as_ptr(), id, v.as_ptr(), n) } + } + pub fn is_manifold_solid(b: &Handle) -> Result { + let mut out = 0i32; + match unsafe { smlib_brep_is_manifold_solid(b.raw(), &mut out) } { + OK => Ok(out != 0), + s => Err(s), + } + } +} + +/// The same calls when SMLib is not built: each one fails with `NOT_BUILT`. +#[cfg(not(feature = "native"))] +mod calls { + use super::*; + use crate::ffi::NOT_BUILT; + + pub fn new() -> Result { + Err(NOT_BUILT) + } + pub fn export(_: &Handle) -> Result { + Err(NOT_BUILT) + } + pub fn import(_: &FlatHandle) -> Result { + Err(NOT_BUILT) + } + pub fn get_i64(_: &FlatHandle, _: i32) -> Result, i32> { + Err(NOT_BUILT) + } + pub fn get_f64(_: &FlatHandle, _: i32) -> Result, i32> { + Err(NOT_BUILT) + } + pub fn set_i64(_: &mut FlatHandle, _: i32, _: &[i64]) -> i32 { + NOT_BUILT + } + pub fn set_f64(_: &mut FlatHandle, _: i32, _: &[f64]) -> i32 { + NOT_BUILT + } + pub fn is_manifold_solid(_: &Handle) -> Result { + Err(NOT_BUILT) + } +} + +pub use calls::*; diff --git a/crates/smlib/src/flat.rs b/crates/smlib/src/flat.rs new file mode 100644 index 0000000000..d163065ee7 --- /dev/null +++ b/crates/smlib/src/flat.rs @@ -0,0 +1,172 @@ +//! SMLib's flat B-rep arrays (`capi/include/smlib.h`, "Flat B-rep arrays"): a B-rep as plain +//! int64 and double arrays laid out like a member of USD's BrepArray schema, read from SMLib with +//! [`Brep::export`](crate::Brep::export) and built into SMLib with [`Flat::import`]. + +use crate::{Brep, Result, error, ffi, init}; + +// Array ids and kind values, as in smlib.h. +pub const REGION_SHELL_COUNT: i32 = 1; +pub const REGION_TYPE: i32 = 2; +pub const SHELL_FACEUSE_COUNT: i32 = 3; +pub const SHELL_WIRE_EDGE_COUNT: i32 = 4; +pub const SHELL_POINT_TYPE: i32 = 5; +pub const FACEUSE_FACE: i32 = 6; +pub const FACEUSE_ORIENTATION: i32 = 7; +pub const FACE_LOOP_COUNT: i32 = 8; +pub const FACE_SURFACE_TYPE: i32 = 9; +pub const FACE_TRIM_TYPE: i32 = 10; +pub const LOOP_EDGEUSE_COUNT: i32 = 11; +pub const LOOP_VERTEX: i32 = 12; +pub const EDGEUSE_EDGE: i32 = 13; +pub const EDGEUSE_ORIENTATION: i32 = 14; +pub const EDGEUSE_NEXT_RADIAL: i32 = 15; +pub const EDGEUSE_RADIAL_ENTRY: i32 = 16; +pub const EDGE_CURVE_TYPE: i32 = 17; +pub const EDGE_VERTICES: i32 = 18; +pub const WIRE_EDGE_CURVE_TYPE: i32 = 19; +pub const WIRE_EDGE_VERTICES: i32 = 20; +pub const VERTEX_POINT_TYPE: i32 = 21; +pub const EDGE_NURBS_VERTEX_COUNT: i32 = 22; +pub const EDGE_NURBS_ORDER: i32 = 23; +pub const WIRE_EDGE_NURBS_VERTEX_COUNT: i32 = 24; +pub const WIRE_EDGE_NURBS_ORDER: i32 = 25; +pub const EDGEUSE_NURBS_VERTEX_COUNT: i32 = 26; +pub const EDGEUSE_NURBS_ORDER: i32 = 27; +pub const FACE_NURBS_U_VERTEX_COUNT: i32 = 28; +pub const FACE_NURBS_V_VERTEX_COUNT: i32 = 29; +pub const FACE_NURBS_U_ORDER: i32 = 30; +pub const FACE_NURBS_V_ORDER: i32 = 31; +pub const SOURCE: i32 = 32; +pub const TOLERANCE: i32 = 101; +pub const EXTENT: i32 = 102; +pub const FACE_RANGE: i32 = 103; +pub const EDGE_RANGE: i32 = 104; +pub const WIRE_EDGE_RANGE: i32 = 105; +pub const SHELL_POINT_POSITION: i32 = 106; +pub const VERTEX_POSITION: i32 = 107; +pub const EDGE_NURBS_CVS: i32 = 108; +pub const EDGE_NURBS_KNOTS: i32 = 109; +pub const EDGE_NURBS_WEIGHTS: i32 = 110; +pub const EDGE_CIRCLE_CENTER: i32 = 111; +pub const EDGE_CIRCLE_AXIS: i32 = 112; +pub const EDGE_CIRCLE_REF_DIRECTION: i32 = 113; +pub const EDGE_CIRCLE_RADIUS: i32 = 114; +pub const EDGE_LINE_ORIGIN: i32 = 115; +pub const EDGE_LINE_DIRECTION: i32 = 116; +pub const EDGE_ELLIPSE_CENTER: i32 = 117; +pub const EDGE_ELLIPSE_AXIS: i32 = 118; +pub const EDGE_ELLIPSE_REF_DIRECTION: i32 = 119; +pub const EDGE_ELLIPSE_X_RADIUS: i32 = 120; +pub const EDGE_ELLIPSE_Y_RADIUS: i32 = 121; +pub const WIRE_EDGE_NURBS_CVS: i32 = 122; +pub const WIRE_EDGE_NURBS_KNOTS: i32 = 123; +pub const WIRE_EDGE_NURBS_WEIGHTS: i32 = 124; +pub const WIRE_EDGE_CIRCLE_CENTER: i32 = 125; +pub const WIRE_EDGE_CIRCLE_AXIS: i32 = 126; +pub const WIRE_EDGE_CIRCLE_REF_DIRECTION: i32 = 127; +pub const WIRE_EDGE_CIRCLE_RADIUS: i32 = 128; +pub const WIRE_EDGE_LINE_ORIGIN: i32 = 129; +pub const WIRE_EDGE_LINE_DIRECTION: i32 = 130; +pub const WIRE_EDGE_ELLIPSE_CENTER: i32 = 131; +pub const WIRE_EDGE_ELLIPSE_AXIS: i32 = 132; +pub const WIRE_EDGE_ELLIPSE_REF_DIRECTION: i32 = 133; +pub const WIRE_EDGE_ELLIPSE_X_RADIUS: i32 = 134; +pub const WIRE_EDGE_ELLIPSE_Y_RADIUS: i32 = 135; +pub const EDGEUSE_NURBS_CVS: i32 = 136; +pub const EDGEUSE_NURBS_KNOTS: i32 = 137; +pub const EDGEUSE_NURBS_WEIGHTS: i32 = 138; +pub const FACE_NURBS_CVS: i32 = 139; +pub const FACE_NURBS_U_KNOTS: i32 = 140; +pub const FACE_NURBS_V_KNOTS: i32 = 141; +pub const FACE_NURBS_WEIGHTS: i32 = 142; +pub const FACE_SPHERE_CENTER: i32 = 143; +pub const FACE_SPHERE_AXIS: i32 = 144; +pub const FACE_SPHERE_REF_DIRECTION: i32 = 145; +pub const FACE_SPHERE_RADIUS: i32 = 146; +pub const FACE_PLANE_ORIGIN: i32 = 147; +pub const FACE_PLANE_AXIS: i32 = 148; +pub const FACE_PLANE_REF_DIRECTION: i32 = 149; +pub const FACE_CYLINDER_ORIGIN: i32 = 150; +pub const FACE_CYLINDER_AXIS: i32 = 151; +pub const FACE_CYLINDER_REF_DIRECTION: i32 = 152; +pub const FACE_CYLINDER_RADIUS: i32 = 153; +pub const FACE_CONE_ORIGIN: i32 = 154; +pub const FACE_CONE_AXIS: i32 = 155; +pub const FACE_CONE_REF_DIRECTION: i32 = 156; +pub const FACE_CONE_RADIUS: i32 = 157; +pub const FACE_CONE_SEMI_ANGLE: i32 = 158; +pub const FACE_TORUS_ORIGIN: i32 = 159; +pub const FACE_TORUS_AXIS: i32 = 160; +pub const FACE_TORUS_REF_DIRECTION: i32 = 161; +pub const FACE_TORUS_MAJOR_RADIUS: i32 = 162; +pub const FACE_TORUS_MINOR_RADIUS: i32 = 163; +pub const SURFACE_NURBS: u8 = 0; +pub const SURFACE_PLANE: u8 = 1; +pub const SURFACE_CYLINDER: u8 = 2; +pub const SURFACE_CONE: u8 = 3; +pub const SURFACE_SPHERE: u8 = 4; +pub const SURFACE_TORUS: u8 = 5; +pub const CURVE_NURBS: u8 = 0; +pub const CURVE_LINE: u8 = 1; +pub const CURVE_CIRCLE: u8 = 2; +pub const CURVE_ELLIPSE: u8 = 3; + +/// One B-rep's flat arrays, owned on the C side. +pub struct Flat(ffi::flat::FlatHandle); + +impl std::fmt::Debug for Flat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("smlib::Flat") + } +} + +impl Flat { + /// An empty set of arrays. + pub fn new() -> Result { + init()?; + ffi::flat::new().map(Flat).map_err(error) + } + + /// The arrays of `a` (see [`crate::import::arrays`]). + pub fn from_arrays(a: &crate::import::Arrays) -> Result { + let mut f = Flat::new()?; + for (id, v) in &a.ints { + f.set_i64(*id, v)?; + } + for (id, v) in &a.reals { + f.set_f64(*id, v)?; + } + Ok(f) + } + + pub fn get_i64(&self, id: i32) -> Result> { + ffi::flat::get_i64(&self.0, id).map_err(error) + } + pub fn get_f64(&self, id: i32) -> Result> { + ffi::flat::get_f64(&self.0, id).map_err(error) + } + pub fn set_i64(&mut self, id: i32, v: &[i64]) -> Result<()> { + crate::check(ffi::flat::set_i64(&mut self.0, id, v)) + } + pub fn set_f64(&mut self, id: i32, v: &[f64]) -> Result<()> { + crate::check(ffi::flat::set_f64(&mut self.0, id, v)) + } + + /// The SMLib B-rep these arrays describe (checked on the C side before SMLib sees them). + pub fn import(&self) -> Result { + init()?; + ffi::flat::import(&self.0).map(Brep).map_err(error) + } +} + +impl Brep { + /// This body as flat arrays. + pub fn export(&self) -> Result { + ffi::flat::export(&self.0).map(Flat).map_err(error) + } + + /// Whether SMLib finds this body one closed, manifold solid. + pub fn is_manifold_solid(&self) -> Result { + ffi::flat::is_manifold_solid(&self.0).map_err(error) + } +} diff --git a/crates/smlib/src/import.rs b/crates/smlib/src/import.rs new file mode 100644 index 0000000000..6eb65d8426 --- /dev/null +++ b/crates/smlib/src/import.rs @@ -0,0 +1,388 @@ +//! Schema 4 records (`solidcraft_geom::brep::Brep`) into SMLib: the record is laid out as the C +//! API's flat arrays (`capi/include/smlib.h`, "Flat B-rep arrays"), which `smlib_brep_import` +//! builds into an `SmBrep`. +//! +//! The two models differ in four places, and the mapping takes care of each: +//! - A closed shell is one record listed by the two regions it separates; SMLib keeps one shell +//! per region, the face used on opposite sides. A sheet's shell holds both sides of its faces. +//! - The infinite region is never a void here; SMLib marks it as one. +//! - Pcurves run along the edge (interval min → max) here; SMLib's UV trims run along their +//! edgeuse. They are carried on B-spline faces only: on analytic faces SMLib keeps its trims in +//! its own parameterisation and rebuilds them from the 3D edges. +//! - Every end value (edge intervals, face domains) is already in the flat arrays' units, radians +//! for angles; the C side converts them to SMLib's own pointwise. + +use std::collections::{BTreeMap, HashMap}; + +use solidcraft_geom::brep as b; + +use crate::flat; + +/// Why a record cannot go to SMLib. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Unsupported { + /// The record failed `Brep::validate`. + Invalid(&'static str), + /// A part of schema 4 this mapping does not carry yet. + NotYet(&'static str), +} + +impl std::fmt::Display for Unsupported { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Unsupported::Invalid(w) => write!(f, "invalid B-rep record: {w}"), + Unsupported::NotYet(w) => write!(f, "not supported yet: {w} in SMLib"), + } + } +} + +type R = std::result::Result; + +/// The flat arrays of one B-rep, by array id. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Arrays { + pub ints: BTreeMap>, + pub reals: BTreeMap>, +} + +impl Arrays { + fn i(&mut self, id: i32) -> &mut Vec { + self.ints.entry(id).or_default() + } + fn r(&mut self, id: i32) -> &mut Vec { + self.reals.entry(id).or_default() + } + fn r3(&mut self, id: i32, p: [f64; 3]) { + self.r(id).extend(p); + } +} + +fn int(n: usize) -> R { + i64::try_from(n).map_err(|_| Unsupported::Invalid("count")) +} + +fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { + [a[0] - b[0], a[1] - b[1], a[2] - b[2]] +} +fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { + [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]] +} +fn dot(a: [f64; 3], b: [f64; 3]) -> f64 { + a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +} +fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { + [a[0] + b[0], a[1] + b[1], a[2] + b[2]] +} +fn scale(a: [f64; 3], s: f64) -> [f64; 3] { + [a[0] * s, a[1] * s, a[2] * s] +} +fn z_axis(f: &b::Frame) -> [f64; 3] { + cross(f.x_axis, f.y_axis) +} + +/// The record as flat arrays. `trusted`: the record comes from a modeller that keeps SMLib's +/// rules (SMLib then skips its healer); otherwise SMLib heals it on import. +pub fn arrays(brep: &b::Brep, trusted: bool) -> R { + brep.validate().map_err(|e| Unsupported::Invalid(e.0))?; + let mut a = Arrays::default(); + + // Regions and shells. Each region gets its own copy of each shell it lists. + let enclosed_by: HashMap = + brep.regions.iter().enumerate().skip(1).filter_map(|(r, region)| region.shells.first().map(|&s| (s, r))).collect(); + let listed = |s: u32| brep.regions.iter().filter(|r| r.shells.contains(&s)).count(); + for (r, region) in brep.regions.iter().enumerate() { + a.i(flat::REGION_SHELL_COUNT).push(int(region.shells.len())?); + // SMLib marks the infinite region as a void. + a.i(flat::REGION_TYPE).push(i64::from(r == 0 || region.void)); + for (k, &s) in region.shells.iter().enumerate() { + let Some(b::Shell::Faces { faces, wires }) = brep.shells.get(s as usize) else { + return Err(Unsupported::NotYet("wire and vertex shells")); + }; + if !wires.is_empty() { + return Err(Unsupported::NotYet("wire edges embedded in a shell")); + } + let uses: Vec<(u32, bool)> = if listed(s) == 2 { + // A closed shell: the outward normals (out of the material) point into the + // region that is not material. As the outer shell of region r (k == 0) the shell + // encloses r; as an inner shell r lies outside it. + let inside = enclosed_by.get(&s).copied().ok_or(Unsupported::Invalid("closed shell encloses no region"))?; + let inside_is_void = brep.regions.get(inside).is_some_and(|e| e.void); + let normals_into_this = if k == 0 && r > 0 { inside_is_void } else { !inside_is_void }; + faces.iter().map(|f| (f.face, f.forward == normals_into_this)).collect() + } else { + // An open shell lies in one region and holds both sides of each face. + faces.iter().flat_map(|f| [(f.face, true), (f.face, false)]).collect() + }; + a.i(flat::SHELL_FACEUSE_COUNT).push(int(uses.len())?); + a.i(flat::SHELL_WIRE_EDGE_COUNT).push(0); + a.i(flat::SHELL_POINT_TYPE).push(0); + for (face, same) in uses { + a.i(flat::FACEUSE_FACE).push(i64::from(face)); + a.i(flat::FACEUSE_ORIENTATION).push(i64::from(same)); + } + } + } + + // Edgeuses are numbered face → loop → position. + let mut eu_index: HashMap<(u32, u32, u32), i64> = HashMap::new(); + let mut n = 0i64; + for (fi, f) in brep.faces.iter().enumerate() { + for (li, l) in f.loops.iter().enumerate() { + if let b::Loop::Edges { edges } = l { + for p in 0..edges.len() { + eu_index.insert((idx(fi)?, idx(li)?, idx(p)?), n); + n += 1; + } + } + } + } + let mut next = vec![-1i64; usize::try_from(n).unwrap_or(0)]; + let mut entry = vec![0i64; next.len()]; + for e in &brep.edges { + let at = |u: &b::EdgeUseRef| eu_index.get(&(u.face, u.r#loop, u.position)).copied().ok_or(Unsupported::Invalid("edge use")); + for (k, u) in e.uses.iter().enumerate() { + let here = at(u)?; + let after = e.uses.get((k + 1) % e.uses.len()).ok_or(Unsupported::Invalid("edge use"))?; + let slot = usize::try_from(here).map_err(|_| Unsupported::Invalid("edge use"))?; + *next.get_mut(slot).ok_or(Unsupported::Invalid("edge use"))? = at(after)?; + *entry.get_mut(slot).ok_or(Unsupported::Invalid("edge use"))? = i64::from(u.upper); + } + } + + // Faces, loops and edgeuses, with the surfaces and, on B-spline faces, the trims. + let any_spline_face = brep.faces.iter().any(|f| matches!(f.surface, b::Surface::Spline { .. } | b::Surface::RationalSpline { .. })); + for f in &brep.faces { + a.i(flat::FACE_LOOP_COUNT).push(int(f.loops.len())?); + a.i(flat::FACE_TRIM_TYPE).push(0); + let spline = surface(&mut a, &f.surface, f.domain)?; + for l in &f.loops { + match l { + b::Loop::Vertex { vertex } => { + a.i(flat::LOOP_EDGEUSE_COUNT).push(0); + a.i(flat::LOOP_VERTEX).push(i64::from(*vertex)); + } + b::Loop::Edges { edges } => { + a.i(flat::LOOP_EDGEUSE_COUNT).push(int(edges.len())?); + a.i(flat::LOOP_VERTEX).push(-1); + for u in edges { + a.i(flat::EDGEUSE_EDGE).push(i64::from(u.edge)); + a.i(flat::EDGEUSE_ORIENTATION).push(i64::from(u.forward)); + if any_spline_face { + if spline { + trim(&mut a, &u.pcurve, u.forward)?; + } else { + a.i(flat::EDGEUSE_NURBS_VERTEX_COUNT).push(0); + a.i(flat::EDGEUSE_NURBS_ORDER).push(0); + } + } + } + } + } + } + } + a.i(flat::EDGEUSE_NEXT_RADIAL).extend(next); + a.i(flat::EDGEUSE_RADIAL_ENTRY).extend(entry); + + for e in &brep.edges { + curve(&mut a, &e.curve)?; + a.i(flat::EDGE_VERTICES).extend([i64::from(e.vertices[0]), i64::from(e.vertices[1])]); + a.r(flat::EDGE_RANGE).extend(e.interval); + } + + let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]); + for v in &brep.vertices { + a.i(flat::VERTEX_POINT_TYPE).push(1); + a.r3(flat::VERTEX_POSITION, v.point); + for k in 0..3 { + lo[k] = lo[k].min(v.point[k]); + hi[k] = hi[k].max(v.point[k]); + } + } + if brep.vertices.is_empty() { + (lo, hi) = ([0.0; 3], [0.0; 3]); + } + a.r(flat::EXTENT).extend(lo.into_iter().chain(hi)); + a.r(flat::TOLERANCE).push(brep.tolerance); + a.i(flat::SOURCE).push(i64::from(trusted)); + Ok(a) +} + +fn idx(i: usize) -> R { + u32::try_from(i).map_err(|_| Unsupported::Invalid("index")) +} + +/// A NURBS net's control points and weights, cartesian, U-major. +fn net(a: &mut Arrays, rows: impl Iterator) { + for (p, w) in rows { + a.r3(flat::FACE_NURBS_CVS, p); + a.r(flat::FACE_NURBS_WEIGHTS).push(w); + } +} + +fn nurbs_face(a: &mut Arrays, u_knots: &[f64], v_knots: &[f64], nu: usize, nv: usize) -> R<()> { + let order = |k: &[f64], n: usize| k.len().checked_sub(n).filter(|o| *o >= 2).ok_or(Unsupported::Invalid("spline order")); + a.i(flat::FACE_SURFACE_TYPE).push(i64::from(flat::SURFACE_NURBS)); + a.i(flat::FACE_NURBS_U_VERTEX_COUNT).push(int(nu)?); + a.i(flat::FACE_NURBS_V_VERTEX_COUNT).push(int(nv)?); + a.i(flat::FACE_NURBS_U_ORDER).push(int(order(u_knots, nu)?)?); + a.i(flat::FACE_NURBS_V_ORDER).push(int(order(v_knots, nv)?)?); + a.r(flat::FACE_NURBS_U_KNOTS).extend_from_slice(u_knots); + a.r(flat::FACE_NURBS_V_KNOTS).extend_from_slice(v_knots); + Ok(()) +} + +/// The face's surface and range; true for a B-spline surface (which carries trims). +fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2]) -> R { + let [[u0, u1], [v0, v1]] = domain; + let analytic = |a: &mut Arrays, kind: u8, ids: [i32; 3], f: &b::Frame| { + a.i(flat::FACE_SURFACE_TYPE).push(i64::from(kind)); + a.r3(ids[0], f.origin); + a.r3(ids[1], z_axis(f)); + a.r3(ids[2], f.x_axis); + a.r(flat::FACE_RANGE).extend([u0, v0, u1, v1]); + }; + match s { + b::Surface::Plane { origin, u_axis, v_axis } => plane(a, *origin, *u_axis, *v_axis, domain)?, + b::Surface::PlaneThrough { origin, u_point, v_point } => plane(a, *origin, sub(*u_point, *origin), sub(*v_point, *origin), domain)?, + b::Surface::Cylinder { frame, radius } => { + analytic(a, flat::SURFACE_CYLINDER, [flat::FACE_CYLINDER_ORIGIN, flat::FACE_CYLINDER_AXIS, flat::FACE_CYLINDER_REF_DIRECTION], frame); + a.r(flat::FACE_CYLINDER_RADIUS).push(*radius); + } + b::Surface::Cone { frame, radius_at_origin, half_angle } => { + analytic(a, flat::SURFACE_CONE, [flat::FACE_CONE_ORIGIN, flat::FACE_CONE_AXIS, flat::FACE_CONE_REF_DIRECTION], frame); + a.r(flat::FACE_CONE_RADIUS).push(*radius_at_origin); + a.r(flat::FACE_CONE_SEMI_ANGLE).push(*half_angle); + } + b::Surface::Sphere { frame, radius } => { + analytic(a, flat::SURFACE_SPHERE, [flat::FACE_SPHERE_CENTER, flat::FACE_SPHERE_AXIS, flat::FACE_SPHERE_REF_DIRECTION], frame); + a.r(flat::FACE_SPHERE_RADIUS).push(*radius); + } + b::Surface::Torus { frame, major_radius, minor_radius } => { + analytic(a, flat::SURFACE_TORUS, [flat::FACE_TORUS_ORIGIN, flat::FACE_TORUS_AXIS, flat::FACE_TORUS_REF_DIRECTION], frame); + a.r(flat::FACE_TORUS_MAJOR_RADIUS).push(*major_radius); + a.r(flat::FACE_TORUS_MINOR_RADIUS).push(*minor_radius); + } + b::Surface::Spline { u_knots, v_knots, points } => { + let nv = points.first().map_or(0, Vec::len); + nurbs_face(a, u_knots, v_knots, points.len(), nv)?; + net(a, points.iter().flatten().map(|p| (*p, 1.0))); + a.r(flat::FACE_RANGE).extend([u0, v0, u1, v1]); + return Ok(true); + } + b::Surface::RationalSpline { u_knots, v_knots, homogeneous } => { + let nv = homogeneous.first().map_or(0, Vec::len); + nurbs_face(a, u_knots, v_knots, homogeneous.len(), nv)?; + net(a, homogeneous.iter().flatten().map(|h| ([h[0] / h[3], h[1] / h[3], h[2] / h[3]], h[3]))); + a.r(flat::FACE_RANGE).extend([u0, v0, u1, v1]); + return Ok(true); + } + b::Surface::PlacedRevolution { .. } => return Err(Unsupported::NotYet("revolved surfaces")), + } + Ok(false) +} + +/// A plane `origin + u·U + v·V`. With unit, orthogonal axes it is SMLib's plane, whose (u, v) are +/// distances along them; otherwise it is the exact bilinear patch over the domain. +fn plane(a: &mut Arrays, origin: [f64; 3], u: [f64; 3], v: [f64; 3], domain: [[f64; 2]; 2]) -> R<()> { + let [[u0, u1], [v0, v1]] = domain; + let unit = |x: [f64; 3]| (dot(x, x).sqrt() - 1.0).abs() <= 1e-12; + if unit(u) && unit(v) && dot(u, v).abs() <= 1e-12 { + a.i(flat::FACE_SURFACE_TYPE).push(i64::from(flat::SURFACE_PLANE)); + a.r3(flat::FACE_PLANE_ORIGIN, origin); + a.r3(flat::FACE_PLANE_AXIS, cross(u, v)); + a.r3(flat::FACE_PLANE_REF_DIRECTION, u); + a.r(flat::FACE_RANGE).extend([u0, v0, u1, v1]); + return Ok(()); + } + nurbs_face(a, &[u0, u0, u1, u1], &[v0, v0, v1, v1], 2, 2)?; + let at = |s: f64, t: f64| add(origin, add(scale(u, s), scale(v, t))); + net(a, [at(u0, v0), at(u0, v1), at(u1, v0), at(u1, v1)].into_iter().map(|p| (p, 1.0))); + a.r(flat::FACE_RANGE).extend([u0, v0, u1, v1]); + Ok(()) +} + +/// A pcurve as the edgeuse's UV trim: reversed when the loop runs against the edge. +fn trim(a: &mut Arrays, c: &b::Curve2, forward: bool) -> R<()> { + let (knots, mut pts): (&[f64], Vec<([f64; 2], f64)>) = match c { + b::Curve2::Spline { knots, points } => (knots, points.iter().map(|p| (*p, 1.0)).collect()), + b::Curve2::RationalSpline { knots, homogeneous } => (knots, homogeneous.iter().map(|h| ([h[0] / h[2], h[1] / h[2]], h[2])).collect()), + }; + let order = knots.len().checked_sub(pts.len()).filter(|o| *o >= 2).ok_or(Unsupported::Invalid("pcurve order"))?; + let mut k = knots.to_vec(); + if !forward { + pts.reverse(); + k = k.iter().rev().map(|t| -t).collect(); + } + a.i(flat::EDGEUSE_NURBS_VERTEX_COUNT).push(int(pts.len())?); + a.i(flat::EDGEUSE_NURBS_ORDER).push(int(order)?); + for (p, w) in pts { + a.r(flat::EDGEUSE_NURBS_CVS).extend(p); + a.r(flat::EDGEUSE_NURBS_WEIGHTS).push(w); + } + a.r(flat::EDGEUSE_NURBS_KNOTS).extend(k); + Ok(()) +} + +fn nurbs_curve(a: &mut Arrays, knots: &[f64], pts: impl ExactSizeIterator) -> R<()> { + let n = pts.len(); + let order = knots.len().checked_sub(n).filter(|o| *o >= 2).ok_or(Unsupported::Invalid("spline order"))?; + a.i(flat::EDGE_CURVE_TYPE).push(i64::from(flat::CURVE_NURBS)); + a.i(flat::EDGE_NURBS_VERTEX_COUNT).push(int(n)?); + a.i(flat::EDGE_NURBS_ORDER).push(int(order)?); + for (p, w) in pts { + a.r3(flat::EDGE_NURBS_CVS, p); + a.r(flat::EDGE_NURBS_WEIGHTS).push(w); + } + a.r(flat::EDGE_NURBS_KNOTS).extend_from_slice(knots); + Ok(()) +} + +fn curve(a: &mut Arrays, c: &b::Curve) -> R<()> { + match c { + b::Curve::Line { start, end } => { + a.i(flat::EDGE_CURVE_TYPE).push(i64::from(flat::CURVE_LINE)); + a.r3(flat::EDGE_LINE_ORIGIN, *start); + a.r3(flat::EDGE_LINE_DIRECTION, sub(*end, *start)); + } + b::Curve::Circle { frame, radius } => { + a.i(flat::EDGE_CURVE_TYPE).push(i64::from(flat::CURVE_CIRCLE)); + a.r3(flat::EDGE_CIRCLE_CENTER, frame.origin); + a.r3(flat::EDGE_CIRCLE_AXIS, z_axis(frame)); + a.r3(flat::EDGE_CIRCLE_REF_DIRECTION, frame.x_axis); + a.r(flat::EDGE_CIRCLE_RADIUS).push(*radius); + } + b::Curve::Ellipse { frame, radii } => { + a.i(flat::EDGE_CURVE_TYPE).push(i64::from(flat::CURVE_ELLIPSE)); + a.r3(flat::EDGE_ELLIPSE_CENTER, frame.origin); + a.r3(flat::EDGE_ELLIPSE_AXIS, z_axis(frame)); + a.r3(flat::EDGE_ELLIPSE_REF_DIRECTION, frame.x_axis); + a.r(flat::EDGE_ELLIPSE_X_RADIUS).push(radii[0]); + a.r(flat::EDGE_ELLIPSE_Y_RADIUS).push(radii[1]); + } + b::Curve::Polyline { points } => { + // Segment i spans [i, i + 1]: a degree-1 B-spline with those knots. + let n = points.len(); + let mut k = vec![0.0]; + k.extend((0..n).map(|i| i as f64)); + k.push(n.saturating_sub(1) as f64); + nurbs_curve(a, &k, points.iter().map(|p| (*p, 1.0)))?; + } + b::Curve::Spline { knots, points } => nurbs_curve(a, knots, points.iter().map(|p| (*p, 1.0)))?, + b::Curve::RationalSpline { knots, homogeneous } => { + nurbs_curve(a, knots, homogeneous.iter().map(|h| ([h[0] / h[3], h[1] / h[3], h[2] / h[3]], h[3])))? + } + b::Curve::SurfaceIntersection { .. } => return Err(Unsupported::NotYet("traced intersection curves")), + } + Ok(()) +} + +#[cfg(test)] +mod tests; + +impl crate::Brep { + /// The SMLib B-rep of a schema 4 record. `trusted` as in [`arrays`]. + pub fn from_record(brep: &b::Brep, trusted: bool) -> crate::Result { + let a = arrays(brep, trusted).map_err(crate::Error::Record)?; + crate::flat::Flat::from_arrays(&a)?.import() + } +} diff --git a/crates/smlib/src/import/tests.rs b/crates/smlib/src/import/tests.rs new file mode 100644 index 0000000000..32a43f3393 --- /dev/null +++ b/crates/smlib/src/import/tests.rs @@ -0,0 +1,152 @@ +use super::*; +use solidcraft_geom::brep::{ + Brep as Record, Curve, Curve2, Edge, EdgeUseRef, Face, Loop, OrientedEdge, OrientedFace, Region, Shell, Surface, Vertex, +}; + +/// A closed polyhedron record from its corner points and its faces, each a cycle of corner +/// indices counter-clockwise seen from outside: planar faces, straight edges, canonical order. +fn polyhedron(corners: &[[f64; 3]], cycles: &[&[usize]]) -> Record { + let unit = |a: [f64; 3]| { + let n = dot(a, a).sqrt(); + scale(a, 1.0 / n) + }; + let mut edge_of: HashMap<(usize, usize), u32> = HashMap::new(); + let mut ends: Vec<(usize, usize)> = Vec::new(); + let mut uses: Vec> = Vec::new(); + let mut faces = Vec::new(); + for (fi, cycle) in cycles.iter().enumerate() { + let o = corners[cycle[0]]; + let x = unit(sub(corners[cycle[1]], o)); + let z = unit(cross(x, sub(corners[cycle[cycle.len() - 1]], o))); + let y = cross(z, x); + let uv = |p: [f64; 3]| [dot(sub(p, o), x), dot(sub(p, o), y)]; + let mut edges = Vec::new(); + let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]); + for k in 0..cycle.len() { + let (a, b) = (cycle[k], cycle[(k + 1) % cycle.len()]); + let (e, forward) = match edge_of.get(&(b, a)) { + Some(&e) => (e, false), + None => { + let e = ends.len() as u32; + edge_of.insert((a, b), e); + ends.push((a, b)); + uses.push(Vec::new()); + (e, true) + } + }; + uses[e as usize].push(EdgeUseRef { face: fi as u32, r#loop: 0, position: k as u32, upper: !forward }); + let (s, t) = ends[e as usize]; + for p in [uv(corners[s]), uv(corners[t])] { + for i in 0..2 { + lo[i] = lo[i].min(p[i]); + hi[i] = hi[i].max(p[i]); + } + } + edges.push(OrientedEdge { + edge: e, + forward, + pcurve: Curve2::Spline { knots: vec![0.0, 0.0, 1.0, 1.0], points: vec![uv(corners[s]), uv(corners[t])] }, + }); + } + faces.push(Face { + surface: Surface::Plane { origin: o, u_axis: x, v_axis: y }, + domain: [[lo[0], hi[0]], [lo[1], hi[1]]], + loops: vec![Loop::Edges { edges }], + }); + } + // Vertices in first appearance along the edges. + let mut vertex_of: HashMap = HashMap::new(); + let mut vertices = Vec::new(); + let mut number = |c: usize, vertices: &mut Vec| { + *vertex_of.entry(c).or_insert_with(|| { + vertices.push(Vertex { point: corners[c], tolerance: 1e-6 }); + vertices.len() as u32 - 1 + }) + }; + let edges = ends + .iter() + .zip(uses) + .map(|(&(s, t), uses)| { + let vs = [number(s, &mut vertices), number(t, &mut vertices)]; + Edge { curve: Curve::Line { start: corners[s], end: corners[t] }, interval: [0.0, 1.0], vertices: vs, tolerance: 1e-6, uses } + }) + .collect(); + Record { + version: solidcraft_geom::brep::VERSION, + tolerance: 1e-6, + regions: vec![Region { shells: vec![0], void: false }, Region { shells: vec![0], void: false }], + shells: vec![Shell::Faces { faces: (0..cycles.len() as u32).map(|face| OrientedFace { face, forward: true }).collect(), wires: vec![] }], + faces, + edges, + vertices, + } +} + +fn cube(s: f64) -> Record { + let c = [[0., 0., 0.], [s, 0., 0.], [s, s, 0.], [0., s, 0.], [0., 0., s], [s, 0., s], [s, s, s], [0., s, s]]; + polyhedron(&c, &[&[0, 3, 2, 1], &[4, 5, 6, 7], &[0, 1, 5, 4], &[1, 2, 6, 5], &[2, 3, 7, 6], &[3, 0, 4, 7]]) +} + +#[test] +fn a_cube_record_is_valid_and_maps_to_two_shells() { + let r = cube(10.0); + r.validate().unwrap(); + let a = arrays(&r, true).unwrap(); + // The infinite region (a void in SMLib) and the solid, one shell each, every face once per side. + assert_eq!(a.ints[&flat::REGION_SHELL_COUNT], vec![1, 1]); + assert_eq!(a.ints[&flat::REGION_TYPE], vec![1, 0]); + assert_eq!(a.ints[&flat::SHELL_FACEUSE_COUNT], vec![6, 6]); + assert_eq!(a.ints[&flat::FACEUSE_ORIENTATION], [vec![1; 6], vec![0; 6]].concat()); + assert_eq!(a.ints[&flat::EDGE_CURVE_TYPE], vec![i64::from(flat::CURVE_LINE); 12]); + assert_eq!(a.ints[&flat::FACE_SURFACE_TYPE], vec![i64::from(flat::SURFACE_PLANE); 6]); + // Each edge's two uses cycle into each other; no trims on analytic faces. + let next = &a.ints[&flat::EDGEUSE_NEXT_RADIAL]; + assert_eq!(next.len(), 24); + assert!(next.iter().enumerate().all(|(i, &n)| n != i as i64 && next[n as usize] == i as i64)); + assert!(!a.ints.contains_key(&flat::EDGEUSE_NURBS_VERTEX_COUNT)); +} + +#[test] +fn records_smlib_cannot_take_are_errors() { + let mut r = cube(1.0); + r.version = 3; + assert!(matches!(arrays(&r, true), Err(Unsupported::Invalid(_)))); + let mut r = cube(1.0); + r.shells = vec![Shell::Vertex { vertex: 0 }]; + assert!(arrays(&r, true).is_err()); +} + +#[cfg(feature = "native")] +#[test] +fn a_cube_record_imports_into_smlib() { + let b = crate::Brep::from_record(&cube(10.0), true).unwrap(); + assert!((b.volume(1e-7).unwrap() - 1000.0).abs() < 1e-6); + assert_eq!(b.counts().unwrap(), [6, 12, 8]); + assert!(b.is_manifold_solid().unwrap()); +} + +#[cfg(feature = "native")] +#[test] +fn a_skewed_plane_goes_as_an_exact_bilinear_patch() { + // A tetrahedron: its faces' natural frames are unit and orthogonal, so make one plane's axes + // non-unit to force the NURBS form and check the volume is unchanged. + let c = [[0., 0., 0.], [6., 0., 0.], [0., 6., 0.], [0., 0., 6.]]; + let mut r = polyhedron(&c, &[&[0, 2, 1], &[0, 1, 3], &[1, 2, 3], &[2, 0, 3]]); + if let Some(Face { surface: Surface::Plane { u_axis, v_axis, .. }, domain, loops }) = r.faces.get_mut(0) { + *u_axis = scale(*u_axis, 2.0); + *v_axis = scale(*v_axis, 2.0); + *domain = domain.map(|d| d.map(|x| x / 2.0)); + if let Some(Loop::Edges { edges }) = loops.get_mut(0) { + for e in edges { + if let Curve2::Spline { points, .. } = &mut e.pcurve { + points.iter_mut().for_each(|p| *p = p.map(|x| x / 2.0)); + } + } + } + } + r.validate().unwrap(); + let a = arrays(&r, true).unwrap(); + assert_eq!(a.ints[&flat::FACE_SURFACE_TYPE][0], i64::from(flat::SURFACE_NURBS)); + let b = crate::Brep::from_record(&r, true).unwrap(); + assert!((b.volume(1e-7).unwrap() - 36.0).abs() < 1e-6, "{:?}", b.volume(1e-7)); +} diff --git a/crates/smlib/src/lib.rs b/crates/smlib/src/lib.rs index a92941e22f..95f5313977 100644 --- a/crates/smlib/src/lib.rs +++ b/crates/smlib/src/lib.rs @@ -17,7 +17,9 @@ use std::sync::atomic::{AtomicI32, Ordering}; mod check; mod ffi; +pub mod flat; pub mod history; +pub mod import; mod measure; pub use check::{CheckLevel, CheckReport}; @@ -38,6 +40,8 @@ pub enum Error { Version(i32), /// This build has no SMLib (the `native` feature is off). NotBuilt, + /// A B-rep record SMLib cannot take (see [`import::Unsupported`]). + Record(import::Unsupported), } impl std::fmt::Display for Error { @@ -50,6 +54,7 @@ impl std::fmt::Display for Error { Error::Exception => write!(f, "SMLib raised an internal exception"), Error::Version(v) => write!(f, "SMLib C API version {v}, expected {}", ffi::ABI_VERSION), Error::NotBuilt => write!(f, "not supported yet: this build has no SMLib"), + Error::Record(u) => write!(f, "{u}"), } } }