mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
solidcraft-smlib: schema 4 records into SMLib through the flat arrays
SMLIB_REV moves to the fork's capi-static 40bb855 (flat arrays, C ABI 2). flat.rs wraps them (Flat: arrays by id, Brep::export, Flat::import, Brep::is_manifold_solid; FFI in ffi/flat.rs). import.rs lays a schema 4 record out as those arrays: a closed shell becomes one SMLib shell per region it separates, a sheet's shell holds both sides of each face, the infinite region is marked void as SMLib has it, pcurves become UV trims on B-spline faces only (reversed for edgeuses against their edge), planes with non-unit axes go as their exact bilinear patch. Brep::from_record builds the SMLib body. kernel: brep_record(body) gives a body's schema 4 record, and smlib_body (feature smlib) its SMLib body, trusted (no healer: healing moved a cylinder's volume by 9e-7). Box, cylinder and tilted cylinder keep their analytic volume in SMLib (1e-9, 1e-6) and are manifold solids; truck's spheres and tori are revolutions, refused with "not supported yet" for now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -190,7 +190,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,
|
||||
|
||||
@@ -82,6 +82,26 @@ pub fn encode_native_body(body: &Body) -> Result<Vec<u8>> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<solidcraft_geom::brep::Brep> {
|
||||
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<solidcraft_smlib::Brep> {
|
||||
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<Body> {
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
3e95b7905ed9e2ba355b0ca9954295ac002d2925
|
||||
40bb855d15391428697d086fd122409c10e5ffee
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<RawFlat>);
|
||||
|
||||
// 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<FlatHandle, i32> {
|
||||
std::ptr::NonNull::new(unsafe { smlib_flat_new() }).map(FlatHandle).ok_or(ERR_EXCEPTION)
|
||||
}
|
||||
pub fn export(b: &Handle) -> Result<FlatHandle, i32> {
|
||||
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<Handle, i32> {
|
||||
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<T: Copy>(s: i32, data: *const T, len: i64) -> Result<Vec<T>, 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<Vec<i64>, 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<Vec<f64>, 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<bool, i32> {
|
||||
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<FlatHandle, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
pub fn export(_: &Handle) -> Result<FlatHandle, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
pub fn import(_: &FlatHandle) -> Result<Handle, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
pub fn get_i64(_: &FlatHandle, _: i32) -> Result<Vec<i64>, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
pub fn get_f64(_: &FlatHandle, _: i32) -> Result<Vec<f64>, 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<bool, i32> {
|
||||
Err(NOT_BUILT)
|
||||
}
|
||||
}
|
||||
|
||||
pub use calls::*;
|
||||
@@ -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<Flat> {
|
||||
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<Flat> {
|
||||
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<Vec<i64>> {
|
||||
ffi::flat::get_i64(&self.0, id).map_err(error)
|
||||
}
|
||||
pub fn get_f64(&self, id: i32) -> Result<Vec<f64>> {
|
||||
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<Brep> {
|
||||
init()?;
|
||||
ffi::flat::import(&self.0).map(Brep).map_err(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl Brep {
|
||||
/// This body as flat arrays.
|
||||
pub fn export(&self) -> Result<Flat> {
|
||||
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<bool> {
|
||||
ffi::flat::is_manifold_solid(&self.0).map_err(error)
|
||||
}
|
||||
}
|
||||
@@ -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<T> = std::result::Result<T, Unsupported>;
|
||||
|
||||
/// The flat arrays of one B-rep, by array id.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Arrays {
|
||||
pub ints: BTreeMap<i32, Vec<i64>>,
|
||||
pub reals: BTreeMap<i32, Vec<f64>>,
|
||||
}
|
||||
|
||||
impl Arrays {
|
||||
fn i(&mut self, id: i32) -> &mut Vec<i64> {
|
||||
self.ints.entry(id).or_default()
|
||||
}
|
||||
fn r(&mut self, id: i32) -> &mut Vec<f64> {
|
||||
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> {
|
||||
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<Arrays> {
|
||||
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<u32, usize> =
|
||||
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> {
|
||||
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<Item = ([f64; 3], f64)>) {
|
||||
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<bool> {
|
||||
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<Item = ([f64; 3], f64)>) -> 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<crate::Brep> {
|
||||
let a = arrays(brep, trusted).map_err(crate::Error::Record)?;
|
||||
crate::flat::Flat::from_arrays(&a)?.import()
|
||||
}
|
||||
}
|
||||
@@ -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<EdgeUseRef>> = 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<usize, u32> = HashMap::new();
|
||||
let mut vertices = Vec::new();
|
||||
let mut number = |c: usize, vertices: &mut Vec<Vertex>| {
|
||||
*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));
|
||||
}
|
||||
@@ -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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user