diff --git a/Cargo.lock b/Cargo.lock index ef5c9e582c..6e826f7b6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4285,14 +4285,12 @@ dependencies = [ name = "solidcraft-smlib" version = "0.30.1" dependencies = [ - "cc", "flate2", "log", "serde_json", "solidcore", "solidcraft-geom", "solidcraft-profile", - "web-time", ] [[package]] diff --git a/apps/solidcraft-cli/Cargo.toml b/apps/solidcraft-cli/Cargo.toml index ed69dc559e..3e97990023 100644 --- a/apps/solidcraft-cli/Cargo.toml +++ b/apps/solidcraft-cli/Cargo.toml @@ -23,10 +23,6 @@ path = "src/alias.rs" default = ["raytrace-cpu", "profile"] bundled-render-library = ["solidcraft-engine/bundled-render-library"] raytrace-cpu = ["solidcraft-engine/raytrace-cpu"] -# Also link the C++ SMLib kernel (solidcraft-engine/smlib), off by default: needs a C++ toolchain -# and scripts/fetch-smlib.sh. solidcore, the Rust kernel, is always built and is the default either -# way; with this on, SOLIDCRAFT_KERNEL=smlib or the Geometry kernel preference can choose C++. -smlib = ["solidcraft-engine/smlib"] # The profiler, compiled in so any build can say where its time goes (`--profile`, # SOLIDCRAFT_PROFILE=1); it records nothing until asked. profile = ["solidcraft-engine/profile"] diff --git a/apps/solidcraft-cli/src/kernel_cases.rs b/apps/solidcraft-cli/src/kernel_cases.rs index e1c9247525..772aac4e2d 100644 --- a/apps/solidcraft-cli/src/kernel_cases.rs +++ b/apps/solidcraft-cli/src/kernel_cases.rs @@ -692,31 +692,21 @@ fn check_case(dir: &str, dumps: Option<&std::path::Path>, validation: solidcraft return out; } }; - // The C++ kernel is needed only if it is the one asked for. - if !solidcraft_engine::kernel::smlib_built() - && solidcraft_engine::kernel::kernel_route::backend() == solidcraft_engine::kernel::kernel_route::Backend::Smlib - { - out["status"] = json!("unsupported"); - out["error"] = json!("this build doesn't include the C++ SMLib kernel; build solidcraft-cli with --features smlib"); - return out; - } // `refused`: only a clean refusal is right; `solid_or_refused`: a clean refusal, or a result // graded as usual (where the geometry has no manifold answer, such as bodies touching along a // curve only). let outcome = want["outcome"].as_str().unwrap_or("solid"); let refused = outcome == "refused"; let may_refuse = refused || outcome == "solid_or_refused"; - // The SMLib calls that fail in this case leave their inputs in its own folder. + // The kernel calls that fail in this case leave their inputs in its own folder. if let Some(root) = dumps { let folder = root.join(format!("{area}-{name}")); - if let Err(e) = std::fs::create_dir_all(&folder) - .map_err(|e| e.to_string()) - .and_then(|()| solidcraft_engine::kernel::smlib_dump_dir(Some(&folder)).map_err(|e| e.to_string())) - { + if let Err(e) = std::fs::create_dir_all(&folder) { out["status"] = json!("fail"); out["error"] = json!(format!("--dumps: {}: {e}", folder.display())); return out; } + solidcraft_engine::kernel::smlib_dump_dir(Some(&folder)); } let mut s = solidcraft_engine::Session::default(); s.set_validation(validation); @@ -988,7 +978,7 @@ pub fn run(args: &[String]) -> Result<(), String> { }; let results: Vec = dirs.iter().map(|d| check_case(d, dumps.as_deref(), validation)).collect(); if dumps.is_some() { - solidcraft_engine::kernel::smlib_dump_dir(None).map_err(|e| format!("kernel-cases: --dumps: {e}"))?; + solidcraft_engine::kernel::smlib_dump_dir(None); } report(args, &results); let bad = results.iter().filter(|r| matches!(r["status"].as_str(), Some("fail" | "crash"))).count(); diff --git a/apps/solidcraft-web/Cargo.toml b/apps/solidcraft-web/Cargo.toml index 57b7c539f9..45d1e52153 100644 --- a/apps/solidcraft-web/Cargo.toml +++ b/apps/solidcraft-web/Cargo.toml @@ -16,10 +16,6 @@ path = "src/main.rs" [features] default = ["profile"] bundled-render-library = ["solidcraft-engine/bundled-render-library"] -# Also link the C++ SMLib kernel (solidcraft-engine/smlib), off by default: needs -# scripts/fetch-smlib.sh wasm32-unknown-unknown, or the sources and wasi-sdk in WASI_SDK_PATH -# (crates/smlib/build.rs). solidcore, the Rust kernel, is always built and is the default either way. -smlib = ["solidcraft-engine/smlib"] # The profiler, compiled in so any build can say where its time goes (the Profile operations # preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked. profile = ["solidcraft-engine/profile"] diff --git a/apps/solidcraft/Cargo.toml b/apps/solidcraft/Cargo.toml index 6860706a12..094f8ae79c 100644 --- a/apps/solidcraft/Cargo.toml +++ b/apps/solidcraft/Cargo.toml @@ -11,10 +11,6 @@ default-run = "solidcraft" default = ["raytrace-cpu", "bundled-render-library", "profile"] bundled-render-library = ["solidcraft-engine/bundled-render-library"] raytrace-cpu = ["solidcraft-engine/raytrace-cpu"] -# Also link the C++ SMLib kernel (solidcraft-engine/smlib), off by default: needs a C++ toolchain -# and scripts/fetch-smlib.sh. solidcore, the Rust kernel, is always built and is the default either -# way; with this on, SOLIDCRAFT_KERNEL=smlib or the Geometry kernel preference can choose C++. -smlib = ["solidcraft-engine/smlib"] # The profiler, compiled in so any build can say where its time goes (the Profile operations # preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked. profile = ["solidcraft-engine/profile"] diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 4a84293194..1aff221ef4 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -13,8 +13,6 @@ default = [] # Optional authored texture/HDR bytes; the command registry and metadata remain available. bundled-render-library = [] raytrace-cpu = ["solidcraft-render/raytrace-cpu"] -# Also link the C++ SMLib kernel (solidcraft-kernel/smlib, off by default; needs scripts/fetch-smlib.sh). -smlib = ["solidcraft-kernel/smlib"] # Turns on the parameter-contract audit recorder (`SOLIDCRAFT_PARAM_AUDIT`, see # `src/schema/record.rs`), for checking a new contract against the whole test suite. param-audit = [] diff --git a/crates/gpu/Cargo.toml b/crates/gpu/Cargo.toml index 2ba7d096a0..759dc816e8 100644 --- a/crates/gpu/Cargo.toml +++ b/crates/gpu/Cargo.toml @@ -26,8 +26,8 @@ wgpu = { workspace = true, features = ["metal", "vulkan", "dx12", "gles"] } [dev-dependencies] image = { workspace = true } solidcraft-kernel = { workspace = true } -# Headless pictures of real designs (capture tests), built by the real kernel: solidcore, as in the -# apps (the C++ SMLib kernel only when the build turns on solidcraft-engine/smlib). +# Headless pictures of real designs (capture tests), built by the real kernel, solidcore, as in the +# apps. solidcraft-engine = { workspace = true, features = ["bundled-render-library"] } solidcraft-io = { workspace = true } serde_json = { workspace = true } diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index 29afa888f7..57cc486384 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "solidcraft-kernel" -description = "SolidCraft B-rep kernel boundary: solids, extrude/revolve/booleans/fillets, meshes, measures and STEP, behind our own API (implemented with SMLib)" +description = "SolidCraft B-rep kernel boundary: solids, extrude/revolve/booleans/fillets, meshes, measures and STEP, behind our own API (implemented with solidcore)" version.workspace = true edition.workspace = true license.workspace = true @@ -8,12 +8,6 @@ license.workspace = true [lints] workspace = true -[features] -# Also link NVIDIA's C++ SMLib (tangency-labs/smlib), off by default; needs a C++ toolchain and -# scripts/fetch-smlib.sh (ADR 0002). solidcore, the Rust port, answers every operation either way; -# without this, asking for the C++ kernel (SOLIDCRAFT_KERNEL=smlib) is refused as not built. -smlib = ["solidcraft-smlib/native"] - [dependencies] solidcraft-geom = { workspace = true } solidcraft-smlib = { workspace = true } diff --git a/crates/kernel/examples/smlib_booleans.rs b/crates/kernel/examples/smlib_booleans.rs deleted file mode 100644 index 756be11c6d..0000000000 --- a/crates/kernel/examples/smlib_booleans.rs +++ /dev/null @@ -1,1056 +0,0 @@ -//! Side-by-side booleans: truck (today's kernel) and SMLib on the same cases, compared on -//! volume, face and edge counts, history and time. It reports; it never changes which kernel the -//! app uses. -//! -//! ```sh -//! scripts/fetch-smlib.sh -//! cargo run --release -p solidcraft-kernel --features smlib --example smlib_booleans -- \ -//! [--seeds N] [--out DIR] -//! ``` -//! -//! Cases: named pairs of primitives whose results are known by hand (the ones the kernel's -//! boolean and history tests build), then `fuzz::boolean_shapes(seed)` for seeds 0..N (the -//! random pairs of `solidcraft-cli fuzz` and the robustness runs). Each case runs union, -//! intersection and cut. Then one named sequence, each operation on the previous result: a -//! block, a boss joined on top, three through-cuts from the side faces, the last splitting the -//! body (the "each cut gets slower" case of TASK_kernel-smlib-booleans). Writes -//! `smlib-booleans.json` and `smlib-booleans.md` into DIR (default `target/smlib-booleans`). -//! -//! SMLib runs in a child process per case (this same program, with `--smlib-case`, and the -//! sequence with `--smlib-sequence`), so a crash inside it is reported as that case's result -//! instead of ending the run. -//! -//! Times are wall times of the boolean call alone (`boolean` on truck, `Brep::boolean` on -//! SMLib). The SMLib side's conversions are timed apart: the operands' truck→SMLib import -//! (`smlib_body`) and the result's SMLib→truck export (`body_from_smlib`), which the app pays -//! for as long as truck holds the bodies. -//! -//! History is compared by its shape, since the two kernels number faces differently until the -//! SMLib export renumbers into canonical order: how many result faces are Kept, Modified, Split, -//! Merged and Generated, and how many edges come from an operand edge, are new where two faces -//! meet, or have no origin. - -#[cfg(not(feature = "smlib"))] -fn main() { - eprintln!("smlib_booleans needs SMLib: run scripts/fetch-smlib.sh, then add --features smlib"); - std::process::exit(2); -} - -#[cfg(feature = "smlib")] -fn main() { - if let Err(e) = run::main() { - eprintln!("smlib_booleans: {e}"); - std::process::exit(1); - } -} - -#[cfg(feature = "smlib")] -mod run { - use std::f64::consts::{FRAC_PI_2, FRAC_PI_6, PI}; - use std::fmt::Write as _; - use std::path::PathBuf; - use std::time::Instant; - - use serde_json::{Value, json}; - use solidcraft_geom::brep::history::{Change, EdgeOrigin, History}; - use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3}; - use solidcraft_kernel::fuzz::{Shape, boolean_shapes}; - use solidcraft_kernel::{Body, BoolOp, boolean, extrude, measure}; - use solidcraft_smlib::MEASURE_ACCURACY; - use solidcraft_smlib::history::Order; - - const OPS: [BoolOp; 3] = [BoolOp::Union, BoolOp::Intersect, BoolOp::Cut]; - - /// Relative volume difference that still counts as the same result: truck measures from a - /// fine tessellation (`measure`), SMLib integrates exactly. - const VOLUME_REL: f64 = 1e-3; - - /// What one kernel made of one case: its volume, face, edge and solid counts, history shape - /// and times, or why it made nothing. - #[derive(Clone, Debug)] - struct Outcome { - error: Option, - empty: bool, - volume: f64, - faces: usize, - edges: usize, - solids: usize, - history: Option, - /// The boolean call alone. - ms: f64, - /// SMLib only: the operands' truck→SMLib import. - import_ms: f64, - /// SMLib only: the result's SMLib→truck export, and why it failed. - export_ms: f64, - export_error: Option, - /// SMLib only: truck's measure of the exported result (NaN when there is none), which - /// tells a wrong body from a wrong SMLib measure. - export_volume: f64, - } - - impl Default for Outcome { - fn default() -> Outcome { - Outcome { - error: None, - empty: false, - volume: 0.0, - faces: 0, - edges: 0, - solids: 0, - history: None, - ms: 0.0, - import_ms: 0.0, - export_ms: 0.0, - export_error: None, - export_volume: f64::NAN, - } - } - } - - /// A history's shape: faces Kept, Modified, Split, Merged, Generated; edges from an operand - /// edge, new where two faces meet, without origin. - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] - struct HistoryShape { - faces: [usize; 5], - edges: [usize; 3], - } - - impl HistoryShape { - fn of(h: &History) -> HistoryShape { - let mut s = HistoryShape::default(); - for f in &h.faces { - let i = match f.change { - Change::Kept => 0, - Change::Modified => 1, - Change::Split => 2, - Change::Merged => 3, - Change::Generated => 4, - }; - if let Some(n) = s.faces.get_mut(i) { - *n += 1; - } - } - for e in &h.edges { - let i = if e.origins.iter().any(|o| matches!(o, EdgeOrigin::Edge { .. })) { - 0 - } else if e.origins.is_empty() { - 2 - } else { - 1 - }; - if let Some(n) = s.edges.get_mut(i) { - *n += 1; - } - } - s - } - - fn text(&self) -> String { - let [k, m, s, g, n] = self.faces; - let [e, meet, none] = self.edges; - format!("K{k} M{m} S{s} J{g} G{n} / e{e} x{meet} -{none}") - } - } - - fn op_name(op: BoolOp) -> &'static str { - match op { - BoolOp::Union => "union", - BoolOp::Intersect => "intersect", - BoolOp::Cut => "cut", - } - } - - fn ms_since(t0: Instant) -> f64 { - t0.elapsed().as_secs_f64() * 1e3 - } - - /// `a op b` on truck, timing the boolean alone. - fn truck_bodies(a: &Body, b: &Body, op: BoolOp) -> (Outcome, Option) { - let mut o = Outcome::default(); - let t0 = Instant::now(); - let result = boolean(a, b, op); - o.ms = ms_since(t0); - match result { - Err(e) => o.error = Some(e.to_string()), - Ok(None) => o.empty = true, - Ok(Some(body)) => { - o.faces = body.face_count(); - o.edges = body.edge_count(); - o.solids = body.shell_count(); - o.history = body.history().map(HistoryShape::of); - match measure(&body) { - Ok(m) => o.volume = m.volume, - Err(e) => o.error = Some(format!("measure: {e}")), - } - return (o, Some(body)); - } - } - (o, None) - } - - fn truck(a: &Shape, b: &Shape, op: BoolOp) -> Outcome { - match a.body().and_then(|ba| b.body().map(|bb| (ba, bb))) { - Err(e) => Outcome { error: Some(format!("truck primitive: {e}")), ..Outcome::default() }, - Ok((ba, bb)) => truck_bodies(&ba, &bb, op).0, - } - } - - /// `a op b` on SMLib, operands already in SMLib: tagged as operands 0 and 1, the boolean - /// timed alone, then measured exactly and exported back to truck (timed apart). - fn smlib_breps(a: &solidcraft_smlib::Brep, b: &solidcraft_smlib::Brep, op: BoolOp) -> (Outcome, Option) { - let mut o = Outcome::default(); - let tagged = a.tagged(0).and_then(|ta| b.tagged(1).map(|tb| (ta, tb))); - let (ta, tb) = match tagged { - Ok(t) => t, - Err(e) => return (Outcome { error: Some(format!("tag: {e}")), ..o }, None), - }; - let t0 = Instant::now(); - let result = side::boolean(&ta, &tb, op); - o.ms = ms_since(t0); - match result { - Err(e) => o.error = Some(e), - Ok(None) => o.empty = true, - Ok(Some(r)) => { - let counts = r.counts().map_err(|e| e.to_string()); - let volume = r.mass_properties(MEASURE_ACCURACY).map(|m| m.volume).map_err(|e| e.to_string()); - let history = r - .history() - .map_err(|e| e.to_string()) - .and_then(|raw| raw.history(&[Order::default(), Order::default()], &Order::default()).map_err(|e| e.to_string())); - match (counts, volume) { - (Ok([f, e, _]), Ok(v)) => { - o.faces = f; - o.edges = e; - o.volume = v; - } - (Err(e), _) | (_, Err(e)) => o.error = Some(e), - } - o.solids = r.solid_count().unwrap_or(0); - o.history = history.ok().as_ref().map(HistoryShape::of); - let t0 = Instant::now(); - let back = solidcraft_kernel::body_from_smlib(&r); - o.export_ms = ms_since(t0); - match back.map_err(|e| e.to_string()).and_then(|b| measure(&b).map_err(|e| format!("measure: {e}"))) { - Ok(m) => o.export_volume = m.volume, - Err(e) => o.export_error = Some(e), - } - return (o, Some(r)); - } - } - (o, None) - } - - fn smlib(a: &Shape, b: &Shape, op: BoolOp) -> Outcome { - let t0 = Instant::now(); - let operands = side::shape(a).and_then(|sa| side::shape(b).map(|sb| (sa, sb))); - let import_ms = ms_since(t0); - match operands { - Err(e) => Outcome { error: Some(e), import_ms, ..Outcome::default() }, - Ok(((sa, ia), (sb, ib))) => { - let (mut o, _) = smlib_breps(&sa, &sb, op); - o.import_ms = if ia || ib { import_ms } else { 0.0 }; - o - } - } - } - - /// The SMLib side, through `solidcraft-smlib`. Boxes and cylinders are the very B-reps truck - /// starts from, through their schema 4 records imported into SMLib (`smlib_body`), so both - /// kernels get the same input whatever the axis. Spheres and tori are SMLib's own primitives - /// (about +Z, like truck's): the import doesn't take truck's revolved surfaces yet. The - /// boolean is SMLib's own (`Brep::boolean`, through smlib_ops.cpp). - mod side { - use solidcraft_kernel::BoolOp; - use solidcraft_kernel::fuzz::Shape; - use solidcraft_smlib::{BooleanOp, Brep}; - - /// The shape in SMLib, and whether it was imported from truck (rather than made there). - pub fn shape(s: &Shape) -> Result<(Brep, bool), String> { - match s { - Shape::Sphere { center, radius } => return Brep::make_sphere(*center, *radius).map(|b| (b, false)).map_err(|e| e.to_string()), - Shape::Torus { center, major, minor } => { - return Brep::make_torus(*center, *major, *minor).map(|b| (b, false)).map_err(|e| e.to_string()); - } - Shape::Box { .. } | Shape::Cylinder { .. } => {} - } - let body = s.body().map_err(|e| format!("truck primitive: {e}"))?; - solidcraft_kernel::smlib_body(&body).map(|b| (b, true)).map_err(|e| format!("import: {e}")) - } - - /// `a op b`; `None` when the result is empty. - pub fn boolean(a: &Brep, b: &Brep, op: BoolOp) -> Result, String> { - let op = match op { - BoolOp::Union => BooleanOp::Union, - BoolOp::Intersect => BooleanOp::Intersect, - BoolOp::Cut => BooleanOp::Subtract, - }; - a.boolean(b, op).map_err(|e| e.to_string()) - } - } - - /// ∫ over the disc y² + z² ≤ r² of the chord 2√(R² − y²): the volume a cylinder of radius r - /// shares with a perpendicular one of radius R ≥ r through its axis, both long enough. - /// Simpson in y = r sin θ, where the integrand is smooth. - fn crossing_cylinders(r: f64, big: f64) -> f64 { - simpson(-FRAC_PI_2, FRAC_PI_2, |t| { - let (s, c) = t.sin_cos(); - 4.0 * r * r * c * c * (big * big - r * r * s * s).max(0.0).sqrt() - }) - } - - fn simpson(a: f64, b: f64, f: impl Fn(f64) -> f64) -> f64 { - let n = 4096; - let h = (b - a) / n as f64; - let mut sum = f(a) + f(b); - for i in 1..n { - sum += f(a + h * i as f64) * if i % 2 == 1 { 4.0 } else { 2.0 }; - } - sum * h / 3.0 - } - - /// Union, intersection and cut volumes of bodies of volume `a` and `b` sharing `common`. - fn by_op(a: f64, b: f64, common: f64) -> [f64; 3] { - [a + b - common, common, a - common] - } - - /// Pairs whose results are known by hand, with their union, intersection and cut volumes - /// (0 is empty). - fn named() -> Vec<(String, Shape, Shape, Option<[f64; 3]>)> { - let bx = |a: [f64; 3], b: [f64; 3]| Shape::Box { a, b }; - let z = [0.0, 0.0, 1.0]; - let ball = 4.0 / 3.0 * PI * 125.0; - // The torus's tube (centre radius 4, radius 2) inside the post's radius 3: the tube's - // section disc for ρ ≤ 3, swept round, with ρ − 4 = 2 sin θ. - let ring_in_post = simpson(-FRAC_PI_2, -FRAC_PI_6, |t| { - let (s, c) = t.sin_cos(); - 2.0 * PI * (4.0 + 2.0 * s) * 8.0 * c * c - }); - vec![ - ("overlapping boxes".into(), bx([0.0; 3], [10.0; 3]), bx([5.0; 3], [15.0; 3]), Some(by_op(1000.0, 1000.0, 125.0))), - ("boxes sharing faces".into(), bx([0.0; 3], [10.0; 3]), bx([5.0, 0.0, 0.0], [15.0, 10.0, 10.0]), Some(by_op(1000.0, 1000.0, 500.0))), - ("boxes apart".into(), bx([0.0; 3], [10.0; 3]), bx([20.0, 0.0, 0.0], [30.0, 10.0, 10.0]), Some(by_op(1000.0, 1000.0, 0.0))), - ( - "slot through a bar".into(), - bx([0.0; 3], [30.0, 10.0, 10.0]), - bx([10.0, -1.0, -1.0], [20.0, 11.0, 11.0]), - Some(by_op(3000.0, 1440.0, 1000.0)), - ), - ("pocket".into(), bx([0.0; 3], [10.0; 3]), bx([3.0; 3], [7.0, 7.0, 23.0]), Some(by_op(1000.0, 320.0, 112.0))), - ( - "hole through a box".into(), - bx([0.0; 3], [10.0; 3]), - Shape::Cylinder { base: [5.0, 5.0, -5.0], axis: z, radius: 2.0, height: 20.0 }, - Some(by_op(1000.0, 80.0 * PI, 40.0 * PI)), - ), - ( - "crossing cylinders".into(), - Shape::Cylinder { base: [0.0, 0.0, -10.0], axis: z, radius: 4.0, height: 20.0 }, - Shape::Cylinder { base: [-10.0, 0.0, 0.0], axis: [1.0, 0.0, 0.0], radius: 3.0, height: 20.0 }, - Some(by_op(320.0 * PI, 180.0 * PI, crossing_cylinders(3.0, 4.0))), - ), - ( - "sphere into a box corner".into(), - bx([0.0; 3], [10.0; 3]), - Shape::Sphere { center: [10.0; 3], radius: 5.0 }, - Some(by_op(1000.0, ball, ball / 8.0)), - ), - ( - "torus around a post".into(), - Shape::Cylinder { base: [0.0; 3], axis: z, radius: 3.0, height: 10.0 }, - Shape::Torus { center: [0.0, 0.0, 5.0], major: 4.0, minor: 2.0 }, - Some(by_op(90.0 * PI, 32.0 * PI * PI, ring_in_post)), - ), - ] - } - - /// One step of the cut sequence: a profile extruded into a tool, combined with the body so - /// far. The first step (no operation) is the starting body itself. - struct Step { - name: &'static str, - op: Option, - plane: Plane, - region: Region2, - start: f64, - end: f64, - /// The body's volume after this step. - volume: f64, - solids: usize, - } - - impl Step { - fn tool(&self) -> Result { - let mut bodies = extrude(&self.plane, std::slice::from_ref(&self.region), self.start, self.end).map_err(|e| e.to_string())?; - match (bodies.pop(), bodies.is_empty()) { - (Some(b), true) => Ok(b), - _ => Err("the profile extruded into other than one body".into()), - } - } - } - - fn region(outer: Loop2) -> Region2 { - Region2 { outer: outer.ccw(), holes: vec![] } - } - - /// A slot between the centres `a` and `b` of its two end arcs, `r` wide either side. - fn slot(a: Vec2, b: Vec2, r: f64) -> Loop2 { - let d = b - a; - let side = Vec2::new(-d.y, d.x) * (r / d.x.hypot(d.y)); - let along = d.y.atan2(d.x); - Loop2 { - segs: vec![ - Seg2::Line { a: a - side, b: b - side }, - Seg2::Arc { center: b, radius: r, start: along - FRAC_PI_2, sweep: PI }, - Seg2::Line { a: b + side, b: a + side }, - Seg2::Arc { center: a, radius: r, start: along + FRAC_PI_2, sweep: PI }, - ], - } - } - - /// Block 63.6 × 31.7 × 9.5; a Ø20 × 10 boss joined on its top; a slot through it front to - /// back; a Ø6 cross hole through the boss from the right side; a slot front to back through - /// the whole height that splits the block in two. Tools run 1 mm past the body each side. - fn sequence() -> Vec { - let (w, d, h) = (63.6, 31.7, 9.5); - let block = w * d * h; - let boss = block + 1000.0 * PI; - // Front plane: sketch x = +X, y = −Z, normal +Y. - let front = Plane { origin: Vec3::new(0.0, -1.0, 0.0), ..Plane::XZ }; - // Left plane: sketch x = +Y, y = +Z, normal +X. - let left = Plane { origin: Vec3::new(-1.0, 0.0, 0.0), ..Plane::YZ }; - let slot1 = boss - (10.0 * 5.0 + PI * 2.5 * 2.5) * d; - let cross = slot1 - crossing_cylinders(3.0, 10.0); - let split = cross - 4.0 * h * d; - vec![ - Step { - name: "block", - op: None, - plane: Plane::XY, - region: region(Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(w, 0.0), Vec2::new(w, d), Vec2::new(0.0, d)])), - start: 0.0, - end: h, - volume: block, - solids: 1, - }, - Step { - name: "join Ø20 boss", - op: Some(BoolOp::Union), - plane: Plane { origin: Vec3::new(0.0, 0.0, h), ..Plane::XY }, - region: region(Loop2::circle(Vec2::new(w / 2.0, d / 2.0), 10.0)), - start: 0.0, - end: 10.0, - volume: boss, - solids: 1, - }, - Step { - name: "cut slot front to back", - op: Some(BoolOp::Cut), - plane: front, - region: region(slot(Vec2::new(8.0, -h / 2.0), Vec2::new(18.0, -h / 2.0), 2.5)), - start: 0.0, - end: d + 2.0, - volume: slot1, - solids: 1, - }, - Step { - name: "cut Ø6 cross hole in the boss", - op: Some(BoolOp::Cut), - plane: left, - region: region(Loop2::circle(Vec2::new(d / 2.0, h + 5.0), 3.0)), - start: 0.0, - end: w + 2.0, - volume: cross, - solids: 1, - }, - Step { - name: "cut splitting slot", - op: Some(BoolOp::Cut), - plane: front, - region: region(slot(Vec2::new(52.0, 3.0), Vec2::new(52.0, -13.0), 2.0)), - start: 0.0, - end: d + 2.0, - volume: split, - solids: 2, - }, - ] - } - - /// The sequence on truck, each step on the previous step's result; stops at the first - /// failure. - fn truck_sequence(steps: &[Step]) -> Vec { - let mut out = Vec::new(); - let mut body: Option = None; - for s in steps { - let tool = match s.tool() { - Ok(t) => t, - Err(e) => { - out.push(Outcome { error: Some(format!("tool: {e}")), ..Outcome::default() }); - break; - } - }; - let (o, next) = match (&body, s.op) { - (Some(b), Some(op)) => truck_bodies(b, &tool, op), - _ => { - let o = Outcome { - faces: tool.face_count(), - edges: tool.edge_count(), - solids: tool.shell_count(), - volume: measure(&tool).map(|m| m.volume).unwrap_or(f64::NAN), - ..Outcome::default() - }; - (o, Some(tool)) - } - }; - let failed = o.error.is_some() || next.is_none(); - out.push(o); - body = next; - if failed { - break; - } - } - out - } - - /// The sequence on SMLib, each step on SMLib's own previous result (only the tools come from - /// truck); stops at the first failure. - fn smlib_sequence(steps: &[Step]) -> Vec { - let mut out = Vec::new(); - let mut body: Option = None; - for s in steps { - let t0 = Instant::now(); - let tool = s.tool().and_then(|t| solidcraft_kernel::smlib_body(&t).map_err(|e| format!("import: {e}"))); - let import_ms = ms_since(t0); - let tool = match tool { - Ok(t) => t, - Err(e) => { - out.push(Outcome { error: Some(format!("tool: {e}")), import_ms, ..Outcome::default() }); - break; - } - }; - let (mut o, next) = match (&body, s.op) { - (Some(b), Some(op)) => smlib_breps(b, &tool, op), - _ => { - let [faces, edges, _] = tool.counts().unwrap_or_default(); - let o = Outcome { - faces, - edges, - solids: tool.solid_count().unwrap_or(0), - volume: tool.mass_properties(MEASURE_ACCURACY).map(|m| m.volume).unwrap_or(f64::NAN), - ..Outcome::default() - }; - (o, Some(tool)) - } - }; - o.import_ms = import_ms; - let failed = o.error.is_some() || next.is_none(); - out.push(o); - body = next; - if failed { - break; - } - } - out - } - - fn outcome_from_json(v: &Value) -> Outcome { - let n = |k: &str| v.get(k).and_then(Value::as_u64).and_then(|x| usize::try_from(x).ok()).unwrap_or(0); - let f = |k: &str| v.get(k).and_then(Value::as_f64).unwrap_or(f64::NAN); - let s = |k: &str| v.get(k).and_then(Value::as_str).map(str::to_string); - let arr = |h: &Value, k: &str| -> Vec { - h.get(k) - .and_then(Value::as_array) - .map(|a| a.iter().filter_map(Value::as_u64).filter_map(|x| usize::try_from(x).ok()).collect()) - .unwrap_or_default() - }; - let history = v - .get("history") - .filter(|h| !h.is_null()) - .and_then(|h| Some(HistoryShape { faces: arr(h, "faces").try_into().ok()?, edges: arr(h, "edges").try_into().ok()? })); - Outcome { - error: s("error"), - empty: v.get("empty").and_then(Value::as_bool).unwrap_or(false), - volume: f("volume"), - faces: n("faces"), - edges: n("edges"), - solids: n("solids"), - history, - ms: f("ms"), - import_ms: f("import_ms"), - export_ms: f("export_ms"), - export_error: s("export_error"), - export_volume: f("export_volume"), - } - } - - /// This program again with `args`, its standard output as JSON; a crash is an error. - fn child(args: &[String]) -> Result { - let exe = std::env::current_exe().map_err(|e| format!("cannot start a child: {e}"))?; - let out = std::process::Command::new(exe) - .args(args) - .stderr(std::process::Stdio::null()) - .output() - .map_err(|e| format!("cannot start a child: {e}"))?; - if !out.status.success() { - return Err(format!("crashed ({})", out.status)); - } - serde_json::from_slice::(&out.stdout).map_err(|e| format!("unreadable child output: {e}")) - } - - /// [`smlib`] for case `index` and operation `op`, in a child process. - fn smlib_isolated(index: usize, op: usize, seeds: u64) -> Outcome { - let args = ["--seeds".to_string(), seeds.to_string(), "--smlib-case".into(), index.to_string(), op.to_string()]; - match child(&args) { - Ok(v) => outcome_from_json(&v), - Err(e) => Outcome { error: Some(e), ..Outcome::default() }, - } - } - - /// [`smlib_sequence`] in a child process. - fn smlib_sequence_isolated() -> Vec { - match child(&["--smlib-sequence".to_string()]) { - Ok(Value::Array(steps)) => steps.iter().map(outcome_from_json).collect(), - Ok(_) => vec![Outcome { error: Some("unreadable child output".into()), ..Outcome::default() }], - Err(e) => vec![Outcome { error: Some(e), ..Outcome::default() }], - } - } - - fn outcome_json(o: &Outcome) -> Value { - json!({ - "error": o.error, - "empty": o.empty, - "volume": o.volume, - "faces": o.faces, - "edges": o.edges, - "solids": o.solids, - "history": o.history.map(|h| json!({"faces": h.faces, "edges": h.edges})), - "ms": o.ms, - "import_ms": o.import_ms, - "export_ms": o.export_ms, - "export_error": o.export_error, - "export_volume": o.export_volume, - }) - } - - /// How the two compare: "same", what differs, or why it couldn't be compared. - fn verdict(t: &Outcome, s: &Outcome) -> (String, &'static str) { - if s.error.as_deref().is_some_and(|e| e.starts_with("not supported yet")) { - return ("not run on SMLib".into(), "skipped"); - } - match (&t.error, &s.error) { - (Some(_), Some(_)) => return ("both failed".into(), "failed"), - (Some(_), None) => return ("truck failed, SMLib didn't".into(), "smlib-only"), - (None, Some(_)) => return ("SMLib failed, truck didn't".into(), "truck-only"), - (None, None) => {} - } - if t.empty || s.empty { - return if t.empty == s.empty { ("both empty".into(), "same") } else { ("one empty".into(), "differs") }; - } - let mut diffs = Vec::new(); - let scale = t.volume.abs().max(s.volume.abs()).max(1e-9); - if (t.volume - s.volume).abs() > VOLUME_REL * scale { - diffs.push(format!("volume {:.4} vs {:.4}", t.volume, s.volume)); - } - if t.faces != s.faces { - diffs.push(format!("faces {} vs {}", t.faces, s.faces)); - } - if t.edges != s.edges { - diffs.push(format!("edges {} vs {}", t.edges, s.edges)); - } - if t.solids != s.solids { - diffs.push(format!("solids {} vs {}", t.solids, s.solids)); - } - match (t.history, s.history) { - (Some(a), Some(b)) if a != b => diffs.push("history".into()), - (None, Some(_)) => diffs.push("no truck history".into()), - (Some(_), None) => diffs.push("no SMLib history".into()), - _ => {} - } - if diffs.is_empty() { ("same".into(), "same") } else { (diffs.join(", "), "differs") } - } - - fn cell(o: &Outcome) -> String { - match (&o.error, o.empty) { - (Some(e), _) => format!("error: {}", e.replace('|', "/")), - (None, true) => "empty".into(), - (None, false) => { - format!("{:.3} · {}f {}e {}s · {}", o.volume, o.faces, o.edges, o.solids, o.history.map_or("no history".into(), |h| h.text())) - } - } - } - - /// The export's time, and what went wrong with it or where truck's measure of the exported - /// body leaves SMLib's own; a dash where SMLib made no body. - fn exported(o: &Outcome) -> String { - match &o.export_error { - Some(e) => format!("{:.1} (failed: {})", o.export_ms, e.replace('|', "/")), - None if !o.export_volume.is_finite() => "–".into(), - None if o.error.is_some() || (o.export_volume - o.volume).abs() > VOLUME_REL * o.volume.abs().max(1e-9) => { - format!("{:.1} (truck measures {:.3})", o.export_ms, o.export_volume) - } - None => format!("{:.1}", o.export_ms), - } - } - - /// Where `s` holds material along the vertical line through (x, y): one z interval, since - /// every fuzz primitive (box, axis-aligned cylinder, sphere, torus about +Z) is convex along - /// z. `None` off it; `Err` for a primitive this can't do (a slanted axis). - fn z_span(s: &Shape, x: f64, y: f64) -> Result, ()> { - let half = |r2: f64| (r2 >= 0.0).then(|| r2.sqrt()); - Ok(match *s { - Shape::Box { a, b } => (x >= a[0] && x <= b[0] && y >= a[1] && y <= b[1]).then_some((a[2], b[2])), - Shape::Sphere { center: c, radius: r } => half(r * r - (x - c[0]).powi(2) - (y - c[1]).powi(2)).map(|h| (c[2] - h, c[2] + h)), - Shape::Torus { center: c, major, minor } => { - let rho = ((x - c[0]).powi(2) + (y - c[1]).powi(2)).sqrt(); - half(minor * minor - (rho - major).powi(2)).map(|h| (c[2] - h, c[2] + h)) - } - Shape::Cylinder { base: c, axis, radius: r, height: h } => match axis { - [0.0, 0.0, 1.0] => half(r * r - (x - c[0]).powi(2) - (y - c[1]).powi(2)).map(|_| (c[2], c[2] + h)), - [1.0, 0.0, 0.0] => { - if x < c[0] || x > c[0] + h { - None - } else { - half(r * r - (y - c[1]).powi(2)).map(|d| (c[2] - d, c[2] + d)) - } - } - [0.0, 1.0, 0.0] => { - if y < c[1] || y > c[1] + h { - None - } else { - half(r * r - (x - c[0]).powi(2)).map(|d| (c[2] - d, c[2] + d)) - } - } - _ => return Err(()), - }, - }) - } - - /// The xy box a primitive covers. - fn xy_bounds(s: &Shape) -> [f64; 4] { - match *s { - Shape::Box { a, b } => [a[0], b[0], a[1], b[1]], - Shape::Sphere { center: c, radius: r } => [c[0] - r, c[0] + r, c[1] - r, c[1] + r], - Shape::Torus { center: c, major, minor } => { - let r = major + minor; - [c[0] - r, c[0] + r, c[1] - r, c[1] + r] - } - Shape::Cylinder { base: c, axis, radius: r, height: h } => { - let lo = |i: usize| c[i] - if axis[i] > 0.5 { 0.0 } else { r }; - let hi = |i: usize| c[i] + if axis[i] > 0.5 { h } else { r }; - [lo(0), hi(0), lo(1), hi(1)] - } - } - } - - /// Cells per side of the reference integration's grid (about; see [`axis_cells`]). - const GRID: usize = 1600; - - /// Where a primitive's material starts or stops changing smoothly along x (`i` = 0) or y - /// (`i` = 1): box faces, a cylinder's ends, and where a round outline turns. - fn breaks(s: &Shape, i: usize) -> Vec { - match *s { - Shape::Box { a, b } => vec![a[i], b[i]], - Shape::Sphere { center: c, radius: r } => vec![c[i] - r, c[i], c[i] + r], - Shape::Torus { center: c, major, minor } => { - let (o, n) = (major + minor, major - minor); - vec![c[i] - o, c[i] - n, c[i], c[i] + n, c[i] + o] - } - Shape::Cylinder { base: c, axis, radius: r, height: h } => { - if axis[i] > 0.5 { - vec![c[i], c[i] + h] - } else { - vec![c[i] - r, c[i], c[i] + r] - } - } - } - } - - /// Midpoints and widths of cells along one axis over `lo..hi`, about `n` of them, with every - /// break a cell boundary, so straight faces fall on cell edges and leave no error. - fn axis_cells(lo: f64, hi: f64, mut cuts: Vec, n: usize) -> Vec<(f64, f64)> { - cuts.retain(|x| *x > lo && *x < hi); - cuts.extend([lo, hi]); - cuts.sort_by(f64::total_cmp); - cuts.dedup(); - let step = (hi - lo) / n as f64; - let mut cells = Vec::new(); - for w in cuts.windows(2) { - let (a, b) = (w[0], w[1]); - let k = ((b - a) / step).ceil().max(1.0) as usize; - let d = (b - a) / k as f64; - cells.extend((0..k).map(|j| (a + (j as f64 + 0.5) * d, d))); - } - cells - } - - /// The volume of `a op b` on a grid of about `n` × `n` cells (see [`reference_volume`]). - fn grid_volume(a: &Shape, b: &Shape, op: BoolOp, n: usize) -> Option { - let (ba, bb) = (xy_bounds(a), xy_bounds(b)); - let r = [ba[0].min(bb[0]), ba[1].max(bb[1]), ba[2].min(bb[2]), ba[3].max(bb[3])]; - let cut = |i: usize| [breaks(a, i), breaks(b, i)].concat(); - let (xs, ys) = (axis_cells(r[0], r[1], cut(0), n), axis_cells(r[2], r[3], cut(1), n)); - let len = |s: Option<(f64, f64)>| s.map_or(0.0, |(lo, hi)| (hi - lo).max(0.0)); - let mut v = 0.0; - for &(x, dx) in &xs { - for &(y, dy) in &ys { - let (p, q) = (z_span(a, x, y).ok()?, z_span(b, x, y).ok()?); - let both = match (p, q) { - (Some(p), Some(q)) => len(Some((p.0.max(q.0), p.1.min(q.1)))), - _ => 0.0, - }; - v += dx - * dy - * match op { - BoolOp::Union => len(p) + len(q) - both, - BoolOp::Intersect => both, - BoolOp::Cut => len(p) - both, - }; - } - } - Some(v) - } - - /// The volume of `a op b` from the primitives' own definitions, by no kernel, and how far it - /// may be off: along each vertical line both operands are exact z intervals, combined - /// exactly, integrated over a grid whose cell edges include every straight face. What error - /// is left comes from curved outlines; the grid at half the resolution bounds it. `None` - /// for a primitive [`z_span`] can't do. - fn reference_volume(a: &Shape, b: &Shape, op: BoolOp) -> Option<(f64, f64)> { - let fine = grid_volume(a, b, op, GRID)?; - let coarse = grid_volume(a, b, op, GRID / 2)?; - Some((fine, (fine - coarse).abs())) - } - - /// Which kernel's volume is off the reference by more than [`VOLUME_REL`]. - fn graded(o: &Outcome, reference: Option<(f64, f64)>) -> Option { - let (r, uncertain) = reference?; - if o.error.is_some() { - return None; - } - let v = if o.empty { 0.0 } else { o.volume }; - Some((v - r).abs() <= (VOLUME_REL * r.abs()).max(3.0 * uncertain).max(1e-6)) - } - - /// How far a measured volume lies from the analytic one, relative; `None` when it made none. - fn off(o: &Outcome, analytic: f64) -> Option { - if o.error.is_some() { - return None; - } - let v = if o.empty { 0.0 } else { o.volume }; - Some(if analytic == 0.0 { v.abs() } else { (v / analytic - 1.0).abs() }) - } - - /// Median, worst and total of `xs` (ms), or dashes when empty. - fn stats(xs: &mut Vec) -> String { - xs.retain(|x| x.is_finite()); - if xs.is_empty() { - return "– | – | –".into(); - } - xs.sort_by(f64::total_cmp); - let median = xs.get(xs.len() / 2).copied().unwrap_or(f64::NAN); - let worst = xs.last().copied().unwrap_or(f64::NAN); - format!("{median:.2} | {worst:.1} | {:.0}", xs.iter().sum::()) - } - - pub fn main() -> Result<(), String> { - let mut seeds = 32u64; - let mut out = PathBuf::from("target/smlib-booleans"); - let mut child_case = None; - let mut child_sequence = false; - let mut args = std::env::args().skip(1); - while let Some(a) = args.next() { - match a.as_str() { - "--seeds" => seeds = args.next().and_then(|n| n.parse().ok()).ok_or("--seeds takes a number")?, - "--out" => out = args.next().map(PathBuf::from).ok_or("--out takes a directory")?, - "--smlib-case" => { - let mut n = || args.next().and_then(|n| n.parse::().ok()).ok_or("--smlib-case takes a case and an operation"); - child_case = Some((n()?, n()?)); - } - "--smlib-sequence" => child_sequence = true, - other => return Err(format!("unknown argument {other}")), - } - } - let mut cases = named(); - cases.extend((0..seeds).map(|s| { - let (a, b) = boolean_shapes(s); - (format!("seed {s}"), a, b, None) - })); - if let Some((index, op)) = child_case { - let ((_, a, b, _), op) = cases.get(index).zip(OPS.get(op)).ok_or("no such case")?; - solidcraft_smlib::init().map_err(|e| e.to_string())?; - println!("{}", outcome_json(&smlib(a, b, *op))); - return Ok(()); - } - let steps = sequence(); - if child_sequence { - solidcraft_smlib::init().map_err(|e| e.to_string())?; - println!("{}", Value::Array(smlib_sequence(&steps).iter().map(outcome_json).collect())); - return Ok(()); - } - solidcraft_smlib::init().map_err(|e| e.to_string())?; - - let mut rows = Vec::new(); - let mut md = String::new(); - let mut tally = std::collections::BTreeMap::<&str, usize>::new(); - let mut grades = std::collections::BTreeMap::<(&str, &str), usize>::new(); - let (mut truck_ms, mut smlib_ms, mut import_ms, mut export_ms) = (Vec::new(), Vec::new(), Vec::new(), Vec::new()); - let (mut truck_fail, mut smlib_fail, mut export_fail) = (0usize, 0usize, 0usize); - let (mut analytic_ops, mut truck_exact, mut smlib_exact) = (0usize, 0usize, 0usize); - for (index, (name, a, b, analytic)) in cases.iter().enumerate() { - for (k, op) in OPS.into_iter().enumerate() { - let t = truck(a, b, op); - let s = smlib_isolated(index, k, seeds); - let (what, class) = verdict(&t, &s); - *tally.entry(class).or_default() += 1; - let want = analytic.and_then(|v| v.get(k).copied()); - // Rows without an analytic volume (the seeds): a reference from the primitives' - // definitions, each kernel graded against it. - let reference = if want.is_none() { reference_volume(a, b, op) } else { None }; - let grade = |o: &Outcome| match graded(o, reference) { - Some(true) => "ok", - Some(false) => "WRONG", - None => "-", - }; - let (gt, gs) = (grade(&t), grade(&s)); - if reference.is_some() { - *grades.entry(("truck", gt)).or_default() += 1; - *grades.entry(("SMLib", gs)).or_default() += 1; - } - let what = if gt == "WRONG" || gs == "WRONG" { - format!( - "{what}; volume vs reference {:.3} ± {:.3}: truck {gt}, SMLib {gs}", - reference.map_or(f64::NAN, |r| r.0), - reference.map_or(f64::NAN, |r| r.1) - ) - } else { - what - }; - if let Some(w) = want { - analytic_ops += 1; - truck_exact += usize::from(off(&t, w).is_some_and(|d| d <= VOLUME_REL)); - smlib_exact += usize::from(off(&s, w).is_some_and(|d| d <= 1e-6)); - } - truck_fail += usize::from(t.error.is_some()); - smlib_fail += usize::from(s.error.is_some()); - export_fail += usize::from(s.export_error.is_some()); - if t.error.is_none() { - truck_ms.push(t.ms); - } - if s.error.is_none() { - smlib_ms.push(s.ms); - import_ms.push(s.import_ms); - if !s.empty { - export_ms.push(s.export_ms); - } - } - let want_text = want.map_or("–".into(), |w| if w == 0.0 { "empty".into() } else { format!("{w:.3}") }); - let back = exported(&s); - let _ = writeln!( - md, - "| {name} | {} | {want_text} | {} | {} | {what} | {:.1} | {:.1} | {:.1} | {back} |", - op_name(op), - cell(&t), - cell(&s), - t.ms, - s.ms, - s.import_ms, - ); - rows.push(json!({ - "case": name, "a": a, "b": b, "op": op_name(op), "analytic": want, - "truck": outcome_json(&t), "smlib": outcome_json(&s), - "verdict": class, "detail": what, - "reference_volume": reference.map(|r| r.0), "reference_uncertainty": reference.map(|r| r.1), - "truck_volume_grade": gt, "smlib_volume_grade": gs, - })); - } - } - - let truck_steps = truck_sequence(&steps); - let smlib_steps = smlib_sequence_isolated(); - let mut seq_md = String::new(); - let mut seq_json = Vec::new(); - for (i, s) in steps.iter().enumerate() { - let (t, m) = (truck_steps.get(i), smlib_steps.get(i)); - let ms = |o: Option<&Outcome>| o.filter(|_| s.op.is_some()).map_or("–".into(), |o| format!("{:.1}", o.ms)); - let vol = |o: Option<&Outcome>| match o { - None => "not run".into(), - Some(o) => match &o.error { - Some(e) => format!("error: {}", e.replace('|', "/")), - None => format!("{:.3} ({}f {}e {}s)", o.volume, o.faces, o.edges, o.solids), - }, - }; - let export = m.filter(|_| s.op.is_some()).map_or("–".into(), exported); - let _ = writeln!( - seq_md, - "| {} | {:.3} ({}s) | {} | {} | {} | {} | {} | {export} |", - s.name, - s.volume, - s.solids, - vol(t), - vol(m), - ms(t), - ms(m), - m.map_or("–".into(), |o| format!("{:.1}", o.import_ms)), - ); - seq_json.push(json!({ - "step": s.name, "op": s.op.map(op_name), "analytic": s.volume, "analytic_solids": s.solids, - "truck": t.map(outcome_json), "smlib": m.map(outcome_json), - })); - } - - let summary: Vec = tally.iter().map(|(k, v)| format!("{k} {v}")).collect(); - let ops = cases.len() * OPS.len(); - let mut report = String::new(); - let _ = writeln!(report, "# Booleans: truck and SMLib side by side\n"); - let _ = writeln!(report, "{} cases × 3 operations: {}.\n", cases.len(), summary.join(", ")); - let grade_text: Vec = grades.iter().map(|((k, g), n)| format!("{k} {g} {n}")).collect(); - let _ = writeln!( - report, - "Rows without an analytic volume, against a reference from the primitives' definitions (exact z \ - intervals on a grid of about {GRID}² cells with every straight face on a cell edge; within \ - {VOLUME_REL:e} relative or 3× the grid's own error bound): {}.\n", - grade_text.join(", ") - ); - let _ = writeln!( - report, - "Failed: truck {truck_fail}/{ops}, SMLib {smlib_fail}/{ops}; SMLib results that don't export back to truck \ - (or that truck can't measure): {export_fail}. Against the {analytic_ops} analytic volumes: truck within {VOLUME_REL:e} on {truck_exact}, \ - SMLib within 1e-6 on {smlib_exact}.\n" - ); - let _ = writeln!(report, "## Times (ms, operations that didn't fail)\n"); - let _ = writeln!(report, "| | median | worst | total |\n|---|---|---|---|"); - let _ = writeln!(report, "| truck boolean | {} |", stats(&mut truck_ms)); - let _ = writeln!(report, "| SMLib boolean | {} |", stats(&mut smlib_ms)); - let _ = writeln!(report, "| SMLib import (truck→SMLib, both operands) | {} |", stats(&mut import_ms)); - let _ = writeln!(report, "| SMLib export (SMLib→truck, result) | {} |\n", stats(&mut export_ms)); - let _ = writeln!( - report, - "## Cut sequence\n\nEach step on the previous step's result, each kernel on its own (SMLib's results stay in SMLib; \ - only the tools come from truck). Volumes in mm³ (faces, edges, solids); times in ms.\n" - ); - let _ = writeln!( - report, - "| step | analytic | truck | SMLib | truck ms | SMLib ms | SMLib import ms | SMLib export ms |\n|---|---|---|---|---|---|---|---|" - ); - report.push_str(&seq_md); - let _ = writeln!(report, "\n## Cases\n"); - let _ = writeln!( - report, - "Volume · faces, edges, solids · history shape: faces **K**ept, **M**odified, **S**plit, **J**oined (merged), \ - **G**enerated; edges from an operand **e**dge, new where faces meet (**x**), without origin (**-**). \ - Times in ms: the boolean on each kernel, then SMLib's import of the operands and export of the result.\n" - ); - let _ = writeln!( - report, - "| case | op | analytic | truck | SMLib | verdict | truck ms | SMLib ms | import ms | export ms |\n\ - |---|---|---|---|---|---|---|---|---|---|" - ); - report.push_str(&md); - - std::fs::create_dir_all(&out).map_err(|e| format!("{}: {e}", out.display()))?; - let json = json!({ "cases": cases.len(), "tally": tally, "reference_grades": grades.iter().map(|((k, g), n)| json!({"kernel": k, "grade": g, "count": n})).collect::>(), "rows": rows, "sequence": seq_json }); - let text = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?; - std::fs::write(out.join("smlib-booleans.json"), text).map_err(|e| e.to_string())?; - std::fs::write(out.join("smlib-booleans.md"), &report).map_err(|e| e.to_string())?; - println!("{} cases × 3: {}; against the reference: {} ({})", cases.len(), summary.join(", "), grade_text.join(", "), out.display()); - Ok(()) - } -} diff --git a/crates/kernel/examples/step_board_fixture.rs b/crates/kernel/examples/step_board_fixture.rs index 393ea5aeb3..51efadac83 100644 --- a/crates/kernel/examples/step_board_fixture.rs +++ b/crates/kernel/examples/step_board_fixture.rs @@ -2,21 +2,13 @@ //! component is its own product with a package body and its own pins (nothing shared). //! //! ```sh -//! scripts/fetch-smlib.sh //! SOLIDCRAFT_STEP_PCB_COMPONENTS=600 SOLIDCRAFT_STEP_PCB_PINS=15 \ -//! cargo run --release -p solidcraft-kernel --features smlib --example step_board_fixture -- board.step +//! cargo run --release -p solidcraft-kernel --example step_board_fixture -- board.step //! ``` //! //! 600 components of 15 pins give 52 MB. The defaults are 500 and 16; the path defaults to //! `board.step`. -#[cfg(not(feature = "smlib"))] -fn main() { - eprintln!("step_board_fixture needs SMLib: run scripts/fetch-smlib.sh, then add --features smlib"); - std::process::exit(2); -} - -#[cfg(feature = "smlib")] fn main() { if let Err(e) = run::main() { eprintln!("step_board_fixture: {e}"); @@ -24,7 +16,6 @@ fn main() { } } -#[cfg(feature = "smlib")] mod run { use solidcraft_geom::Vec3; use solidcraft_kernel::{Body, ExportBody, ExportProduct, StepHeader, box_solid, cylinder, fillet, step_export_products}; diff --git a/crates/kernel/src/blend_smlib.rs b/crates/kernel/src/blend_smlib.rs index 46615328cc..77b62d9ccf 100644 --- a/crates/kernel/src/blend_smlib.rs +++ b/crates/kernel/src/blend_smlib.rs @@ -444,15 +444,7 @@ mod imp { "not supported yet: the {what} meets a corner of this kind, which SMLib's blends do not handle yet (where its edges \ run on tangentially into each other, or meet in a way no blend corner builds)" )), - // A setback too short for its corner's fill (SMLib certifies it convex only from - // its least on) names that least; any other is a blend too large for its room. - SmError::NoRoom => match solidcraft_smlib::blend_fit() { - Some(solidcraft_smlib::Fit::SetbackAtLeast(least)) => KernelError::Invalid(format!( - "the setback is too short for its corner: {}{least:.4} mm (the corner's patch is convex only from there)", - crate::SETBACK_AT_LEAST - )), - _ => KernelError::TooLarge(format!("the {what} is larger than the faces beside its edges allow")), - }, + SmError::NoRoom => KernelError::TooLarge(format!("the {what} is larger than the faces beside its edges allow")), SmError::Unported(why) => KernelError::Invalid(format!("not supported yet: {why}")), e => failed(what, "operation", e), })?; diff --git a/crates/kernel/src/boolean_history_tests.rs b/crates/kernel/src/boolean_history_tests.rs index 279762ac03..c739f4fa14 100644 --- a/crates/kernel/src/boolean_history_tests.rs +++ b/crates/kernel/src/boolean_history_tests.rs @@ -429,7 +429,6 @@ 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::smlib_boolean::smlib_boolean; diff --git a/crates/kernel/src/fillet_setback_tests.rs b/crates/kernel/src/fillet_setback_tests.rs index 04a92776e3..37d3132705 100644 --- a/crates/kernel/src/fillet_setback_tests.rs +++ b/crates/kernel/src/fillet_setback_tests.rs @@ -45,8 +45,8 @@ fn a_setback_off_the_rounded_corner_is_refused() { /// A setback that does not reach past where the corner's rounds meet without one (on faces square /// to each other, the radius) is refused before SMLib runs, naming that least setback. Past it, a -/// setback still too short for SMLib's corner fill (certified convex only from a setback on, -/// smlib_blend_fit_size's SMLIB_FIT_SETBACK_AT_LEAST) is refused naming the smallest that builds, +/// setback still too short for the corner fill (certified convex only from a setback on, as SMLib +/// f916a8d's setback corner does) is refused naming the smallest that builds, /// at most twice the one asked for (the certificate scales the setbacks up to 2); that one builds. #[test] fn a_setback_not_past_the_radius_is_refused_naming_the_least() { diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 993e0af631..ae969590c3 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -214,8 +214,6 @@ pub use prism_curve::{PlanarPiece, PlanarSpline}; pub use prism_measure::{PrismSection, certified_prism_equivalent, certified_prism_section}; pub use repair::{Hole, RebuiltFace, RefusedFace, RepairOptions, RepairReport, SewnGap, repair}; pub use sew::{EdgeSpec, FaceSpec, SurfSpec, sew, sew_touching}; -/// Which kernel the process runs on, solidcore or SMLib (crates/smlib/src/route.rs). -pub use solidcraft_smlib::route as kernel_route; pub use splitface::{SplitTool, split_body, split_faces, split_faces_with_map}; pub use step::step_export; pub use step_in::{ @@ -238,7 +236,7 @@ pub use topo::{CylinderFace, cylinder_end_at, cylinder_face, cylinder_face_at, m pub use topo::{TopoCounts, merged_topology, seam_flags}; pub use tspline::tspline_sheet; pub use validation::{Validation, check_kept, validating}; -pub use validity::{SmlibCheck, Validity, smlib_built, smlib_dump_dir, validity}; +pub use validity::{SmlibCheck, Validity, smlib_dump_dir, validity}; #[derive(Debug, Clone, PartialEq, thiserror::Error)] pub enum KernelError { diff --git a/crates/kernel/src/modify_history_tests.rs b/crates/kernel/src/modify_history_tests.rs index 3cb4910b8b..4f5e6144ff 100644 --- a/crates/kernel/src/modify_history_tests.rs +++ b/crates/kernel/src/modify_history_tests.rs @@ -523,7 +523,6 @@ fn chamfer_after_box_cuts() { /// Shell, offset faces and draft straight through `crate::offset_backend`: refusals, and their /// histories checked against the geometry and their numbers against the analytic ones. -#[cfg(feature = "smlib")] mod smlib { use super::*; use crate::offset_backend; diff --git a/crates/kernel/src/smlib_sweeps_tests.rs b/crates/kernel/src/smlib_sweeps_tests.rs index 11ef9426d5..4ce441d73a 100644 --- a/crates/kernel/src/smlib_sweeps_tests.rs +++ b/crates/kernel/src/smlib_sweeps_tests.rs @@ -1,6 +1,5 @@ //! The sweeps through SMLib ([`crate::smlib_sweeps`]): analytic volumes, and the history and //! construction sources the builders' vocabulary gives each face. -#![cfg(feature = "smlib")] use std::f64::consts::{PI, TAU}; diff --git a/crates/kernel/src/step_in/tests.rs b/crates/kernel/src/step_in/tests.rs index a86bac8d31..cb0d13751a 100644 --- a/crates/kernel/src/step_in/tests.rs +++ b/crates/kernel/src/step_in/tests.rs @@ -540,23 +540,6 @@ fn offset_surface_cube_import_is_closed_and_editable() { assert!(rel(measure(&result).unwrap().volume, 1000.0 - 40.0 * std::f64::consts::PI) < 1e-3); } -/// A solid whose B-rep SMLib refuses, its faces too (here every SMLib call: the C++ kernel is asked -/// for, SOLIDCRAFT_KERNEL=smlib, on a build without it), is refused with the kernel's reason, naming -/// the solid. It once came in as a mesh of its faces, which the planner could still triangulate. -/// On solidcore, the default, the cube imports, so there is nothing to refuse. -#[cfg(not(feature = "smlib"))] -#[test] -fn a_solid_smlib_refuses_with_its_faces_is_refused_not_meshed() { - use crate::kernel_route::{Backend, choice}; - if choice() != Backend::Smlib { - eprintln!("skipped: needs SOLIDCRAFT_KERNEL=smlib on a build without the C++ kernel"); - return; - } - let text = format!("{HEAD}{}#60=SHAPE_REPRESENTATION('',(#101),$);\n{TAIL}", cube_entities()); - let why = step_import(&text).map(|imp| imp.warnings).unwrap_err().to_string(); - assert!(why.contains("solid 'Cube': not imported: ") && why.contains("SMLib"), "{why}"); -} - /// A hand-written unit cube brep (`#100`…) in a representation with the given unit and items. fn cube_entities() -> String { let mut s = String::new(); @@ -2019,11 +2002,10 @@ pub(super) fn schema_4_volume(text: &str) -> f64 { measure(&Body::from_record(r.solids[0].brep.clone()).unwrap()).unwrap().volume } -/// The step-conformance parts read into SMLib (`SOLIDCRAFT_KERNEL=smlib`): the STEP reader's +/// The step-conformance parts read into the kernel: the STEP reader's /// schema 4 record, SMLib's body of it, back as a kernel body. Each has its exact volume and /// face count, and every face is traced to a face of the file (the NURBS cylinder, dome and /// torus ring among them: their faces wind around a spline closed on itself, and get a seam). -#[cfg(feature = "smlib")] #[test] fn step_conformance_parts_read_into_smlib() { let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/step-conformance"); diff --git a/crates/kernel/src/tess/tests.rs b/crates/kernel/src/tess/tests.rs index befd7f9b38..6b8207ea06 100644 --- a/crates/kernel/src/tess/tests.rs +++ b/crates/kernel/src/tess/tests.rs @@ -373,7 +373,6 @@ fn closed_display_mesh(name: &str, b: &Body, tol: f64) -> Mesh { /// A sphere from SMLib cut through its centre: the half sphere is bounded by its rim and its /// seam from the pole up to the rim, used both ways. The seam's two uses share their nodes, so /// the mesh has no crack along it and nothing degenerate at the pole. -#[cfg(feature = "smlib")] #[test] fn a_half_sphere_from_smlib_is_closed_along_its_seam() { use solidcraft_smlib::{BooleanOp, Brep as S}; @@ -406,7 +405,6 @@ fn display_plans_stay_within_the_chord_tolerance() { } /// SMLib's torus is one face bounded by its two seams: a grid over the whole parameter box. -#[cfg(feature = "smlib")] #[test] fn an_smlib_torus_stays_within_the_chord_tolerance() { let Ok(ring) = solidcraft_smlib::Brep::make_torus([0.0; 3], 22.0, 4.0) else { panic!("torus") }; @@ -424,7 +422,6 @@ fn an_smlib_torus_stays_within_the_chord_tolerance() { /// A groove of trapezoid `section` (radius, axial offset) in the plane through the z axis along /// y, carried `turns` turns of `pitch` about z from `base`, as an SMLib solid. -#[cfg(feature = "smlib")] fn smlib_groove(section: [(f64, f64); 4], base: f64, pitch: f64, turns: f64) -> solidcraft_smlib::Brep { use solidcraft_geom::{Loop2, Vec2}; smlib_screw(Loop2::polygon(§ion.map(|(r, z)| Vec2::new(r, z))), base, pitch, turns) @@ -432,7 +429,6 @@ fn smlib_groove(section: [(f64, f64); 4], base: f64, pitch: f64, turns: f64) -> /// The `section` (radius, axial offset) in the plane through the z axis along y, carried /// `turns` turns of `pitch` about z from `base`, as an SMLib solid. -#[cfg(feature = "smlib")] fn smlib_screw(section: solidcraft_geom::Loop2, base: f64, pitch: f64, turns: f64) -> solidcraft_smlib::Brep { use solidcraft_geom::{Plane, Region2}; let origin = Vec3::new(0.0, 0.0, base); @@ -444,7 +440,6 @@ fn smlib_screw(section: solidcraft_geom::Loop2, base: f64, pitch: f64, turns: f6 ok(crate::native::smlib_body(&groove)) } -#[cfg(feature = "smlib")] fn smlib_cut(a: &solidcraft_smlib::Brep, b: &solidcraft_smlib::Brep) -> solidcraft_smlib::Brep { match a.boolean(b, solidcraft_smlib::BooleanOp::Subtract) { Ok(Some(r)) => r, @@ -455,7 +450,6 @@ fn smlib_cut(a: &solidcraft_smlib::Brep, b: &solidcraft_smlib::Brep) -> solidcra /// The ISO basic profile's groove for pitch `p` and major diameter `d`: internal, from 0.1 p /// inside the hole wall of radius `wall` out to the major radius; external, from the root out /// past the shaft's surface (radius `wall`). -#[cfg(feature = "smlib")] fn iso_groove(p: f64, d: f64, wall: f64, internal: bool) -> [(f64, f64); 4] { let h = p * 3f64.sqrt() / 2.0; let slope = (PI / 6.0).tan(); @@ -478,7 +472,6 @@ fn iso_groove(p: f64, d: f64, wall: f64, internal: bool) -> [(f64, f64); 4] { /// parabolic segment) that flat triangles bridge rather than follow: a face no wider than its /// chords sag (the thread's crest band where it runs out through the block's end, 0.06 mm sag /// at 0.1) takes up to that much more or less area however it is triangulated inside. -#[cfg(feature = "smlib")] fn assert_faces_lie_flat(name: &str, plan: &solidcraft_geom::tess::TessPlan, tol: f64) { for fp in &plan.faces { let Some(s) = fp.surface.and_then(|i| plan.surfaces.get(i as usize)) else { continue }; @@ -530,7 +523,6 @@ fn assert_faces_lie_flat(name: &str, plan: &solidcraft_geom::tess::TessPlan, tol /// #1995: an M8x1 thread through a 7 mm hole in a 20 mm block, both ends open. Truck meshed the /// groove's winding walls with triangles reaching past both ends of the block; the body's mesh /// now comes from the display plan, inside the block and with the analytic volume. -#[cfg(feature = "smlib")] #[test] fn a_modelled_thread_in_a_bore_meshes_inside_its_block() { use solidcraft_smlib::Brep as S; @@ -562,7 +554,6 @@ fn a_modelled_thread_in_a_bore_meshes_inside_its_block() { /// a triangle's side, with no four corners to blend. The butterfly of a round face made it a /// grid of ~200 000 points whose triangles zigzagged along the strip (six times the band's area, /// and off the surface by more than the tolerance); it is its boundary nodes alone now. -#[cfg(feature = "smlib")] #[test] fn a_thread_root_band_running_out_through_an_end_face_lies_on_its_surface() { let Ok(shaft) = solidcraft_smlib::Brep::make_cylinder([0.0; 3], 5.0, 30.0) else { panic!("shaft") }; @@ -587,7 +578,6 @@ fn a_thread_root_band_running_out_through_an_end_face_lies_on_its_surface() { /// halves go to the display plan for their mesh; at the measuring tolerance a half's wall wants /// more cells than a grid may have, and its cut grid gave up for an under-refined Delaunay (the /// halves measured 1.5% short of the whole). The cut grid now thins its lines to fit. -#[cfg(feature = "smlib")] #[test] fn the_halves_of_a_split_smlib_coil_measure_as_the_whole() { use solidcraft_geom::{Loop2, Vec2}; diff --git a/crates/kernel/src/validation.rs b/crates/kernel/src/validation.rs index 4ebfbf108b..3f03bbad9d 100644 --- a/crates/kernel/src/validation.rs +++ b/crates/kernel/src/validation.rs @@ -226,7 +226,6 @@ mod tests { /// A box with one corner moved off its three edges: SMLib's check refuses it. Under each_step /// the boolean that takes it refuses its result there; under each_feature it is not checked, /// says the check was left for later, and the check of the body kept refuses it. - #[cfg(feature = "smlib")] #[test] fn a_body_the_check_refuses_is_refused_at_the_step_or_at_the_end() { use solidcraft_geom::Vec3; diff --git a/crates/kernel/src/validity.rs b/crates/kernel/src/validity.rs index 73fdfe1437..28a46cfccf 100644 --- a/crates/kernel/src/validity.rs +++ b/crates/kernel/src/validity.rs @@ -54,14 +54,9 @@ fn smlib_validity(body: &Body) -> Option { }) } -/// Whether this build links SMLib (the `smlib` feature). -pub fn smlib_built() -> bool { - solidcraft_smlib::built() -} - /// Where every kernel call that fails from now on leaves its inputs for replay -/// (`solidcraft_smlib::set_dump_dir`, on either kernel: the call and its B-reps, which `Brep::read` -/// reads back); `None` stops it. -pub fn smlib_dump_dir(dir: Option<&std::path::Path>) -> crate::Result<()> { - solidcraft_smlib::set_dump_dir(dir).map_err(|e| crate::KernelError::Failed(format!("kernel dump folder: {e}"))) +/// (`solidcraft_smlib::set_dump_dir`: the call and its B-reps, which `Brep::read` reads back); +/// `None` stops it. +pub fn smlib_dump_dir(dir: Option<&std::path::Path>) { + solidcraft_smlib::set_dump_dir(dir); } diff --git a/crates/smlib/Cargo.toml b/crates/smlib/Cargo.toml index bc849548ae..65b1ec063c 100644 --- a/crates/smlib/Cargo.toml +++ b/crates/smlib/Cargo.toml @@ -1,30 +1,13 @@ [package] name = "solidcraft-smlib" -description = "SolidCraft: NVIDIA's SMLib B-rep kernel (tangency-labs/smlib, Apache-2.0), statically linked behind its C API, with a safe Rust wrapper" +description = "SolidCraft: the B-rep kernel, solidcore (tangency-labs/solidcore, the Rust port of NVIDIA's SMLib, Apache-2.0), behind a safe wrapper" version.workspace = true edition.workspace = true license.workspace = true publish = false -links = "smlib" -# The workspace lints, except unsafe_code: denied, not forbidden, so that src/ffi.rs can allow it. -# Calling a C library has no safe API. Keep the rest in sync with [workspace.lints]. -[lints.rust] -unsafe_code = "deny" - -[lints.clippy] -dbg_macro = "warn" -todo = "warn" -too_many_arguments = "allow" -type_complexity = "allow" -neg_cmp_op_on_partial_ord = "allow" - -[features] -# Compile and link the C++ SMLib (the apps' and the kernel's `smlib` feature turns it on; off by -# default). Off, the crate builds without a C++ toolchain, the SMLib sources or a prebuilt library, -# build.rs does nothing, solidcore answers every operation, and a call asked of the C++ kernel -# returns `Error::NotBuilt`. -native = [] +[lints] +workspace = true [dependencies] log = { workspace = true } @@ -32,13 +15,6 @@ solidcraft-geom = { workspace = true } solidcraft-profile = { workspace = true } solidcore = { workspace = true } -[target.'cfg(target_arch = "wasm32")'.dependencies] -# The clocks SMLib's C library asks for in a browser build (src/ffi.rs). -web-time = { workspace = true } - -[build-dependencies] -cc = { version = "1.2", features = ["parallel"] } - [dev-dependencies] solidcraft-profile = { workspace = true, features = ["enabled"] } flate2 = "1.1" diff --git a/crates/smlib/SMLIB_PREBUILT b/crates/smlib/SMLIB_PREBUILT deleted file mode 100644 index 1df3eafcd7..0000000000 --- a/crates/smlib/SMLIB_PREBUILT +++ /dev/null @@ -1,10 +0,0 @@ -# Rust target, SHA-256 of smlib-.tar.gz in the release prebuilt- of -# tangency-labs/smlib. Written by scripts/fetch-smlib.sh --pin; never edited by hand. -aarch64-apple-darwin b045d887901556d6c8f60a38fbbecdda765d8dafa6f7a5f72100a7deba3d1d6b -aarch64-pc-windows-msvc c5dbd7dc9221e8638cc9e09273f9e2fe19720f0a02dd40c66d709c3d8effe0c0 -aarch64-unknown-linux-gnu f3ad86d6cd79570c428905548fabdb92005a78a9fc8d200d12047a2e972894ed -i686-pc-windows-msvc 1e4e3999663f4cae113149bcf84d970264b776cd762fd2b7d2dfcf49f9f35772 -wasm32-unknown-unknown 33702afaa8af13e38c9aa8b894fa38f8e69b147c5cda0606c16b0c786dd46389 -x86_64-apple-darwin b41d081c90d0028d76d92c08c72465da39d52976ab416c878d5ff0ecb6eccb73 -x86_64-pc-windows-msvc 10252e3cd701be53697eea11ce68d29b244a2d25b677b57abf632f9f00bbc05b -x86_64-unknown-linux-gnu e6a81eb955405c815e3efc05ac3ab6bd78df83e43d244a6b33bbb390a197c2db diff --git a/crates/smlib/SMLIB_REV b/crates/smlib/SMLIB_REV deleted file mode 100644 index 13788515b7..0000000000 --- a/crates/smlib/SMLIB_REV +++ /dev/null @@ -1 +0,0 @@ -a18bbe523b9c7819f1a9da8c40a54c15f9936125 diff --git a/crates/smlib/build.rs b/crates/smlib/build.rs deleted file mode 100644 index f9eb2f3119..0000000000 --- a/crates/smlib/build.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! Links SMLib, SM_API and their C API (tangency-labs/smlib, at the commit in `SMLIB_REV`) as -//! static libraries, when the `native` feature is on. In order: -//! - `SOLIDCRAFT_SMLIB_DIR`: a checkout, compiled here; -//! - the prebuilt library for the target, as `scripts/fetch-smlib.sh` leaves it in -//! `target/smlib--prebuilt/`, when its SHA-256 is the one in `SMLIB_PREBUILT`; -//! - the sources in `target/smlib-` (`scripts/fetch-smlib.sh --source`), compiled here. -//! -//! The prebuilt libraries come from the fork's `capi/CMakeLists.txt`. The compiled path follows -//! it: the same file lists, defines and include order (SMLib/inc and SM_API/inc each have a -//! StdAfx.h, so each part must find its own first). - -use std::path::{Path, PathBuf}; - -fn main() { - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=SMLIB_REV"); - println!("cargo:rerun-if-changed=SMLIB_PREBUILT"); - println!("cargo:rerun-if-env-changed=SOLIDCRAFT_SMLIB_DIR"); - println!("cargo:rerun-if-env-changed=WASI_SDK_PATH"); - if std::env::var_os("CARGO_FEATURE_NATIVE").is_none() { - return; - } - if let Err(e) = build() { - // A build script has no other way to fail the build. - eprintln!("error: {e}"); - std::process::exit(1); - } -} - -fn build() -> Result<(), String> { - let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").map_err(|e| e.to_string())?); - let rev = std::fs::read_to_string(manifest.join("SMLIB_REV")).map_err(|e| format!("SMLIB_REV: {e}"))?.trim().to_string(); - let rev12 = rev.get(..12).unwrap_or(&rev); - let target = std::env::var("TARGET").unwrap_or_default(); - let fetched = manifest.join("../../target"); - let root = match std::env::var_os("SOLIDCRAFT_SMLIB_DIR") { - Some(d) => PathBuf::from(d), - None => { - let prebuilt = fetched.join(format!("smlib-{rev12}-prebuilt")).join(&target); - if let Some(sha) = pinned(&manifest, &target)? { - println!("cargo:rerun-if-changed={}", prebuilt.join(".sha256").display()); - let have = std::fs::read_to_string(prebuilt.join(".sha256")).unwrap_or_default(); - if have.trim() == sha { - return link_prebuilt(&prebuilt, &target); - } - } - fetched.join(format!("smlib-{rev12}")) - } - }; - if !root.join("capi/include/smlib.h").is_file() { - return Err(format!( - "SMLib {rev} for {target} is not fetched (feature `native`): run scripts/fetch-smlib.sh {target}, or set SOLIDCRAFT_SMLIB_DIR to a tangency-labs/smlib checkout at that commit" - )); - } - println!("cargo:rerun-if-changed={}", root.join("capi").display()); - // SMLib's own sources too: a fix to the kernel must rebuild it. - println!("cargo:rerun-if-changed={}", root.join("source").display()); - let src = root.join("source"); - let core = sources(&src.join("SMLib/src"), &["StdAfx.cpp", "SmDllMain.cpp"])?; - let api = sources(&src.join("SM_API/src"), &["StdAfx.cpp"])?; - let msvc = target.contains("msvc"); - let wasi = if target == "wasm32-unknown-unknown" { Some(wasi_sdk()?) } else { None }; - - let base = || { - let mut b = cc::Build::new(); - b.cpp(true).std("c++17").define("NDEBUG", None).define("NOMINMAX", None).define("SM_STATIC", None).define("SM_RAND_SEED", "20240101u"); - // Rust links the MSVC runtime as a DLL; the cc crate's default (/MD) matches. - if msvc { - b.flag("/EHsc").flag("/bigobj"); - } else { - // No fused multiply-add, as the prebuilt libraries (capi/CMakeLists.txt): SMLib's - // tangent cases decide on the last bits. MSVC does not contract by default. - b.flag("-ffp-contract=off"); - } - if let Some(sdk) = &wasi { - b.compiler(sdk.join("bin/clang++")).archiver(sdk.join("bin/llvm-ar")).target(WASI_TARGET); - b.flag(format!("--sysroot={}", sdk.join("share/wasi-sysroot").display())).flag("-fwasm-exceptions"); - // The exnref encoding, as wasi-sdk's libc++ and Rust use: a module may not mix it with - // the legacy one clang still defaults to. - b.flag("-mllvm").flag("-wasm-use-legacy-eh=false"); - } - b - }; - let quiet = |b: &mut cc::Build| { - b.warnings(false); - if msvc { - b.flag("/w"); - } else { - b.flag("-w"); - } - }; - - // Link order: the C API needs SM_API, which needs SMLib. - let capi = sources(&root.join("capi/src"), &[])?; - let mut c = base(); - c.files(&capi).include(root.join("capi/include")).include(src.join("SM_API/inc")).include(src.join("SMLib/inc")); - quiet(&mut c); - c.try_compile("smlib_capi").map_err(|e| e.to_string())?; - - let mut a = base(); - a.files(&api).include(src.join("SM_API/inc")).include(src.join("SMLib/inc")); - quiet(&mut a); - a.try_compile("smlib_api").map_err(|e| e.to_string())?; - - let mut k = base(); - k.files(&core).include(src.join("SMLib/inc")); - quiet(&mut k); - k.try_compile("smlib_core").map_err(|e| e.to_string())?; - - // The C++ runtime and the C library SMLib needs, from wasi-sdk: the exception-handling build of - // libc++. The few WASI calls libc makes are answered by src/wasi.rs. - if let Some(sdk) = &wasi { - let lib = sdk.join("share/wasi-sysroot/lib").join(WASI_TARGET); - println!("cargo:rustc-link-search=native={}", lib.join("eh").display()); - println!("cargo:rustc-link-search=native={}", lib.display()); - for l in ["c++", "c++abi", "unwind", "c"] { - println!("cargo:rustc-link-lib=static={l}"); - } - } - Ok(()) -} - -/// The SHA-256 `SMLIB_PREBUILT` pins for `target`'s prebuilt archive, if there is one. -fn pinned(manifest: &Path, target: &str) -> Result, String> { - let pins = std::fs::read_to_string(manifest.join("SMLIB_PREBUILT")).map_err(|e| format!("SMLIB_PREBUILT: {e}"))?; - Ok(pins.lines().filter(|l| !l.starts_with('#')).find_map(|l| { - let mut f = l.split_whitespace(); - (f.next() == Some(target)).then(|| f.next().map(str::to_string)).flatten() - })) -} - -/// Links the prebuilt C API in `dir` (one archive, `smlib`) and the C++ runtime it was compiled -/// against: the system's, or for wasm wasi-sdk's, which the archive carries. -fn link_prebuilt(dir: &Path, target: &str) -> Result<(), String> { - let lib = dir.join("lib"); - if !lib.is_dir() { - return Err(format!("{} has no lib folder: run scripts/fetch-smlib.sh {target} again", dir.display())); - } - println!("cargo:rustc-link-search=native={}", lib.display()); - println!("cargo:rustc-link-lib=static=smlib"); - if target == "wasm32-unknown-unknown" { - for l in ["c++", "c++abi", "unwind", "c"] { - println!("cargo:rustc-link-lib=static={l}"); - } - } else if target.contains("apple") { - println!("cargo:rustc-link-lib=c++"); - } else if !target.contains("msvc") { - println!("cargo:rustc-link-lib=stdc++"); - } - Ok(()) -} - -/// The C target SMLib is compiled for inside a `wasm32-unknown-unknown` build: wasi-sdk's, the -/// one with a C++ standard library. -const WASI_TARGET: &str = "wasm32-wasip1"; - -/// wasi-sdk, from `WASI_SDK_PATH` (github.com/WebAssembly/wasi-sdk, release 34 or later). -fn wasi_sdk() -> Result { - let sdk = - std::env::var_os("WASI_SDK_PATH").map(PathBuf::from).ok_or("building SMLib for wasm32-unknown-unknown needs wasi-sdk: set WASI_SDK_PATH")?; - if !sdk.join("bin/clang++").is_file() { - return Err(format!("WASI_SDK_PATH={} has no bin/clang++", sdk.display())); - } - Ok(sdk) -} - -/// The `.cpp` files in `dir`, sorted, without `skip`. -fn sources(dir: &Path, skip: &[&str]) -> Result, String> { - let mut v: Vec = std::fs::read_dir(dir) - .map_err(|e| format!("{}: {e}", dir.display()))? - .filter_map(|e| e.ok().map(|e| e.path())) - .filter(|p| p.extension().is_some_and(|x| x == "cpp") && !p.file_name().is_some_and(|n| skip.iter().any(|s| n == *s))) - .collect(); - v.sort(); - if v.is_empty() { - return Err(format!("no sources in {}", dir.display())); - } - Ok(v) -} diff --git a/crates/smlib/src/check.rs b/crates/smlib/src/check.rs index 13f940076c..21db7d05e7 100644 --- a/crates/smlib/src/check.rs +++ b/crates/smlib/src/check.rs @@ -1,7 +1,7 @@ //! SMLib's own validity check: `SmBrep::ValidatePointers`, then `AssertValid` over the whole //! topology graph. Tests run it on every SMLib result and every import. -use crate::{Brep, Error, Result, core, error, ffi, route}; +use crate::{Brep, Result, core}; /// How many of SMLib's tests to run. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -53,25 +53,11 @@ impl Brep { /// Runs SMLib's validity check at `level`. A body that fails it is still `Ok`, with the /// failures in the report; `Err` means the check could not run. pub fn check(&self, level: CheckLevel) -> Result { - if route::backend() == route::Backend::Solidcore { - return core::check(self.core()?, level); - } - let mut buf = vec![0u8; 1 << 16]; - let (len, failures) = match ffi::check::check(self.cpp()?, level as i32, &mut buf) { - Ok(r) => r, - // Too long for the first buffer: once more with the length it asked for. - Err((ffi::ERR_BUFFER, needed)) => { - buf = vec![0u8; needed.saturating_add(1)]; - ffi::check::check(self.cpp()?, level as i32, &mut buf).map_err(|(s, _)| error(s))? - } - Err((s, _)) => return Err(error(s)), - }; - let text = String::from_utf8_lossy(buf.get(..len).ok_or(Error::Argument)?).into_owned(); - Ok(CheckReport { failures: usize::try_from(failures).unwrap_or(0), text }) + core::check(self.core(), level) } } -#[cfg(all(test, feature = "native"))] +#[cfg(test)] mod tests { use super::*; @@ -105,7 +91,6 @@ mod tests { #[test] fn a_copy_and_a_native_file_round_trip_stay_valid() { - let _cpp = crate::route::on_cpp(); let b = Brep::make_box([0.0; 3], [2.0, 3.0, 4.0]).unwrap(); assert!(b.try_clone().unwrap().check(CheckLevel::All).unwrap().is_valid()); let dir = std::env::temp_dir().join(format!("solidcraft-smlib-check-{}", std::process::id())); diff --git a/crates/smlib/src/core.rs b/crates/smlib/src/core.rs index 5bbc56ac1f..822a76cb8a 100644 --- a/crates/smlib/src/core.rs +++ b/crates/smlib/src/core.rs @@ -398,10 +398,8 @@ pub(crate) static KERNEL: Mutex> = Mutex::new(None); pub(crate) fn run(call: &'static str, f: impl FnOnce(&mut Kernel) -> std::result::Result) -> Result { let mut guard = KERNEL.lock().unwrap_or_else(PoisonError::into_inner); let k = guard.get_or_insert_with(fresh); - // A profiler span per call, as the C API's calls are: where the kernel's time goes. + // A profiler span per call: where the kernel's time goes. let _span = solidcraft_profile::span("solidcore", call); - #[cfg(test)] - crate::route::reached_core(); dump::begin(); dump::set_current(Some(call)); let r = match catch_unwind(AssertUnwindSafe(|| f(k))) { diff --git a/crates/smlib/src/core/blend.rs b/crates/smlib/src/core/blend.rs index f6a1e5035a..ebbbf7e68a 100644 --- a/crates/smlib/src/core/blend.rs +++ b/crates/smlib/src/core/blend.rs @@ -169,11 +169,15 @@ pub(super) fn edge_miss(m: Miss) -> Error { /// point's index to `out_failed_corner` only, so the status comes back bare. fn corner_miss(m: Miss) -> Error { match m { - Miss::None(_) => Error::Status(crate::ffi::ops::ERR_PICK_NONE), - Miss::Ambiguous(_) => Error::Status(crate::ffi::ops::ERR_PICK_AMBIGUOUS), + Miss::None(_) => Error::Status(ERR_PICK_NONE), + Miss::Ambiguous(_) => Error::Status(ERR_PICK_AMBIGUOUS), } } +/// The capi's statuses for a pick that is near nothing, and for one equally near to two. +const ERR_PICK_NONE: i32 = -1101; +const ERR_PICK_AMBIGUOUS: i32 = -1102; + /// A corner set back: its vertex and the setback along each of its edges (the capi's /// `SetBack`; the entry points take no ease, which is 0). struct SetBack { diff --git a/crates/smlib/src/core/blend_tests.rs b/crates/smlib/src/core/blend_tests.rs index e03e1e2312..98a5fecc0e 100644 --- a/crates/smlib/src/core/blend_tests.rs +++ b/crates/smlib/src/core/blend_tests.rs @@ -1,7 +1,5 @@ //! Fillets and chamfers on solidcore, against closed forms: every result exports to a record that -//! validates. With SMLib built, the same case on the C++ kernel gives the same volume, counts and -//! faces by surface kind, or the same error (parity only: the expected values are the analytic -//! ones). +//! validates. //! //! The bodies are solidcore's own boxes and cylinders. @@ -283,82 +281,7 @@ fn solidcore_case(name: &'static str) -> Got { }) } -/// The case on the C++ kernel, called directly (whatever the choice). -#[cfg(feature = "native")] -fn cpp_case(name: &str) -> Got { - use crate::ops::{pick_error, setback_error}; - use crate::{Brep, ffi}; - crate::init().unwrap(); - let make = |s: Shape| -> Brep { - match s { - Shape::Box(o, z) => ffi::make_box(&o, &z), - Shape::Cylinder(o, r, h) => ffi::make_cylinder(&o, r, h), - Shape::Holed(o, z, c, r, h) => { - ffi::ops::boolean(&ffi::make_box(&o, &z).unwrap(), &ffi::make_cylinder(&c, r, h).unwrap(), ffi::ops::BOOLEAN_SUBTRACT) - } - } - .map(Brep::made) - .unwrap() - }; - let kind = |k| match k { - Blend::Round => ffi::ops::BLEND_ROUND, - Blend::Curvature => ffi::ops::BLEND_CURVATURE, - Blend::Chord => ffi::ops::BLEND_CHORD, - Blend::Chamfer => ffi::ops::BLEND_CHAMFER, - }; - let c = case(name); - let tol = tolerance(name); - let r = match &c.op { - Op::Blend(s, k, points, size) => { - ffi::ops::blend(kind(*k), make(*s).cpp().unwrap(), points, &vec![*size; points.len()], tol).map_err(pick_error) - } - Op::Sides(s, points, d, second, first) => { - let (value, angle) = match second { - ChamferSecond::Distance(d) => (*d, false), - ChamferSecond::Angle(a) => (*a, true), - }; - ffi::ops::chamfer_sides(make(*s).cpp().unwrap(), points, tol, *d, value, angle, first).map_err(pick_error) - } - Op::Setback(s, points, size, corners, setbacks) => { - ffi::ops::blend_setback(ffi::ops::BLEND_ROUND, make(*s).cpp().unwrap(), points, &vec![*size; points.len()], corners, setbacks, tol) - .map_err(setback_error) - } - Op::Variable(s, point, start, degree, knots, radii) => { - ffi::ops::fillet_variable(make(*s).cpp().unwrap(), point, tol, start, i32::try_from(*degree).unwrap(), knots, radii).map_err( - |s| match s { - ffi::ops::ERR_PICK_NONE => Error::NoMatch { point: 0 }, - ffi::ops::ERR_PICK_AMBIGUOUS => Error::Ambiguous { point: 0 }, - s => crate::error(s), - }, - ) - } - }; - let got = r.map(|h| { - let v = ffi::volume(&h, 1e-8).unwrap(); - let n = ffi::counts(&h).unwrap().map(|x| usize::try_from(x).unwrap()); - let b = Brep::made(h); - let (rec, _) = native(&b.cpp_arrays().unwrap()).unwrap(); - rec.validate().unwrap(); - Measured { volume: v, counts: n, kinds: kinds(&rec) } - }); - judge(name, "C++", &c.expect, got) -} -/// solidcore and C++ agree: volume within 1e-9, the same counts and faces by kind, or the same -/// error. -#[cfg(feature = "native")] -fn parity(name: &'static str) { - let cpp = cpp_case(name); - let ours = solidcore_case(name); - match (ours, cpp) { - (Ok(a), Ok(b)) => { - assert!(rel(a.volume, b.volume) < 1e-9, "{name}: solidcore {} vs C++ {}", a.volume, b.volume); - assert_eq!(a.counts, b.counts, "{name}: face, edge, vertex counts (solidcore vs C++)"); - assert_eq!(a.kinds, b.kinds, "{name}: faces by surface kind (solidcore vs C++)"); - } - (a, b) => assert_eq!(a.err(), b.err(), "{name}: errors (solidcore vs C++)"), - } -} macro_rules! cases { ($($m:ident $(: $ign:meta)?),* $(,)?) => {$( @@ -368,19 +291,6 @@ macro_rules! cases { fn solidcore_matches_the_closed_form() { let _ = super::solidcore_case(stringify!($m)); } - #[cfg(feature = "native")] - #[test] - $(#[$ign])? - fn solidcore_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - super::parity(stringify!($m)); - } - #[cfg(feature = "native")] - #[test] - fn the_cpp_kernel_matches_the_closed_form() { - let _cpp = crate::route::on_cpp(); - let _ = super::cpp_case(stringify!($m)); - } } )*}; } @@ -437,8 +347,6 @@ fn a_cliff_overflow_the_fork_refuses_has_no_room() { /// over it too (Simpson on 4000 spans, against a bound of its quartic end edge's tolerance). #[test] fn overflow_goes_first() { - #[cfg(feature = "native")] - let _cpp = crate::route::on_cpp(); let f = |u: f64| 0.5 * (u * (441.0 - u * u).sqrt() + 441.0 * (u / 21.0).asin()); let w = 440f64.sqrt(); let alone = 20.0 * (f(w) - f(1.0) - (w - 1.0)); @@ -467,14 +375,6 @@ fn overflow_goes_first() { println!("overflow first {sizes:?}: {:.9}, expected {want:.9}", got.0); assert_eq!(got.1, dropped, "{sizes:?}"); assert!((got.0 - want).abs() <= bound, "{sizes:?}: {} vs {want}", got.0); - #[cfg(feature = "native")] - { - let body = crate::Brep::made_core(Shape::Box([0.0; 3], [20.0; 3]).core()); - let (cpp, d) = crate::ffi::ops::blend_dropping(crate::ffi::ops::BLEND_ROUND, body.cpp().unwrap(), points, sizes, 1.0).unwrap(); - assert_eq!(d, dropped, "C++ {sizes:?}"); - let v = crate::ffi::volume(&cpp, 1e-8).unwrap(); - assert!((got.0 - v).abs() <= bound, "{sizes:?}: solidcore {} vs C++ {v}", got.0); - } } } @@ -612,11 +512,9 @@ fn pocket_chamfer_on_a_boolean_body() { /// The corner of r5 rounds at (60, 40, 20) on a 60 x 40 x 20 box, set back 8 and left open: the /// box's six faces and the three rounds (9 faces, 30 edges, 22 vertices), a sheet, and one hole /// of twelve edges (each section and face curve in two), chained end to start, each beside a face -/// of the sheet. With SMLib built, the C++ kernel gives the same counts and the same hole. +/// of the sheet. #[test] fn a_setback_corner_left_open_returns_its_hole() { - #[cfg(feature = "native")] - let _cpp = crate::route::on_cpp(); let edges = [[30.0, 40.0, 20.0], [60.0, 20.0, 20.0], [60.0, 40.0, 10.0]]; let (open, holes) = timed("setback_open", move || { let b = Shape::Box([0.0; 3], [60.0, 40.0, 20.0]).core(); @@ -636,25 +534,6 @@ fn a_setback_corner_left_open_returns_its_hole() { assert_eq!(end(h, true), end(next, false), "edge {k} ends where the next starts"); assert!(h.face < 9); } - #[cfg(feature = "native")] - { - crate::init().unwrap(); - let b = crate::ffi::make_box(&[0.0; 3], &[60.0, 40.0, 20.0]).map(crate::Brep::made).unwrap(); - let (h, values) = crate::ffi::ops::blend_setback_open( - crate::ffi::ops::BLEND_ROUND, - b.cpp().unwrap(), - &edges, - &[5.0; 3], - &[[60.0, 40.0, 20.0]], - &[8.0], - 0.1, - ) - .unwrap(); - assert_eq!(crate::ffi::counts(&h).unwrap(), [9, 30, 22], "C++ counts"); - let ours: Vec = - std::iter::once(12).chain(hole.edges.iter().flat_map(|h| [h.edge as i64, h.face as i64, i64::from(h.forward)])).collect(); - assert_eq!(values, ours, "the hole (C++ vs solidcore)"); - } // A corner where an edge is not rounded builds no setback, and the refusal names it. let refused = timed("setback_open_refused", move || { let b = Shape::Box([0.0; 3], [60.0, 40.0, 20.0]).core(); @@ -789,64 +668,6 @@ fn the_first_distance_goes_on_the_face_along_first() { } } -/// Face and edge tags (`Brep::tagged`) through blends on solidcore, as on C++: the box is -/// tagged on the C++ kernel, its solidcore body converted from that and tagged the same way, and -/// the two results' histories must be the same. -#[cfg(feature = "native")] -mod history { - use solidcraft_geom::brep::history::Change; - - use crate::history::Raw; - use crate::{Blend, Brep, ffi}; - - /// A 20 x 30 x 10 box, tagged as operand 0, with `kind` of `size` on its top front edge - /// (y = 0, z = 10), on both kernels: solidcore's history, then the C++ kernel's. - fn one_edge(kind: Blend, size: f64) -> (Raw, Raw) { - crate::init().unwrap(); - let a = Brep::made(ffi::make_box(&[0.0; 3], &[20.0, 30.0, 10.0]).unwrap()).tagged(0).unwrap(); - let edge = [[10.0, 0.0, 10.0]]; - let code = match kind { - Blend::Round => ffi::ops::BLEND_ROUND, - Blend::Curvature => ffi::ops::BLEND_CURVATURE, - Blend::Chord => ffi::ops::BLEND_CHORD, - Blend::Chamfer => ffi::ops::BLEND_CHAMFER, - }; - let cpp = ffi::ops::blend(code, a.cpp().unwrap(), &edge, &[size], super::TOL).unwrap(); - let cpp = Brep::made(cpp).history().unwrap(); - let core = super::blend(a.core().unwrap(), kind, &edge, &[size], super::TOL, &[], &[], super::Corners::Filled).unwrap(); - (Brep::made_core(core).history().unwrap(), cpp) - } - - /// The changes of `h`'s faces, sorted: one blend of one box edge leaves the bottom and back - /// as they were, trims the top, front and both sides, and makes the blend face. - fn changes(h: &Raw) -> Vec { - let mut c: Vec = h.faces.iter().map(|f| f.change).collect(); - c.sort_by_key(|c| *c as u8); - c - } - - fn check(kind: Blend, size: f64) { - let (core, cpp) = one_edge(kind, size); - let want = [Change::Kept, Change::Kept, Change::Modified, Change::Modified, Change::Modified, Change::Modified, Change::Generated]; - assert_eq!(changes(&cpp), want, "C++ {kind:?}: {cpp:?}"); - assert_eq!(core.canonical(), cpp.canonical(), "{kind:?}"); - } - - /// A 2 mm round on the top front edge of a box: the top and front are trimmed, the round is - /// new. - #[test] - fn tags_cross_a_fillet() { - let _cpp = crate::route::on_cpp(); - check(Blend::Round, 2.0); - } - - /// A 2 mm chamfer on the same edge: the same faces kept, trimmed and made as the round. - #[test] - fn tags_cross_a_chamfer() { - let _cpp = crate::route::on_cpp(); - check(Blend::Chamfer, 2.0); - } -} /// The capi's `knife_edge_chamfers` (fork e96898d): a 10 high extrusion of the profile whose arc /// of radius R = 21 runs out tangent into its top line at the tip x = 62 1/6 (a knife edge, a @@ -926,25 +747,12 @@ fn edges_871() -> Vec<[f64; 3]> { [23.5, 9.5].iter().flat_map(|x| yz.iter().map(move |(y, z)| [*x, *y, *z])).collect() } + /// #871's rounds at r 0.5 fit (the wall between the window and the block's side is 1.26 thick): /// 38 faces, and the closed form `issue_871_tests::rounds_that_fit_are_closed` derives. At r 1 they -/// overlap, which is refused. With SMLib built, the C++ kernel gives the same. -#[cfg(feature = "native")] +/// overlap, which is refused. #[test] fn rounds_on_the_871_part() { - let _cpp = crate::route::on_cpp(); - rounds_on_the_871_part_check(); -} - -/// The same on a build without the C++ kernel: solidcore's half alone. A test of its own, so that -/// tests/KNOWN_FAILING_TESTS's entry for the C++ comparison (rounds_on_the_871_part) is graded only where it runs. -#[cfg(not(feature = "native"))] -#[test] -fn rounds_on_the_871_part_on_solidcore() { - rounds_on_the_871_part_check(); -} - -fn rounds_on_the_871_part_check() { let p = part_871(); let e = edges_871(); let sizes = |r: f64| vec![r; e.len()]; @@ -956,27 +764,6 @@ fn rounds_on_the_871_part_check() { assert_eq!(refused, Error::NoRoom); assert_eq!(n[0], 38); assert!((v - exact).abs() < 1e-3, "{v} vs {exact}"); - #[cfg(feature = "native")] - { - use crate::{Brep, ffi}; - crate::init().unwrap(); - let bx = |lo: [f64; 3], hi: [f64; 3]| ffi::make_box(&lo, &[hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]]).unwrap(); - let top = ffi::ops::boolean( - &bx([9.5, 8.0, 0.0], [23.5, 29.5, 10.0]), - &bx([9.5, 12.74437365538526, 10.0], [23.5, 22.69275963366936, 20.0]), - ffi::ops::BOOLEAN_UNION, - ) - .unwrap(); - let cut = ffi::ops::boolean(&top, &bx([9.5, 14.0, 7.0], [23.5, 19.2, 14.1]), ffi::ops::BOOLEAN_SUBTRACT).unwrap(); - let cpp = Brep::made(cut); - let h = cpp.cpp().unwrap(); - let fit = ffi::ops::blend(ffi::ops::BLEND_ROUND, h, &e, &sizes(0.5), TOL).unwrap(); - let (cv, cn) = (ffi::volume(&fit, 1e-8).unwrap(), ffi::counts(&fit).unwrap().map(|x| usize::try_from(x).unwrap())); - let crefused = ffi::ops::blend(ffi::ops::BLEND_ROUND, h, &e, &sizes(1.0), TOL).map(|_| ()); - assert!(rel(v, cv) < 1e-9, "solidcore {v} vs C++ {cv}"); - assert_eq!(n, cn); - assert!(crefused.is_err()); - } } /// The fork's `tabbed_plate_rounds_mitred_on_the_arcs_continuation`: a plate 2 thick, its outline an @@ -984,11 +771,9 @@ fn rounds_on_the_871_part_check() { /// outline rounded r 0.5. The arc's round and the tab side's are mitred on the arc wall's /// continuation. Removed: the outline's area less the area of the outline moved in by /// d = r (1 - cos phi), at the height r sin phi into the round (midpoint rule, 8192 steps; the area -/// by Green's theorem). With SMLib built, the C++ kernel gives the same. +/// by Green's theorem). #[test] fn tabbed_plate_rounds_mitred_on_the_arcs_continuation() { - #[cfg(feature = "native")] - let _cpp = crate::route::on_cpp(); use super::sweep::extrude; use crate::sweeps::{Curve, DraftCorner}; const Z: [f64; 3] = [0.0, 0.0, 1.0]; @@ -1037,15 +822,42 @@ fn tabbed_plate_rounds_mitred_on_the_arcs_continuation() { rec.validate().unwrap(); assert!(super::is_manifold_solid(&round).unwrap()); assert!(super::check(&round, crate::check::CheckLevel::All).unwrap().is_valid()); - #[cfg(feature = "native")] - { - use crate::{Brep, ffi}; - crate::init().unwrap(); - let list = crate::sweeps::List::of(&outline).unwrap(); - let cpp = ffi::sweeps::extrude(&list.raw(), &Z, 2.0, 0.0, ffi::sweeps::DRAFT_CORNER_CURVATURE, true, &mut [f64::NAN; 3]).unwrap(); - let cround = ffi::ops::blend(ffi::ops::BLEND_ROUND, Brep::made(cpp).cpp().unwrap(), &top, &[r; 6], TOL).unwrap(); - let cv = ffi::volume(&cround, 1e-8).unwrap(); - assert!(rel(volume(&round, 1e-8).unwrap(), cv) < 1e-9); - assert_eq!(counts(&round).unwrap(), ffi::counts(&cround).unwrap().map(|x| usize::try_from(x).unwrap())); +} + +/// Face and edge tags (`Brep::tagged`) through blends: a 20 x 30 x 10 box tagged as operand 0, +/// with one blend on its top front edge (y = 0, z = 10), leaves the bottom and back as they were, +/// trims the top, front and both sides, and makes the blend face. +mod history { + use solidcraft_geom::brep::history::Change; + + use crate::history::Raw; + use crate::{Blend, Brep}; + + fn one_edge(kind: Blend, size: f64) -> Raw { + let a = Brep::make_box([0.0; 3], [20.0, 30.0, 10.0]).unwrap().tagged(0).unwrap(); + let edge = [[10.0, 0.0, 10.0]]; + let core = super::blend(a.core(), kind, &edge, &[size], super::TOL, &[], &[], super::Corners::Filled).unwrap(); + Brep::made_core(core).history().unwrap() + } + + fn check(kind: Blend, size: f64) { + let h = one_edge(kind, size); + let mut changes: Vec = h.faces.iter().map(|f| f.change).collect(); + changes.sort_by_key(|c| *c as u8); + let want = [Change::Kept, Change::Kept, Change::Modified, Change::Modified, Change::Modified, Change::Modified, Change::Generated]; + assert_eq!(changes, want, "{kind:?}: {h:?}"); + } + + /// A 2 mm round on the top front edge of a box: the top and front are trimmed, the round is + /// new. + #[test] + fn tags_cross_a_fillet() { + check(Blend::Round, 2.0); + } + + /// A 2 mm chamfer on the same edge: the same faces kept, trimmed and made as the round. + #[test] + fn tags_cross_a_chamfer() { + check(Blend::Chamfer, 2.0); } } diff --git a/crates/smlib/src/core/boolean_tests.rs b/crates/smlib/src/core/boolean_tests.rs index 8b6606a158..c18394a602 100644 --- a/crates/smlib/src/core/boolean_tests.rs +++ b/crates/smlib/src/core/boolean_tests.rs @@ -1,7 +1,5 @@ -//! Booleans on solidcore, against analytic volumes: every result exports to a record that -//! validates and counts the solids it should. With SMLib built, the same case on the C++ kernel -//! gives the same volume, counts, faces by surface kind and solids (parity only: the expected -//! values are the analytic ones). +//! Booleans on solidcore, against analytic volumes: every result exports to a record that validates +//! and counts the solids it should. //! //! Curved operands are built on solidcore itself. @@ -188,65 +186,7 @@ fn solidcore_case(name: &'static str) -> Option { }) } -/// The case on the C++ kernel, called directly (whatever the choice): what it measured. -#[cfg(feature = "native")] -fn cpp_case(name: &str) -> Option { - use crate::{Brep, ffi}; - crate::init().unwrap(); - let make = |s: Shape| -> Brep { - match s { - Shape::Box(o, z) => ffi::make_box(&o, &z), - Shape::Cylinder(o, r, h) => ffi::make_cylinder(&o, r, h), - Shape::Sphere(c, r) => ffi::make_sphere(&c, r), - } - .map(Brep::made) - .unwrap() - }; - let bool_op = |op| match op { - BooleanOp::Union => ffi::ops::BOOLEAN_UNION, - BooleanOp::Subtract => ffi::ops::BOOLEAN_SUBTRACT, - BooleanOp::Intersect => ffi::ops::BOOLEAN_INTERSECT, - }; - let c = case(name); - let r = match &c.op { - Op::Boolean(a, b, op) => ffi::ops::boolean(make(*a).cpp().unwrap(), make(*b).cpp().unwrap(), bool_op(*op)), - Op::UnionAll(shapes) => { - let bodies: Vec = shapes.iter().map(|s| make(*s)).collect(); - let handles: Vec<&ffi::Handle> = bodies.iter().map(|b| b.cpp().unwrap()).collect(); - ffi::ops::boolean_union_all(&handles) - } - Op::Imprint(a, t) => ffi::ops::imprint(make(*a).cpp().unwrap(), make(*t).cpp().unwrap()), - }; - let h = match r { - Ok(h) => h, - Err(ffi::ERR_EMPTY) => return None, - Err(s) => panic!("{name}: C++ failed: {:?}", crate::error(s)), - }; - let v = ffi::volume(&h, 1e-8).unwrap(); - let n = ffi::counts(&h).unwrap().map(|x| usize::try_from(x).unwrap()); - let solids = usize::try_from(ffi::ops::solid_count(&h).unwrap()).unwrap(); - let b = Brep::made(h); - let (rec, _) = native(&b.cpp_arrays().unwrap()).unwrap(); - rec.validate().unwrap(); - Some(Measured { volume: v, counts: n, kinds: kinds(&rec), solids }) -} -/// solidcore and C++ agree: volume within 1e-9, the same counts and faces by kind. -#[cfg(feature = "native")] -fn parity(name: &'static str) { - let ours = solidcore_case(name); - let cpp = cpp_case(name); - match (ours, cpp) { - (None, None) => {} - (Some(a), Some(b)) => { - assert!(rel(a.volume, b.volume) < 1e-9, "{name}: solidcore {} vs C++ {}", a.volume, b.volume); - assert_eq!(a.counts, b.counts, "{name}: face, edge, vertex counts (solidcore vs C++)"); - assert_eq!(a.kinds, b.kinds, "{name}: faces by surface kind (solidcore vs C++)"); - assert_eq!(a.solids, b.solids, "{name}: solids (solidcore vs C++)"); - } - (a, b) => panic!("{name}: solidcore {a:?} vs C++ {b:?}"), - } -} macro_rules! cases { ($($m:ident $(: $ign:meta)?),* $(,)?) => {$( @@ -256,13 +196,6 @@ macro_rules! cases { fn solidcore_matches_the_analytic_volume() { super::solidcore_case(stringify!($m)); } - #[cfg(feature = "native")] - #[test] - $(#[$ign])? - fn solidcore_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - super::parity(stringify!($m)); - } } )*}; } @@ -390,305 +323,6 @@ fn a_failed_closest_point_counts_as_off_the_piece() { .unwrap(); } -/// Face and edge tags (`Brep::tagged`) through operations on solidcore, as on C++: each -/// operand is tagged on the C++ kernel, its solidcore body converted from that and tagged the same -/// way there, and the two results' histories ([`Raw`], each in its own kernel's face and edge -/// order) must be the same. -#[cfg(feature = "native")] -mod history { - use crate::history::Raw; - use crate::{BooleanOp, Brep, ffi}; - - fn tagged_box(origin: [f64; 3], size: [f64; 3], operand: usize) -> Brep { - crate::init().unwrap(); - Brep::made(ffi::make_box(&origin, &size).unwrap()).tagged(operand).unwrap() - } - - /// Two 10 mm cubes overlapping in a 5 mm one (the capi's `overlapping_boxes_union`): 12 - /// faces, each from one face of one box, six from each. - fn check(h: &Raw) { - assert_eq!(h.faces.len(), 12, "{h:?}"); - assert!(h.faces.iter().all(|f| f.origins.len() == 1), "a face without exactly one origin: {h:?}"); - for operand in [0, 1] { - assert_eq!(h.faces.iter().filter(|f| f.origins[0].0 == operand).count(), 6, "operand {operand}: {h:?}"); - } - } - - #[test] - fn tags_cross_a_cpp_boolean() { - let _cpp = crate::route::on_cpp(); - let (a, b) = (tagged_box([0.0; 3], [10.0; 3], 0), tagged_box([5.0; 3], [10.0; 3], 1)); - let r = Brep::made(ffi::ops::boolean(a.cpp().unwrap(), b.cpp().unwrap(), ffi::ops::BOOLEAN_UNION).unwrap()); - check(&r.history().unwrap()); - } - - #[test] - fn tags_cross_a_solidcore_boolean() { - let _cpp = crate::route::on_cpp(); - let (a, b) = (tagged_box([0.0; 3], [10.0; 3], 0), tagged_box([5.0; 3], [10.0; 3], 1)); - let r = super::boolean(a.core().unwrap(), b.core().unwrap(), BooleanOp::Union).unwrap().unwrap(); - check(&Brep::made_core(r).history().unwrap()); - } - - /// `a` op `b` on both kernels: the same history. - fn parity(a: &Brep, b: &Brep, op: BooleanOp) { - let code = match op { - BooleanOp::Union => ffi::ops::BOOLEAN_UNION, - BooleanOp::Subtract => ffi::ops::BOOLEAN_SUBTRACT, - BooleanOp::Intersect => ffi::ops::BOOLEAN_INTERSECT, - }; - let cpp = Brep::made(ffi::ops::boolean(a.cpp().unwrap(), b.cpp().unwrap(), code).unwrap()).history().unwrap(); - let core = Brep::made_core(super::boolean(a.core().unwrap(), b.core().unwrap(), op).unwrap().unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - } - - #[test] - fn an_overlapping_union_has_the_cpp_history() { - let _cpp = crate::route::on_cpp(); - parity(&tagged_box([0.0; 3], [10.0; 3], 0), &tagged_box([5.0; 3], [10.0; 3], 1), BooleanOp::Union); - } - - /// A slot across the top of a 20 mm cube: the top is cut in two, the slot's walls and floor - /// come from the tool. - #[test] - fn a_slot_has_the_cpp_history() { - let _cpp = crate::route::on_cpp(); - let (a, b) = (tagged_box([0.0; 3], [20.0; 3], 0), tagged_box([5.0, -1.0, 10.0], [10.0, 22.0, 15.0], 1)); - parity(&a, &b, BooleanOp::Subtract); - } - - /// Two boxes side by side, sharing the face x = 10 and their top and bottom planes: faces - /// found lying on each other's (SMLib's coincidences and merges) stand for both. - #[test] - fn boxes_side_by_side_have_the_cpp_history() { - let _cpp = crate::route::on_cpp(); - let (a, b) = (tagged_box([0.0; 3], [10.0; 3], 0), tagged_box([10.0, 0.0, 0.0], [10.0; 3], 1)); - parity(&a, &b, BooleanOp::Union); - } - - /// A box sharing one corner region with another: the intersection keeps three faces of each. - #[test] - fn an_intersection_has_the_cpp_history() { - let _cpp = crate::route::on_cpp(); - parity(&tagged_box([0.0; 3], [10.0; 3], 0), &tagged_box([5.0; 3], [10.0; 3], 1), BooleanOp::Intersect); - } - - /// `SmApiBrepCopy` (the copy every solidcore operation starts from) keeps every tag: the - /// copy's history is the operand's own, every face Kept, as on C++. - #[test] - fn tags_cross_a_copy() { - let _cpp = crate::route::on_cpp(); - let a = tagged_box([0.0; 3], [10.0, 20.0, 30.0], 0); - // Converted (and tagged) before the kernel's lock is taken: the conversion takes it too. - let body = a.core().unwrap(); - let copy = super::run("copy", |k| { - let g = body.0.lock().unwrap(); - solidcore::topology::brep::p3::brep_copy(k, &g) - }) - .unwrap(); - let core = Brep::made_core(super::Body::new(copy)).history().unwrap(); - let cpp = Brep::made(ffi::copy(a.cpp().unwrap()).unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - assert!(core.faces.iter().all(|f| f.change == solidcraft_geom::brep::history::Change::Kept), "{core:?}"); - } - - /// A post standing on the top of a cube, imprinted on it: the top is split along the - /// post's foot, the other faces are as they were. - #[test] - fn an_imprint_has_the_cpp_history() { - let _cpp = crate::route::on_cpp(); - let (a, b) = (tagged_box([0.0; 3], [20.0; 3], 0), tagged_box([5.0, 5.0, 10.0], [10.0, 10.0, 20.0], 1)); - let cpp = Brep::made(ffi::ops::imprint(a.cpp().unwrap(), b.cpp().unwrap()).unwrap()).history().unwrap(); - let core = Brep::made_core(super::imprint(a.core().unwrap(), b.core().unwrap()).unwrap().unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - } - - /// A 10 mm wide bar laid across the top of a 40 x 30 x 20 block, overhanging its front and - /// back, imprinted on it: the bar's sides cut the top in three, and cut the top edges of the - /// faces y = 0 and y = 30 only by a vertex each (where they cross them at x = 10 and x = 20). - /// Those faces are still all of themselves, their boundaries where they were: Kept, as are - /// x = 0, x = 40 and the bottom (the fork's 1c7accd; before, y = 0 and y = 30 were Modified). - #[test] - fn a_face_whose_edge_is_only_cut_by_a_vertex_is_kept() { - let _cpp = crate::route::on_cpp(); - use solidcraft_geom::brep::history::Change; - let (a, b) = (tagged_box([0.0; 3], [40.0, 30.0, 20.0], 0), tagged_box([10.0, -5.0, 20.0], [10.0, 40.0, 10.0], 1)); - let cpp = Brep::made(ffi::ops::imprint(a.cpp().unwrap(), b.cpp().unwrap()).unwrap()).history().unwrap(); - let core = Brep::made_core(super::imprint(a.core().unwrap(), b.core().unwrap()).unwrap().unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - let of_a = |h: &Raw, c: Change| h.faces.iter().filter(|f| f.origins.iter().all(|o| o.0 == 0) && f.change == c).count(); - for h in [&core, &cpp] { - assert_eq!(of_a(h, Change::Kept), 5, "{h:?}"); - assert_eq!(of_a(h, Change::Modified), 0, "{h:?}"); - } - } - - /// A 10 mm post standing on a 20 mm cube's top against its x = 0 side, imprinted on it: the - /// post's foot cuts the top's edge along x = 0 by two vertices. Each piece of that edge is a - /// split child, standing for the edge and lying where it lay (the capi's Split gives an untagged - /// child the split edge's `at`), so the side x = 0 is still all of itself and Kept. - #[test] - fn an_imprint_split_edge_lies_where_the_edge_lay() { - let _cpp = crate::route::on_cpp(); - use solidcraft_geom::brep::history::Change; - let (a, b) = (tagged_box([0.0; 3], [20.0; 3], 0), tagged_box([0.0, 5.0, 20.0], [10.0; 3], 1)); - let cpp = Brep::made(ffi::ops::imprint(a.cpp().unwrap(), b.cpp().unwrap()).unwrap()).history().unwrap(); - let core = Brep::made_core(super::imprint(a.core().unwrap(), b.core().unwrap()).unwrap().unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - let of_a = |h: &Raw, c: Change| h.faces.iter().filter(|f| f.origins.iter().all(|o| o.0 == 0) && f.change == c).count(); - for h in [&core, &cpp] { - // The top's edge along x = 0 is in three pieces, each standing for it alone. - let pieces = |e: usize| h.edges.iter().filter(|x| x.origins == [(0, e)]).count(); - assert!((0..12).any(|e| pieces(e) == 3), "{h:?}"); - assert_eq!(of_a(h, Change::Kept), 5, "{h:?}"); - assert_eq!(of_a(h, Change::Modified), 0, "{h:?}"); - assert_eq!(of_a(h, Change::Split), 2, "{h:?}"); - } - } - - /// A 10 mm post standing on a 20 mm cube's top, subtracted from it: the post only touches the - /// top, which the boolean copies the post's foot onto (`SmAttribute::Copy`). SMLib's Copy - /// leaves a target that already has a tag alone, so the top stands for the cube's top only. - #[test] - fn a_copy_onto_a_tagged_face_keeps_its_own_tag() { - let _cpp = crate::route::on_cpp(); - use solidcraft_geom::brep::history::Change; - let (a, b) = (tagged_box([0.0; 3], [20.0; 3], 0), tagged_box([5.0, 5.0, 20.0], [10.0; 3], 1)); - parity(&a, &b, BooleanOp::Subtract); - let core = Brep::made_core(super::boolean(a.core().unwrap(), b.core().unwrap(), BooleanOp::Subtract).unwrap().unwrap()).history().unwrap(); - assert_eq!(core.faces.len(), 6, "{core:?}"); - assert!(core.faces.iter().all(|f| f.change == Change::Kept && f.origins.len() == 1 && f.origins[0].0 == 0), "{core:?}"); - } - - /// Two 10 mm cubes side by side sharing the face x = 10, the second subtracted from the first: - /// the second's edge on that face is copied onto the first's edge it lies on, which keeps its - /// own tag (SMLib's Copy), as on C++. - #[test] - fn a_copy_onto_a_tagged_edge_has_the_cpp_history() { - let _cpp = crate::route::on_cpp(); - parity(&tagged_box([0.0; 3], [10.0; 3], 0), &tagged_box([10.0, 0.0, 0.0], [10.0; 3], 1), BooleanOp::Subtract); - } - - fn tagged_cylinder(origin: [f64; 3], radius: f64, height: f64, operand: usize) -> Brep { - crate::init().unwrap(); - Brep::made(ffi::make_cylinder(&origin, radius, height).unwrap()).tagged(operand).unwrap() - } - - /// A 40 x 40 x 10 plate with a round hole r 5 at (20, 20), made before tagging: its hole's rims - /// are whole circles, edges closed on themselves. - fn holed_plate(operand: usize) -> Brep { - crate::init().unwrap(); - let plate = ffi::make_box(&[0.0; 3], &[40.0, 40.0, 10.0]).unwrap(); - let rod = ffi::make_cylinder(&[20.0, 20.0, -5.0], 5.0, 20.0).unwrap(); - Brep::made(ffi::ops::boolean(&plate, &rod, ffi::ops::BOOLEAN_SUBTRACT).unwrap()).tagged(operand).unwrap() - } - - /// Faces of operand 0 with this change, in a history. - fn of_first(h: &Raw, c: solidcraft_geom::brep::history::Change) -> usize { - h.faces.iter().filter(|f| f.origins.iter().all(|o| o.0 == 0) && !f.origins.is_empty() && f.change == c).count() - } - - /// A hole through a block: the tool's and the block's faces meet along whole circles. - #[test] - fn a_round_hole_has_the_cpp_history() { - let _cpp = crate::route::on_cpp(); - parity(&tagged_box([0.0; 3], [40.0, 40.0, 10.0], 0), &tagged_cylinder([20.0, 20.0, -5.0], 5.0, 20.0, 1), BooleanOp::Subtract); - } - - /// A corner cut far from the hole: the hole's wall keeps its two whole circles, each a closed - /// edge still where it lay (`moved`'s `open.is_empty()`), so it is Kept on both kernels. - #[test] - fn a_face_bounded_by_closed_edges_is_kept() { - let _cpp = crate::route::on_cpp(); - use solidcraft_geom::brep::history::Change; - let (a, b) = (holed_plate(0), tagged_box([35.0, 35.0, -5.0], [10.0, 10.0, 20.0], 1)); - let cpp = Brep::made(ffi::ops::boolean(a.cpp().unwrap(), b.cpp().unwrap(), ffi::ops::BOOLEAN_SUBTRACT).unwrap()).history().unwrap(); - let core = Brep::made_core(super::boolean(a.core().unwrap(), b.core().unwrap(), BooleanOp::Subtract).unwrap().unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - // The hole's wall and the two sides the corner misses. - assert_eq!(of_first(&core, Change::Kept), 3, "{core:?}"); - } - - /// A bar laid on the plate over the hole, imprinted: the bar's sides cross the hole's upper rim - /// and cut that closed edge by vertices; the top is split, the wall's rim stays where it lay. - #[test] - fn an_imprint_cutting_a_closed_edge_has_the_cpp_history() { - let _cpp = crate::route::on_cpp(); - let (a, b) = (holed_plate(0), tagged_box([-5.0, 18.0, 10.0], [50.0, 4.0, 5.0], 1)); - let cpp = Brep::made(ffi::ops::imprint(a.cpp().unwrap(), b.cpp().unwrap()).unwrap()).history().unwrap(); - let core = Brep::made_core(super::imprint(a.core().unwrap(), b.core().unwrap()).unwrap().unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - } - - /// A cylinder and a sphere cut by a slab across their seams (each kernel's angle 0, along +x): - /// each curved face is split in two across its seam. - #[test] - fn a_curved_face_split_across_its_seam_has_the_cpp_history() { - let _cpp = crate::route::on_cpp(); - use solidcraft_geom::brep::history::Change; - let slab = || tagged_box([-20.0, -1.0, -20.0], [40.0, 2.0, 40.0], 1); - let sphere = Brep::made(ffi::make_sphere(&[0.0, 0.0, 0.0], 7.0).unwrap()).tagged(0).unwrap(); - for (name, a) in [("cylinder", tagged_cylinder([0.0, 0.0, -5.0], 5.0, 10.0, 0)), ("sphere", sphere)] { - parity(&a, &slab(), BooleanOp::Subtract); - let cpp = Brep::made(ffi::ops::boolean(a.cpp().unwrap(), slab().cpp().unwrap(), ffi::ops::BOOLEAN_SUBTRACT).unwrap()).history().unwrap(); - assert!(of_first(&cpp, Change::Split) >= 2, "{name}: {cpp:?}"); - } - } - - /// A chain of three overlapping boxes in one `union_all`. - #[test] - fn a_union_of_three_has_the_cpp_history() { - let _cpp = crate::route::on_cpp(); - let (a, b, c) = (tagged_box([0.0; 3], [10.0; 3], 0), tagged_box([5.0; 3], [10.0; 3], 1), tagged_box([10.0; 3], [10.0; 3], 2)); - let cpp = Brep::made(ffi::ops::boolean_union_all(&[a.cpp().unwrap(), b.cpp().unwrap(), c.cpp().unwrap()]).unwrap()).history().unwrap(); - let core = Brep::made_core(super::union_all(&[a.core().unwrap(), b.core().unwrap(), c.core().unwrap()]).unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - assert_eq!(core.faces.iter().flat_map(|f| f.origins.iter().map(|o| o.0)).max(), Some(2), "{core:?}"); - } - - /// A row of four boxes sharing their top and bottom planes, united in pairs and the pairs - /// united: each top merged with its neighbour's stands for two faces, and the merge of those - /// stands for all four, both sources carrying several origins. - #[test] - fn a_merge_of_several_origins_on_each_side_has_the_cpp_history() { - let _cpp = crate::route::on_cpp(); - let boxes: Vec = (0..4u32).map(|i| tagged_box([f64::from(i) * 10.0, 0.0, 0.0], [10.0; 3], i as usize)).collect(); - let c = |x: usize, y: usize| ffi::ops::boolean(boxes[x].cpp().unwrap(), boxes[y].cpp().unwrap(), ffi::ops::BOOLEAN_UNION).unwrap(); - let (ab, cd) = (c(0, 1), c(2, 3)); - let cpp = Brep::made(ffi::ops::boolean(&ab, &cd, ffi::ops::BOOLEAN_UNION).unwrap()).history().unwrap(); - let s = |x: usize, y: usize| super::boolean(boxes[x].core().unwrap(), boxes[y].core().unwrap(), BooleanOp::Union).unwrap().unwrap(); - let (ab, cd) = (s(0, 1), s(2, 3)); - let core = Brep::made_core(super::boolean(&ab, &cd, BooleanOp::Union).unwrap().unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - assert!(core.faces.iter().any(|f| f.origins.len() == 4), "{core:?}"); - } - - /// Two overlapping squares combined as regions of one plane. - #[test] - fn a_boolean2d_has_the_cpp_history() { - let _cpp = crate::route::on_cpp(); - use crate::sweeps::Curve; - let square = |x: f64, y: f64, s: f64, operand: usize| { - crate::init().unwrap(); - let c = [[x, y, 0.0], [x + s, y, 0.0], [x + s, y + s, 0.0], [x, y + s, 0.0]]; - let l: Vec = (0..4).map(|i| Curve::Line { start: c[i], end: c[(i + 1) % 4] }).collect(); - crate::sweeps::ON_CPP.with(|f| f.set(true)); - let b = Brep::planar_faces(&l).unwrap(); - crate::sweeps::ON_CPP.with(|f| f.set(false)); - b.tagged(operand).unwrap() - }; - let (a, b) = (square(0.0, 0.0, 10.0, 0), square(5.0, 5.0, 10.0, 1)); - for (op, code) in [ - (BooleanOp::Union, ffi::ops::BOOLEAN_UNION), - (BooleanOp::Subtract, ffi::ops::BOOLEAN_SUBTRACT), - (BooleanOp::Intersect, ffi::ops::BOOLEAN_INTERSECT), - ] { - let cpp = Brep::made(ffi::ops::boolean2d(a.cpp().unwrap(), b.cpp().unwrap(), code).unwrap()).history().unwrap(); - let core = Brep::made_core(super::boolean2d(a.core().unwrap(), b.core().unwrap(), op).unwrap().unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical(), "{op:?}"); - } - } -} /// Rig case booleans/2928, reduced (fork c2e3d9d): a ring, r 3..5 about (0, 10), z 0..2, unioned /// with a half annulus prism, r 4..5 about (e, 10), z 2..20, whose cap sits on the ring's top. Two @@ -726,3 +360,140 @@ fn a_ring_and_a_lip_with_walls_nearly_together() { assert!((v - 113.0 * PI).abs() < 1e-3, "e {e}: {v}"); } } + +/// Face and edge tags (`Brep::tagged`) through booleans: each operand tagged, the result's history +/// ([`Raw`]) read back. +mod history { + use solidcraft_geom::brep::history::Change; + + use crate::history::Raw; + use crate::{BooleanOp, Brep}; + + fn tagged_box(origin: [f64; 3], size: [f64; 3], operand: usize) -> Brep { + Brep::make_box(origin, size).unwrap().tagged(operand).unwrap() + } + + fn tagged_cylinder(origin: [f64; 3], radius: f64, height: f64, operand: usize) -> Brep { + Brep::make_cylinder(origin, radius, height).unwrap().tagged(operand).unwrap() + } + + /// A 40 x 40 x 10 plate with a round hole r 5 at (20, 20), made before tagging: its hole's rims + /// are whole circles, edges closed on themselves. + fn holed_plate(operand: usize) -> Brep { + let plate = Brep::make_box([0.0; 3], [40.0, 40.0, 10.0]).unwrap(); + let rod = Brep::make_cylinder([20.0, 20.0, -5.0], 5.0, 20.0).unwrap(); + plate.boolean(&rod, BooleanOp::Subtract).unwrap().unwrap().tagged(operand).unwrap() + } + + fn boolean(a: &Brep, b: &Brep, op: BooleanOp) -> Raw { + a.boolean(b, op).unwrap().unwrap().history().unwrap() + } + + fn imprint(a: &Brep, b: &Brep) -> Raw { + a.imprint(b).unwrap().unwrap().history().unwrap() + } + + /// Faces of operand 0 with this change. + fn of_first(h: &Raw, c: Change) -> usize { + h.faces.iter().filter(|f| !f.origins.is_empty() && f.origins.iter().all(|o| o.0 == 0) && f.change == c).count() + } + + /// Two 10 mm cubes overlapping in a 5 mm one (the capi's `overlapping_boxes_union`): 12 + /// faces, each from one face of one box, six from each. + #[test] + fn tags_cross_a_boolean() { + let h = boolean(&tagged_box([0.0; 3], [10.0; 3], 0), &tagged_box([5.0; 3], [10.0; 3], 1), BooleanOp::Union); + assert_eq!(h.faces.len(), 12, "{h:?}"); + assert!(h.faces.iter().all(|f| f.origins.len() == 1), "a face without exactly one origin: {h:?}"); + for operand in [0, 1] { + assert_eq!(h.faces.iter().filter(|f| f.origins[0].0 == operand).count(), 6, "operand {operand}: {h:?}"); + } + } + + /// `SmApiBrepCopy` (the copy every solidcore operation starts from) keeps every tag: the + /// copy's history is the operand's own, every face Kept. + #[test] + fn tags_cross_a_copy() { + let a = tagged_box([0.0; 3], [10.0, 20.0, 30.0], 0); + let h = a.try_clone().unwrap().history().unwrap(); + assert_eq!(h.faces.len(), 6, "{h:?}"); + assert!(h.faces.iter().all(|f| f.change == Change::Kept), "{h:?}"); + } + + /// A 10 mm wide bar laid across the top of a 40 x 30 x 20 block, overhanging its front and + /// back, imprinted on it: the bar's sides cut the top in three, and cut the top edges of the + /// faces y = 0 and y = 30 only by a vertex each (where they cross them at x = 10 and x = 20). + /// Those faces are still all of themselves, their boundaries where they were: Kept, as are + /// x = 0, x = 40 and the bottom. + #[test] + fn a_face_whose_edge_is_only_cut_by_a_vertex_is_kept() { + let h = imprint(&tagged_box([0.0; 3], [40.0, 30.0, 20.0], 0), &tagged_box([10.0, -5.0, 20.0], [10.0, 40.0, 10.0], 1)); + assert_eq!(of_first(&h, Change::Kept), 5, "{h:?}"); + assert_eq!(of_first(&h, Change::Modified), 0, "{h:?}"); + } + + /// A 10 mm post standing on a 20 mm cube's top against its x = 0 side, imprinted on it: the + /// post's foot cuts the top's edge along x = 0 by two vertices. Each piece of that edge is a + /// split child, standing for the edge and lying where it lay (the capi's Split gives an untagged + /// child the split edge's `at`), so the side x = 0 is still all of itself and Kept. + #[test] + fn an_imprint_split_edge_lies_where_the_edge_lay() { + let h = imprint(&tagged_box([0.0; 3], [20.0; 3], 0), &tagged_box([0.0, 5.0, 20.0], [10.0; 3], 1)); + // The top's edge along x = 0 is in three pieces, each standing for it alone. + let pieces = |e: usize| h.edges.iter().filter(|x| x.origins == [(0, e)]).count(); + assert!((0..12).any(|e| pieces(e) == 3), "{h:?}"); + assert_eq!(of_first(&h, Change::Kept), 5, "{h:?}"); + assert_eq!(of_first(&h, Change::Modified), 0, "{h:?}"); + assert_eq!(of_first(&h, Change::Split), 2, "{h:?}"); + } + + /// A 10 mm post standing on a 20 mm cube's top, subtracted from it: the post only touches the + /// top, which the boolean copies the post's foot onto (`SmAttribute::Copy`). SMLib's Copy + /// leaves a target that already has a tag alone, so the top stands for the cube's top only. + #[test] + fn a_copy_onto_a_tagged_face_keeps_its_own_tag() { + let h = boolean(&tagged_box([0.0; 3], [20.0; 3], 0), &tagged_box([5.0, 5.0, 20.0], [10.0; 3], 1), BooleanOp::Subtract); + assert_eq!(h.faces.len(), 6, "{h:?}"); + assert!(h.faces.iter().all(|f| f.change == Change::Kept && f.origins.len() == 1 && f.origins[0].0 == 0), "{h:?}"); + } + + /// A corner cut far from the hole: the hole's wall keeps its two whole circles, each a closed + /// edge still where it lay (`moved`'s `open.is_empty()`), so it is Kept, with the two sides the + /// corner misses. + #[test] + fn a_face_bounded_by_closed_edges_is_kept() { + let h = boolean(&holed_plate(0), &tagged_box([35.0, 35.0, -5.0], [10.0, 10.0, 20.0], 1), BooleanOp::Subtract); + assert_eq!(of_first(&h, Change::Kept), 3, "{h:?}"); + } + + /// A cylinder and a sphere cut by a slab across their seams (angle 0, along +x): each curved + /// face is split in two across its seam. + #[test] + fn a_curved_face_split_across_its_seam_is_split() { + let slab = || tagged_box([-20.0, -1.0, -20.0], [40.0, 2.0, 40.0], 1); + let sphere = Brep::make_sphere([0.0; 3], 7.0).unwrap().tagged(0).unwrap(); + for (name, a) in [("cylinder", tagged_cylinder([0.0, 0.0, -5.0], 5.0, 10.0, 0)), ("sphere", sphere)] { + let h = boolean(&a, &slab(), BooleanOp::Subtract); + assert!(of_first(&h, Change::Split) >= 2, "{name}: {h:?}"); + } + } + + /// A chain of three overlapping boxes in one `union_all`: the result names all three. + #[test] + fn a_union_of_three_names_each() { + let (a, b, c) = (tagged_box([0.0; 3], [10.0; 3], 0), tagged_box([5.0; 3], [10.0; 3], 1), tagged_box([10.0; 3], [10.0; 3], 2)); + let h = Brep::union_all(&[&a, &b, &c]).unwrap().history().unwrap(); + assert_eq!(h.faces.iter().flat_map(|f| f.origins.iter().map(|o| o.0)).max(), Some(2), "{h:?}"); + } + + /// A row of four boxes sharing their top and bottom planes, united in pairs and the pairs + /// united: each top merged with its neighbour's stands for two faces, and the merge of those + /// stands for all four. + #[test] + fn a_merge_of_several_origins_on_each_side_stands_for_all() { + let boxes: Vec = (0..4u32).map(|i| tagged_box([f64::from(i) * 10.0, 0.0, 0.0], [10.0; 3], i as usize)).collect(); + let u = |x: &Brep, y: &Brep| x.boolean(y, BooleanOp::Union).unwrap().unwrap(); + let h = u(&u(&boxes[0], &boxes[1]), &u(&boxes[2], &boxes[3])).history().unwrap(); + assert!(h.faces.iter().any(|f| f.origins.len() == 4), "{h:?}"); + } +} diff --git a/crates/smlib/src/core/convert.rs b/crates/smlib/src/core/convert.rs index 56988f618b..7353a713a5 100644 --- a/crates/smlib/src/core/convert.rs +++ b/crates/smlib/src/core/convert.rs @@ -2492,11 +2492,6 @@ fn extent2d(u0: f64, v0: f64, u1: f64, v1: f64) -> Result { Ok(Extent2d::from_bounds(u0, v0, u1, v1)) } -/// Whether `Validate` refuses these arrays. -#[cfg(all(test, feature = "native"))] -pub(super) fn validate_refuses(a: &Arrays) -> bool { - validate(&In(a)).is_none() -} /// A body from the flat arrays the C API imports (`BrepFromFlat`): `Validate`, /// `FlatToBrepData`, then `MakeTopologyFromData` with the B-rep's tolerance as it is ([`own_tolerances`]) and no UV diff --git a/crates/smlib/src/core/convert_tests.rs b/crates/smlib/src/core/convert_tests.rs index c2754d75ca..445a6c1c00 100644 --- a/crates/smlib/src/core/convert_tests.rs +++ b/crates/smlib/src/core/convert_tests.rs @@ -1,6 +1,5 @@ //! solidcore's primitives through the flat arrays: each exports to a valid record of the right -//! volume, the record comes back to an equal body, and (with SMLib built) the arrays are the C++ -//! kernel's own. A body with a sealed void crosses with each region's enclosing shell first, +//! volume, and the record comes back to an equal body. A body with a sealed void crosses with each region's enclosing shell first, //! whatever order its arrays list the shells in (the capi's `PutEnclosingShellsFirst`). use std::f64::consts::PI; @@ -91,46 +90,7 @@ fn round_trips(name: &str) { assert!(rel(v, c.volume) < 1e-9, "{name}: {v} vs {}", c.volume); } -/// The C++ kernel's arrays of the same primitive: every id, ints exact, reals within 1e-12. -#[cfg(feature = "native")] -fn matches_cpp(name: &str) { - use crate::flat::{TOLERANCE, VERTEX_TOLERANCE, WIRE_EDGE_NURBS_FORM}; - use crate::{Brep, ffi}; - // built by the C API whichever kernel is in force - crate::init().unwrap(); - let cpp = match name { - "box" => ffi::make_box(&[1.0, -2.0, 3.0], &[10.0, 20.0, 30.0]), - "cylinder" => ffi::make_cylinder(&[1.0, 2.0, -3.0], 5.0, 12.0), - "sphere" => ffi::make_sphere(&[1.0, 2.0, 3.0], 7.0), - "cone" => ffi::make_cone(&[0.0, 1.0, 2.0], 6.0, 0.0, 9.0), - "frustum" => ffi::make_cone(&[0.0, 1.0, 2.0], 6.0, 2.0, 9.0), - "torus" => ffi::make_torus(&[1.0, -1.0, 0.5], 10.0, 3.0), - _ => panic!("{name}"), - } - .map(Brep::made) - .unwrap(); - let flat = cpp.export().unwrap(); - let ours = to_arrays(&case(name).body).unwrap(); - for id in 1..=WIRE_EDGE_NURBS_FORM { - assert_eq!(ours.ints.get(&id).unwrap(), &flat.get_i64(id).unwrap(), "{name}: int array {id}"); - } - for id in TOLERANCE..=VERTEX_TOLERANCE { - let (a, b) = (ours.reals.get(&id).unwrap(), flat.get_f64(id).unwrap()); - assert_eq!(a.len(), b.len(), "{name}: real array {id} length"); - for (i, (x, y)) in a.iter().zip(&b).enumerate() { - assert!((x - y).abs() <= 1e-12 * x.abs().max(1.0), "{name}: real array {id}[{i}]: {x} vs {y}"); - } - } -} -/// The record of solidcore's arrays, built in the C++ kernel, measures the analytic volume. -#[cfg(feature = "native")] -fn record_measures_in_cpp(name: &str) { - let c = case(name); - let rec = record(&to_arrays(&c.body).unwrap()); - let v = crate::Brep::from_record(&rec, true).unwrap().volume(1e-8).unwrap(); - assert!(rel(v, c.volume) < 1e-9, "{name}: {v} vs {}", c.volume); -} macro_rules! primitive { ($m:ident, $name:literal $(, volume: $vol:meta)? $(, round_trip: $rt:meta)?) => { @@ -149,18 +109,6 @@ macro_rules! primitive { fn round_trips_through_the_record() { super::round_trips($name); } - #[cfg(feature = "native")] - #[test] - fn its_record_measures_in_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - super::record_measures_in_cpp($name); - } - #[cfg(feature = "native")] - #[test] - fn arrays_match_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - super::matches_cpp($name); - } } }; } @@ -218,100 +166,15 @@ fn a_parabola_gives_its_nurb_or_says_it_cannot() { assert!(matches!(&none, Err(e) if e.to_string().contains("MakeNurb")), "{none:?}"); } -/// `Validate` against the C++ one: each array of each body grown, emptied and given hostile values. -/// What ours refuses the C++ kernel does not build, and what the C++ kernel refuses with -/// `SM_ERR_INVALID_INPUT`, its `Validate`'s answer (its build can answer it too), ours does not build. -#[cfg(feature = "native")] -#[test] -fn validate_refuses_what_the_cpp_kernel_refuses() { - let _cpp = crate::route::on_cpp(); - /// `SM_ERR_INVALID_INPUT`. - const INVALID_INPUT: i32 = 1007; - use crate::flat::Flat; - use crate::{Brep, Error, ffi}; - crate::init().unwrap(); - let plate = ffi::make_box(&[0.0; 3], &[40.0, 30.0, 5.0]).unwrap(); - let rod = ffi::make_cylinder(&[20.0, 15.0, -1.0], 6.0, 7.0).unwrap(); - let cone = ffi::make_cone(&[0.0, 1.0, 2.0], 6.0, 2.0, 9.0).unwrap(); - let bodies = [ - ("holed plate", Brep::made(ffi::ops::boolean(&plate, &rod, ffi::ops::BOOLEAN_SUBTRACT).unwrap())), - ("frustum", Brep::made(cone)), - ("sphere", Brep::made(ffi::make_sphere(&[1.0, 2.0, 3.0], 7.0).unwrap())), - ("torus", Brep::made(ffi::make_torus(&[1.0, -1.0, 0.5], 10.0, 3.0).unwrap())), - ]; - let (mut compared, mut refusals) = (0usize, 0usize); - for (name, body) in &bodies { - let good = body.cpp_arrays().unwrap(); - let mut hostile: Vec<(String, Arrays)> = Vec::new(); - for (id, v) in &good.ints { - let mut grown = good.clone(); - grown.ints.get_mut(id).unwrap().push(1); - hostile.push((format!("int {id} grown"), grown)); - let mut none = good.clone(); - none.ints.get_mut(id).unwrap().clear(); - hostile.push((format!("int {id} emptied"), none)); - for at in [0, v.len().saturating_sub(1)] { - for x in [-1, 0, 1, 2, 7, 1_000_000, i64::MAX, i64::MIN] { - if v.get(at).is_some_and(|y| *y != x) { - let mut a = good.clone(); - a.ints.get_mut(id).unwrap()[at] = x; - hostile.push((format!("int {id}[{at}] = {x}"), a)); - } - } - } - } - for (id, v) in &good.reals { - let mut grown = good.clone(); - grown.reals.get_mut(id).unwrap().push(1.0); - hostile.push((format!("real {id} grown"), grown)); - let mut none = good.clone(); - none.reals.get_mut(id).unwrap().clear(); - hostile.push((format!("real {id} emptied"), none)); - for at in [0, v.len().saturating_sub(1)] { - for x in [0.0, -1.0, 2.0, 1e10, -1e-13, f64::NAN, f64::INFINITY] { - if !v.is_empty() { - let mut a = good.clone(); - a.reals.get_mut(id).unwrap()[at] = x; - hostile.push((format!("real {id}[{at}] = {x}"), a)); - } - } - } - } - for (what, a) in hostile { - let cpp = Flat::from_arrays(&a).unwrap().import().map(|_| ()); - let cpp_refused = cpp == Err(Error::Status(INVALID_INPUT)); - let ours = super::convert::validate_refuses(&a); - compared += 1; - refusals += usize::from(ours); - assert!(!(ours && cpp.is_ok()), "{name}, {what}: refused here, built in C++"); - assert!(!(cpp_refused && !ours && from_arrays(&a).is_ok()), "{name}, {what}: refused in C++, built here"); - } - } - assert!(compared > 500 && refusals > 200, "{compared} compared, {refusals} refused"); -} -/// The C++ kernel's own box arrays (what `Brep::core` converts) build in solidcore. -#[cfg(feature = "native")] -#[test] -fn a_cpp_box_comes_into_solidcore() { - let _cpp = crate::route::on_cpp(); - crate::init().unwrap(); - let cpp = crate::ffi::make_box(&[1.0, -2.0, 3.0], &[10.0, 20.0, 30.0]).map(crate::Brep::made).unwrap(); - let body = from_arrays(&cpp.cpp_arrays().unwrap()).unwrap(); - let v = volume(&body, 1e-8).unwrap(); - assert!(rel(v, 6000.0) < 1e-9, "{v}"); - record(&to_arrays(&body).unwrap()); -} /// Each edge's and vertex's tolerance crosses the kernels as it is. One box edge given eight times -/// the box's tolerance, in arrays whose TOLERANCE is the largest (as the C++ kernel writes them), +/// the box's tolerance, in arrays whose TOLERANCE is the largest (as SMLib's export wrote them), /// keeps it round after round, and the B-rep and every other edge and vertex keep the box's. Every /// crossing used to give every entity the largest, so a chain of blends on a mixed route grew the /// whole body's tolerance fourfold per blend (fillets/2341). #[test] fn tolerances_cross_the_arrays_per_entity() { - #[cfg(feature = "native")] - let _cpp = crate::route::on_cpp(); use crate::flat::{EDGE_TOLERANCE, TOLERANCE, VERTEX_TOLERANCE}; let mut a = to_arrays(&case("box").body).unwrap(); let base = a.reals[&TOLERANCE][0]; @@ -326,13 +189,6 @@ fn tolerances_cross_the_arrays_per_entity() { let edges = &a.reals[&EDGE_TOLERANCE]; assert_eq!(edges[0], 8.0 * base, "round {round}"); assert!(edges[1..].iter().chain(&a.reals[&VERTEX_TOLERANCE]).all(|&t| t == base), "round {round}: {a:?}"); - // and through the C++ kernel, which keeps them too - #[cfg(feature = "native")] - { - let cpp = crate::flat::Flat::from_arrays(&a).unwrap().import().unwrap().cpp_arrays().unwrap(); - assert_eq!((&cpp.reals[&EDGE_TOLERANCE], &cpp.reals[&VERTEX_TOLERANCE]), (edges, &a.reals[&VERTEX_TOLERANCE]), "round {round}"); - a = cpp; - } } } @@ -431,322 +287,4 @@ fn shells_listed_inner_first_import_outer_first() { assert!(rel(v, SEALED_VOID_VOLUME) < 1e-9, "inner first: {v}"); } -/// The C++ kernel's sealed void: solidcore imports its arrays with the outer shell first, in -/// either shell order, at the C++ kernel's volume, and the C++ kernel reads solidcore's arrays -/// (either order) back as a body that passes its checks at that volume. -#[cfg(feature = "native")] -#[test] -fn a_sealed_void_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - use crate::{Brep, ffi}; - crate::init().unwrap(); - let make = |o: f64, s: f64| ffi::make_box(&[o; 3], &[s; 3]).map(Brep::made).unwrap(); - let (outer, inner) = (make(-10.0, 20.0), make(-5.0, 10.0)); - let cpp = Brep::made(ffi::ops::boolean(outer.cpp().unwrap(), inner.cpp().unwrap(), ffi::ops::BOOLEAN_SUBTRACT).unwrap()); - let theirs = ffi::volume(cpp.cpp().unwrap(), 1e-9).unwrap(); - assert!(rel(theirs, SEALED_VOID_VOLUME) < 1e-9, "C++ {theirs}"); - let arrays = cpp.cpp_arrays().unwrap(); - outer_first(&arrays, "C++ export"); - for (at, a) in [("C++ arrays", arrays.clone()), ("C++ arrays, inner first", shells_reversed(&arrays))] { - let body = from_arrays(&a).unwrap(); - outer_first(&to_arrays(&body).unwrap(), at); - let v = volume(&body, 1e-9).unwrap(); - assert!(rel(v, theirs) < 1e-9, "{at}: solidcore {v} vs C++ {theirs}"); - } - let ours = to_arrays(&sealed_void()).unwrap(); - for (at, a) in [("solidcore arrays", ours.clone()), ("solidcore arrays, inner first", shells_reversed(&ours))] { - let h = crate::flat::Flat::from_arrays(&a).unwrap().import().unwrap(); - let v = ffi::volume(h.cpp().unwrap(), 1e-9).unwrap(); - assert!(rel(v, theirs) < 1e-9, "{at}: C++ {v} vs {theirs}"); - let mut buf = vec![0u8; 1 << 16]; - let (len, failures) = ffi::check::check(h.cpp().unwrap(), 2, &mut buf).unwrap(); - assert_eq!(failures, 0, "{at}: {}", String::from_utf8_lossy(&buf[..len])); - outer_first(&h.cpp_arrays().unwrap(), &format!("{at}, C++ export")); - } -} -/// C++ bodies through solidcore and back. Three crossings of the same C++ arrays are held equal, -/// ints exactly and reals to 1e-12 (the record too, where the record can say it): -/// - the C++ kernel's own: its import of them, exported again; -/// - solidcore's: `from_arrays`, then `to_arrays`; -/// - both: solidcore's arrays imported by the C++ kernel and exported again. -/// -/// The first is the reference, not the arrays that went in: a crossing itself moves a few reals on -/// the C++ kernel too (UV trims made at export, about 1e-7 on a shelled box), so those are held to -/// the arrays that went in only at [`FIRST_CROSSING`]. Tolerances are excepted: they cross on -/// purpose as each entity's own (`own_tolerances`, TOLERANCE as twice the B-rep's) where the C++ -/// export writes the largest over them all, so that a body does not grow its tolerance at every -/// crossing (`tolerances_cross_the_arrays_per_entity`); on a body an operation made the numbers -/// cannot agree. So is EXTENT, the bounding box padded by those tolerances. -#[cfg(feature = "native")] -mod cpp_round_trip { - use serde_json::Value; - use solidcraft_geom::brep as b; - - use super::{close, from_arrays, native, rel, to_arrays, volume}; - use crate::flat::{self, EDGE_TOLERANCE, EXTENT, Flat, TOLERANCE, VERTEX_TOLERANCE}; - use crate::import::Arrays; - use crate::sweeps::{Curve, DraftCorner, SweepFrame}; - use crate::{Brep, ffi}; - - const PICK_TOL: f64 = 1e-6; - - /// How far any crossing, the C++ kernel's own included, may move a real of the arrays that - /// went in. - const FIRST_CROSSING: f64 = 1e-6; - - fn cpp_box(o: [f64; 3], s: [f64; 3]) -> Brep { - crate::init().unwrap(); - Brep::made(ffi::make_box(&o, &s).unwrap()) - } - - fn cpp_cylinder(o: [f64; 3], r: f64, h: f64) -> Brep { - crate::init().unwrap(); - Brep::made(ffi::make_cylinder(&o, r, h).unwrap()) - } - - fn cut(a: &Brep, b: &Brep) -> Brep { - Brep::made(ffi::ops::boolean(a.cpp().unwrap(), b.cpp().unwrap(), ffi::ops::BOOLEAN_SUBTRACT).unwrap()) - } - - /// `f` with the sweeps on the C++ kernel, whatever the choice. - fn on_cpp(f: impl FnOnce() -> crate::Result) -> Brep { - crate::init().unwrap(); - crate::sweeps::ON_CPP.with(|c| c.set(true)); - let r = f(); - crate::sweeps::ON_CPP.with(|c| c.set(false)); - r.unwrap() - } - - /// The record as JSON, every `tolerance` (the B-rep's, an edge's, a vertex's) gone. - fn without_tolerances(rec: &b::Brep) -> Value { - fn strip(v: &mut Value) { - match v { - Value::Object(m) => { - m.remove("tolerance"); - m.values_mut().for_each(strip); - } - Value::Array(a) => a.iter_mut().for_each(strip), - _ => {} - } - } - let mut v = serde_json::to_value(rec).unwrap(); - strip(&mut v); - v - } - - /// `want` and `got` agree: ints exact, reals within `tol`, tolerances and EXTENT excepted. - fn same_arrays(at: &str, want: &Arrays, got: &Arrays, tol: f64) { - assert_eq!(want.ints.keys().collect::>(), got.ints.keys().collect::>(), "{at}: int arrays"); - for (id, v) in &want.ints { - assert_eq!(v, &got.ints[id], "{at}: int array {id}"); - } - assert_eq!(want.reals.keys().collect::>(), got.reals.keys().collect::>(), "{at}: real arrays"); - for (id, v) in &want.reals { - if [TOLERANCE, EXTENT, EDGE_TOLERANCE, VERTEX_TOLERANCE].contains(id) { - assert_eq!(v.len(), got.reals[id].len(), "{at}: real array {id} length"); - continue; - } - let (x, y) = (serde_json::to_value(v).unwrap(), serde_json::to_value(&got.reals[id]).unwrap()); - close(&x, &y, tol, &format!("{at}: real array {id}")); - } - } - - /// [`same_arrays`], and their records the same, valid and with tolerances excepted. - fn same_records(at: &str, want: &Arrays, got: &Arrays, tol: f64) -> b::Brep { - same_arrays(at, want, got, tol); - let ((w, _), (g, _)) = (native(want).unwrap(), native(got).unwrap()); - w.validate().unwrap(); - g.validate().unwrap(); - close(&without_tolerances(&w), &without_tolerances(&g), tol, at); - w - } - - /// The three crossings of `theirs`: the C++ kernel's own, solidcore's, and solidcore's arrays - /// back through the C++ kernel. - fn crossings(theirs: &Arrays) -> [Arrays; 3] { - let own = Flat::from_arrays(theirs).unwrap().import().unwrap().cpp_arrays().unwrap(); - let ours = to_arrays(&from_arrays(theirs).unwrap()).unwrap(); - let back = Flat::from_arrays(&ours).unwrap().import().unwrap().cpp_arrays().unwrap(); - [own, ours, back] - } - - /// Arrays the record cannot say (wire and point shells): the crossings' arrays only. - fn crosses_as_arrays(name: &str, theirs: &Arrays) { - let [own, ours, back] = crossings(theirs); - same_arrays(&format!("{name}, solidcore's arrays"), &own, &ours, 1e-12); - same_arrays(&format!("{name}, back through C++"), &own, &back, 1e-12); - } - - /// `cpp` across: returns its record, for the case to say what it holds. - fn crosses(name: &str, cpp: &Brep) -> b::Brep { - let theirs = cpp.cpp_arrays().unwrap(); - let [own, ours, back] = crossings(&theirs); - same_records(&format!("{name}, solidcore's arrays"), &own, &ours, 1e-12); - same_records(&format!("{name}, back through C++"), &own, &back, 1e-12); - same_records(&format!("{name}, the arrays that went in"), &theirs, &ours, FIRST_CROSSING) - } - - /// [`crosses`], and solidcore measures the C++ kernel's volume. - fn crosses_with_volume(name: &str, cpp: &Brep) -> b::Brep { - let rec = crosses(name, cpp); - let body = from_arrays(&cpp.cpp_arrays().unwrap()).unwrap(); - let (v, want) = (volume(&body, 1e-8).unwrap(), ffi::volume(cpp.cpp().unwrap(), 1e-8).unwrap()); - assert!(rel(v, want) < 1e-7, "{name}: solidcore {v} vs C++ {want}"); - rec - } - - fn has_face(rec: &b::Brep, f: impl Fn(&b::Face) -> bool) -> bool { - rec.faces.iter().any(f) - } - - /// A 20 mm box with three edges at one corner rounded: rolling-ball faces and a corner patch, - /// on an operation's tolerances. - #[test] - fn a_filleted_box() { - let _cpp = crate::route::on_cpp(); - let a = cpp_box([0.0; 3], [20.0; 3]); - let edges = [[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]]; - let f = Brep::made(ffi::ops::pick_op(ffi::ops::Pick::Fillet, a.cpp().unwrap(), &edges, PICK_TOL, 2.0).unwrap()); - let rec = crosses_with_volume("filleted box", &f); - assert!(rec.faces.len() > 6, "{}", rec.faces.len()); - } - - /// A plate with a round hole: inner loops, and a cylinder face looking at its axis. - #[test] - fn a_holed_plate() { - let _cpp = crate::route::on_cpp(); - let p = cut(&cpp_box([0.0; 3], [40.0, 30.0, 5.0]), &cpp_cylinder([20.0, 15.0, -1.0], 6.0, 7.0)); - let rec = crosses_with_volume("holed plate", &p); - assert!(has_face(&rec, |f| f.loops.len() > 1), "no inner loop"); - } - - /// A square swept along a cubic B-spline path: B-spline walls that are nothing analytic. - #[test] - fn a_swept_nurbs_face() { - let _cpp = crate::route::on_cpp(); - let c = [[-2.0, -2.0, 0.0], [2.0, -2.0, 0.0], [2.0, 2.0, 0.0], [-2.0, 2.0, 0.0]]; - let square: Vec = (0..4).map(|i| Curve::Line { start: c[i], end: c[(i + 1) % 4] }).collect(); - let points = [[0.0, 0.0, 0.0, 1.0], [0.0, 10.0, 20.0, 1.0], [10.0, -10.0, 40.0, 1.0], [10.0, 0.0, 60.0, 1.0]]; - let path = [Curve::Nurbs { degree: 3, points: points.to_vec(), knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0] }]; - let s = on_cpp(|| Brep::sweep(&square, &path, SweepFrame::Follow, true)); - let rec = crosses_with_volume("swept square", &s); - assert!(has_face(&rec, |f| matches!(f.surface, b::Surface::Spline { .. } | b::Surface::RationalSpline { .. })), "no NURBS face"); - } - - /// A cylinder sliced by a plane leaning 45 degrees: the cut's edge an ellipse. - #[test] - fn an_ellipse_edge() { - let _cpp = crate::route::on_cpp(); - let cyl = cpp_cylinder([0.0, 0.0, -30.0], 5.0, 60.0); - let (s, c) = std::f64::consts::FRAC_PI_4.sin_cos(); - // About y through the origin; the box's underside is the plane z = 0 turned. - let turn = [[c, 0.0, s, 0.0], [0.0, 1.0, 0.0, 0.0], [-s, 0.0, c, 0.0], [0.0, 0.0, 0.0, 1.0]]; - let tool = Brep::made(ffi::transform(cpp_box([-50.0, -50.0, 0.0], [100.0; 3]).cpp().unwrap(), &turn).unwrap()); - let rec = crosses_with_volume("leaning cut", &cut(&cyl, &tool)); - assert!(rec.edges.iter().any(|e| matches!(e.curve, b::Curve::Ellipse { .. })), "no ellipse edge"); - } - - /// A 20 mm box with a 10 mm void sealed inside: two shells in the solid's region. - #[test] - fn a_sealed_void() { - let _cpp = crate::route::on_cpp(); - let v = cut(&cpp_box([-10.0; 3], [20.0; 3]), &cpp_box([-5.0; 3], [10.0; 3])); - let rec = crosses_with_volume("sealed void", &v); - assert!(rec.regions.iter().any(|r| r.shells.len() == 2), "{:?}", rec.regions); - assert!(rec.regions.iter().any(|r| r.void), "{:?}", rec.regions); - } - - /// One planar face, and an open extrusion: a sheet's shells, no solid. - #[test] - fn sheets() { - let _cpp = crate::route::on_cpp(); - let c = [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 6.0, 0.0], [0.0, 6.0, 0.0]]; - let rect: Vec = (0..4).map(|i| Curve::Line { start: c[i], end: c[(i + 1) % 4] }).collect(); - let rec = crosses("planar sheet", &on_cpp(|| Brep::planar_faces(&rect))); - assert_eq!(rec.faces.len(), 1); - let open = on_cpp(|| Brep::extrude(&rect, [0.0, 0.0, 1.0], 5.0, 0.0, DraftCorner::default(), false)); - let rec = crosses("open extrusion", &open); - assert_eq!(rec.faces.len(), 4); - } - - /// A C++ box's arrays with one wire edge of each kind (a line, a whole circle, a half ellipse, - /// a cubic B-spline) and one point, each a shell of its own in the infinite region. The C API - /// gives no body without a face (`take`), so these lie beside a box. - fn box_with_wires_and_a_point() -> Arrays { - let tau = std::f64::consts::TAU; - let mut a = cpp_box([20.0; 3], [5.0; 3]).cpp_arrays().unwrap(); - let first_new = a.ints[&flat::VERTEX_POINT_TYPE].len() as i64; - a.ints.get_mut(&flat::REGION_SHELL_COUNT).unwrap()[0] += 5; - for (id, v) in - [(flat::SHELL_FACEUSE_COUNT, [0, 0, 0, 0, 0]), (flat::SHELL_WIRE_EDGE_COUNT, [1, 1, 1, 1, 0]), (flat::SHELL_POINT_TYPE, [0, 0, 0, 0, 1])] - { - a.ints.get_mut(&id).unwrap().splice(1..1, v); - } - a.ints.insert( - flat::WIRE_EDGE_CURVE_TYPE, - [flat::CURVE_LINE, flat::CURVE_CIRCLE, flat::CURVE_ELLIPSE, flat::CURVE_NURBS].map(i64::from).to_vec(), - ); - a.ints.insert(flat::WIRE_EDGE_VERTICES, [0, 1, 2, 2, 3, 4, 5, 6].map(|v| first_new + v).to_vec()); - a.ints.insert(flat::WIRE_EDGE_NURBS_VERTEX_COUNT, vec![4]); - a.ints.insert(flat::WIRE_EDGE_NURBS_ORDER, vec![4]); - let points = [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [4.0, 0.0, 5.0], [3.0, 0.0, -5.0], [-3.0, 0.0, -5.0], [0.0, 9.0, 0.0], [9.0, 9.0, 3.0]]; - a.ints.get_mut(&flat::VERTEX_POINT_TYPE).unwrap().extend([1; 7]); - a.reals.get_mut(&flat::VERTEX_POSITION).unwrap().extend(points.concat()); - let base = a.reals[&TOLERANCE][0]; - a.reals.get_mut(&VERTEX_TOLERANCE).unwrap().extend([base; 7]); - let reals: [(i32, Vec); 17] = [ - (flat::SHELL_POINT_POSITION, vec![1.0, 2.0, 3.0]), - (flat::WIRE_EDGE_RANGE, vec![0.0, 10.0, 0.0, tau, 0.0, std::f64::consts::PI, 0.0, 1.0]), - (flat::WIRE_EDGE_LINE_ORIGIN, vec![0.0; 3]), - (flat::WIRE_EDGE_LINE_DIRECTION, vec![1.0, 0.0, 0.0]), - (flat::WIRE_EDGE_CIRCLE_CENTER, vec![0.0, 0.0, 5.0]), - (flat::WIRE_EDGE_CIRCLE_AXIS, vec![0.0, 0.0, 1.0]), - (flat::WIRE_EDGE_CIRCLE_REF_DIRECTION, vec![1.0, 0.0, 0.0]), - (flat::WIRE_EDGE_CIRCLE_RADIUS, vec![4.0]), - (flat::WIRE_EDGE_ELLIPSE_CENTER, vec![0.0, 0.0, -5.0]), - (flat::WIRE_EDGE_ELLIPSE_AXIS, vec![0.0, 0.0, 1.0]), - (flat::WIRE_EDGE_ELLIPSE_REF_DIRECTION, vec![1.0, 0.0, 0.0]), - (flat::WIRE_EDGE_ELLIPSE_X_RADIUS, vec![3.0]), - (flat::WIRE_EDGE_ELLIPSE_Y_RADIUS, vec![2.0]), - (flat::WIRE_EDGE_NURBS_CVS, [points[5], [3.0, 12.0, 0.0], [6.0, 6.0, 2.0], points[6]].concat()), - (flat::WIRE_EDGE_NURBS_KNOTS, vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]), - (flat::WIRE_EDGE_NURBS_WEIGHTS, Vec::new()), - (EXTENT, vec![-3.0, 0.0, -5.0, 25.0, 25.0, 25.0]), - ]; - a.reals.extend(reals); - a - } - - /// Wire edges no face uses and a point shell. The record says no such shell from SMLib, so - /// arrays only. - #[test] - fn wire_edges_and_a_point_shell() { - let _cpp = crate::route::on_cpp(); - let theirs = Flat::from_arrays(&box_with_wires_and_a_point()).unwrap().import().unwrap().cpp_arrays().unwrap(); - assert_eq!(theirs.ints[&flat::WIRE_EDGE_CURVE_TYPE].len(), 4); - assert_eq!(theirs.reals[&flat::SHELL_POINT_POSITION], [1.0, 2.0, 3.0]); - crosses_as_arrays("wire edges and a point", &theirs); - } - - /// Two boxes apart, united: two lumps. - #[test] - fn a_multi_lump_body() { - let _cpp = crate::route::on_cpp(); - let (a, b) = (cpp_box([0.0; 3], [10.0; 3]), cpp_box([30.0, 0.0, 0.0], [5.0; 3])); - let u = Brep::made(ffi::ops::boolean(a.cpp().unwrap(), b.cpp().unwrap(), ffi::ops::BOOLEAN_UNION).unwrap()); - let rec = crosses_with_volume("two lumps", &u); - assert_eq!(ffi::ops::solid_count(u.cpp().unwrap()).unwrap(), 2); - assert_eq!(rec.faces.len(), 12); - } - - /// A rounded box shelled: faces on the offsets of its rounds, on the operation's tolerances. - #[test] - fn a_shelled_rounded_box() { - let _cpp = crate::route::on_cpp(); - let a = cpp_box([0.0; 3], [20.0; 3]); - let r = ffi::ops::pick_op(ffi::ops::Pick::Fillet, a.cpp().unwrap(), &[[10.0, 0.0, 0.0]], PICK_TOL, 2.0).unwrap(); - let s = Brep::made(ffi::ops::pick_op(ffi::ops::Pick::Shell, &r, &[[10.0, 10.0, 20.0]], PICK_TOL, -1.0).unwrap()); - crosses_with_volume("shelled rounded box", &s); - } -} diff --git a/crates/smlib/src/core/measure_tests.rs b/crates/smlib/src/core/measure_tests.rs index 418c02fccd..1e5e388796 100644 --- a/crates/smlib/src/core/measure_tests.rs +++ b/crates/smlib/src/core/measure_tests.rs @@ -1,6 +1,5 @@ //! The Measure and Check families on solidcore's primitives: mass properties, area and bounds -//! against their closed forms ([`crate::measure::closed`]), one solid each, edges on their faces; -//! and (with SMLib built) the same answers as the C++ kernel for the same primitive. +//! against their closed forms ([`crate::measure::closed`]), one solid each, edges on their faces. use super::{Body, area, bounds, check, edge_face_distance, is_manifold_solid, make_box, make_cone, make_cylinder, make_sphere, make_torus}; use super::{counts, mass_properties, solid_count}; @@ -109,102 +108,14 @@ fn check_refuses_only_an_unknown_naming() { name(NamingScheme::Caller).unwrap(); } -/// The C++ kernel's body of the same primitive, built by its C API whichever kernel is in -/// force: a body imported from solidcore's arrays is not the C++ kernel's own (its -/// `edge_face_distance` measures to each face's surface over its domain, which the import sets). -#[cfg(feature = "native")] -fn cpp(name: &str) -> crate::Brep { - use crate::{Brep, ffi}; - crate::init().unwrap(); - match name { - "box" => ffi::make_box(&[1.0, -2.0, 3.0], &[10.0, 20.0, 30.0]), - "cylinder" => ffi::make_cylinder(&[1.0, 2.0, -3.0], 5.0, 12.0), - "sphere" => ffi::make_sphere(&[1.0, 2.0, 3.0], 7.0), - "cone" => ffi::make_cone(&[0.0, 1.0, 2.0], 6.0, 0.0, 9.0), - "frustum" => ffi::make_cone(&[0.0, 1.0, 2.0], 6.0, 2.0, 9.0), - "torus" => ffi::make_torus(&[1.0, -1.0, 0.5], 10.0, 3.0), - _ => panic!("{name}"), - } - .map(Brep::made) - .unwrap() -} -/// `x` within `TOL` of the C++ kernel's `y`, relative to `scale`. -#[cfg(feature = "native")] -fn same(x: f64, y: f64, scale: f64, what: &str) { - assert!((x - y).abs() <= TOL * scale, "{what}: solidcore {x} vs C++ {y}"); -} -#[cfg(feature = "native")] -fn mass_properties_parity(name: &str) { - let (b, c) = case(name); - let ours = mass_properties(&b, MEASURE_ACCURACY).unwrap(); - let theirs = cpp(name).mass_properties(MEASURE_ACCURACY).unwrap(); - same(ours.area, theirs.area, theirs.area, &format!("{name} area")); - same(ours.volume, theirs.volume, theirs.volume, &format!("{name} volume")); - let imax = theirs.moments.iter().fold(0.0f64, |a, &b| a.max(b)); - for i in 0..3 { - same(ours.centroid[i], theirs.centroid[i], size(&c), &format!("{name} centroid[{i}]")); - same(ours.moments[i], theirs.moments[i], theirs.moments[i], &format!("{name} moments[{i}]")); - same(ours.products[i], theirs.products[i], imax, &format!("{name} products[{i}]")); - } -} -#[cfg(feature = "native")] -fn area_parity(name: &str) { - let (b, _) = case(name); - let theirs = cpp(name).area(MEASURE_ACCURACY).unwrap(); - same(area(&b, MEASURE_ACCURACY).unwrap(), theirs, theirs, &format!("{name} area")); -} -#[cfg(feature = "native")] -fn bounds_parity(name: &str) { - let (b, c) = case(name); - let (lo, hi) = bounds(&b).unwrap(); - let (tlo, thi) = cpp(name).bounds().unwrap(); - for i in 0..3 { - same(lo[i], tlo[i], size(&c), &format!("{name} low[{i}]")); - same(hi[i], thi[i], size(&c), &format!("{name} high[{i}]")); - } -} -#[cfg(feature = "native")] -fn solid_count_parity(name: &str) { - let (b, _) = case(name); - assert_eq!(solid_count(&b).unwrap(), cpp(name).solid_count().unwrap(), "{name}"); -} -#[cfg(feature = "native")] -fn manifold_parity(name: &str) { - let (b, _) = case(name); - assert_eq!(is_manifold_solid(&b).unwrap(), cpp(name).is_manifold_solid().unwrap(), "{name}"); -} -/// The whole edge-by-face distance matrix, the C++ kernel's own (both bodies list their edges -/// and faces in the same order: `convert_tests` shows their arrays are equal). -#[cfg(feature = "native")] -fn distance_parity(name: &str) { - let (b, c) = case(name); - let theirs = cpp(name); - let [faces, edges, _] = counts(&b).unwrap(); - for e in 0..edges { - for f in 0..faces { - let (x, y) = (edge_face_distance(&b, e, f), theirs.edge_face_distance(e, f)); - match (x, y) { - (Ok(x), Ok(y)) => same(x, y, size(&c), &format!("{name} edge {e} face {f}")), - (x, y) => assert_eq!(x.is_ok(), y.is_ok(), "{name} edge {e} face {f}: {x:?} vs {y:?}"), - } - } - } -} -#[cfg(feature = "native")] -fn check_parity(name: &str) { - let (b, _) = case(name); - for level in [CheckLevel::Basic, CheckLevel::Geometry, CheckLevel::All] { - assert_eq!(check(&b, level).unwrap(), cpp(name).check(level).unwrap(), "{name}, {level:?}"); - } -} macro_rules! primitive { ($m:ident, $name:literal $(, mass: $mass:meta)? $(, area: $area:meta)?) => { @@ -240,50 +151,6 @@ macro_rules! primitive { fn checks_valid() { super::checks_valid($name); } - #[cfg(feature = "native")] - #[test] - $(#[$mass])? - fn mass_properties_match_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - mass_properties_parity($name); - } - #[cfg(feature = "native")] - #[test] - $(#[$area])? - fn area_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - area_parity($name); - } - #[cfg(feature = "native")] - #[test] - fn bounds_match_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - bounds_parity($name); - } - #[cfg(feature = "native")] - #[test] - fn solid_count_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - solid_count_parity($name); - } - #[cfg(feature = "native")] - #[test] - fn manifold_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - manifold_parity($name); - } - #[cfg(feature = "native")] - #[test] - fn distances_match_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - distance_parity($name); - } - #[cfg(feature = "native")] - #[test] - fn check_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - check_parity($name); - } } }; } @@ -295,35 +162,18 @@ primitive!(cone, "cone"); primitive!(frustum, "frustum"); primitive!(torus, "torus"); -/// The head of each report line, `::Test #`, or the whole line for the capi's -/// own lines (a failed status without a report, a skipped AssertValid). -#[cfg(feature = "native")] -fn tests_of(text: &str) -> Vec<&str> { - text.lines().map(|l| l.find(" #").and_then(|i| l[i + 2..].find(' ').map(|j| &l[..i + 2 + j])).unwrap_or(l)).collect() -} -/// A valid box passes every test at level 2 (SolidCraft's level), with an empty report, on -/// solidcore and on the C++ kernel. +/// A valid box passes every test at level 2 (SolidCraft's level), with an empty report. #[test] fn a_valid_box_passes_level_2() { - #[cfg(feature = "native")] - let _cpp = crate::route::on_cpp(); let (b, _) = case("box"); let r = check(&b, CheckLevel::All).unwrap(); assert_eq!((r.failures, r.text.as_str()), (0, ""), "{r}"); - #[cfg(feature = "native")] - { - let r = cpp("box").check(CheckLevel::All).unwrap(); - assert_eq!((r.failures, r.text.as_str()), (0, ""), "{r}"); - } } -/// A box with one corner moved 1 mm off its edges, through the arrays: the check finds it broken, -/// and the C++ kernel fails the same tests on the same arrays. +/// A box with one corner moved 1 mm off its edges, through the arrays: the check finds it broken. #[test] fn a_vertex_off_its_edges_fails_the_check() { - #[cfg(feature = "native")] - let _cpp = crate::route::on_cpp(); use crate::flat::VERTEX_POSITION; let (b, _) = case("box"); let mut a = super::to_arrays(&b).unwrap(); @@ -332,13 +182,6 @@ fn a_vertex_off_its_edges_fails_the_check() { let r = check(&broken, CheckLevel::All).unwrap(); assert!(!r.is_valid(), "{r}"); assert_eq!(r.failures, r.text.lines().filter(|l| l.contains("::Test #")).count(), "{r}"); - #[cfg(feature = "native")] - { - let cpp = crate::flat::Flat::from_arrays(&a).unwrap().import().unwrap(); - let c = cpp.check(CheckLevel::All).unwrap(); - assert_eq!(tests_of(&r.text), tests_of(&c.text), "solidcore:\n{}\nC++:\n{}", r.text, c.text); - assert_eq!(r, c); - } } #[test] @@ -379,31 +222,18 @@ fn sealed_voids() -> [(&'static str, Body); 2] { } /// The sealed void's mass properties: 8000 − 1000 to 1e-9, centroid at the origin, one solid, -/// in either shell order; with SMLib built, the C++ kernel's own. +/// in either shell order. #[test] fn a_sealed_void_measures_its_volume() { - #[cfg(feature = "native")] - let _cpp = crate::route::on_cpp(); let want = 8000.0 - 1000.0; // Ixx of a cube of side s about its centre: s⁵ / 6. let moment = (20.0f64.powi(5) - 10.0f64.powi(5)) / 6.0; - #[cfg(feature = "native")] - let theirs = { - use crate::{Brep, ffi}; - crate::init().unwrap(); - let make = |o: f64, s: f64| ffi::make_box(&[o; 3], &[s; 3]).map(Brep::made).unwrap(); - let (outer, inner) = (make(-10.0, 20.0), make(-5.0, 10.0)); - let h = ffi::ops::boolean(outer.cpp().unwrap(), inner.cpp().unwrap(), ffi::ops::BOOLEAN_SUBTRACT).unwrap(); - ffi::volume(&h, 1e-9).unwrap() - }; for (at, b) in sealed_voids() { let m = mass_properties(&b, MEASURE_ACCURACY).unwrap(); assert!(rel(m.volume, want) < TOL, "{at}: volume {}", m.volume); assert!(m.centroid.iter().all(|c| c.abs() < TOL * 20.0), "{at}: centroid {:?}", m.centroid); assert!(m.moments.iter().all(|&i| rel(i, moment) < TOL), "{at}: moments {:?} vs {moment}", m.moments); assert_eq!(solid_count(&b).unwrap(), 1, "{at}"); - #[cfg(feature = "native")] - same(m.volume, theirs, theirs, &format!("{at} volume")); } } @@ -436,12 +266,9 @@ fn faces_apart_with_a_shell_each_check_valid() { /// `smlib_brep_classify_point` (fork 34d6b2b), the capi test's cases: a 10 x 20 x 30 box at /// `(1, -2, 3)`: inside, outside, on a face, an edge and a vertex; the cylinder of radius 5 about /// the z axis through `(1, 2)`: off its curved wall by 1e-8 on it (within the zone tolerance), by -/// 1e-4 inside or outside, on its axis inside, on its top's centre on it. With SMLib built, the -/// C++ kernel's own primitives classify the same. +/// 1e-4 inside or outside, on its axis inside, on its top's centre on it. #[test] fn points_classify_as_the_capi() { - #[cfg(feature = "native")] - let _cpp = crate::route::on_cpp(); use super::classify_point; use crate::measure::PointClass::{Inside, OnBoundary, Outside}; @@ -467,17 +294,6 @@ fn points_classify_as_the_capi() { for (name, p, want) in cases { let body = if name == "box" { &bx } else { &cyl }; assert_eq!(classify_point(body, p).unwrap(), want, "{name} {p:?}"); - #[cfg(feature = "native")] - { - use crate::ffi::measure::{POINT_INSIDE, POINT_ON_BOUNDARY, POINT_OUTSIDE, classify_point as cpp_classify}; - let theirs = cpp_classify(cpp(name).cpp().unwrap(), p).unwrap(); - let want_cpp = match want { - Inside => POINT_INSIDE, - Outside => POINT_OUTSIDE, - OnBoundary => POINT_ON_BOUNDARY, - }; - assert_eq!(theirs, want_cpp, "C++ {name} {p:?}"); - } } assert_eq!(classify_point(&bx, [f64::NAN, 0.0, 0.0]).unwrap_err(), crate::Error::Argument); assert_eq!(classify_point(&bx, [0.0, f64::INFINITY, 0.0]).unwrap_err(), crate::Error::Argument); @@ -485,11 +301,9 @@ fn points_classify_as_the_capi() { /// A sheet hung on a solid (fork 9767916's capi test): a 10 mm cube and a 10 x 10 sheet on from its /// top front edge, merged into one body. Its area is every face's, the cube's 600 and the sheet's -/// 100, not the cube's boundary alone; its volume the cube's. The same on the C++ kernel. +/// 100, not the cube's boundary alone; its volume the cube's. #[test] fn a_sheet_hung_on_a_solid_counts_in_the_area() { - #[cfg(feature = "native")] - let _cpp = crate::route::on_cpp(); use solidcore::boolean::api_brep::boolean_merge; use solidcore::geom::vector3d::Vec3; use solidcore::primitive::api_primitives::{create_box, create_plane}; @@ -502,10 +316,4 @@ fn a_sheet_hung_on_a_solid_counts_in_the_area() { .unwrap(); let m = mass_properties(&both, 1e-8).unwrap(); assert!((m.area - 700.0).abs() < 1e-6 && (m.volume - 1000.0).abs() < 1e-6, "{m:?}"); - #[cfg(feature = "native")] - { - let cpp = crate::Brep::made_core(both); - let c = crate::ffi::measure::mass_properties(cpp.cpp().unwrap(), 1e-8).unwrap(); - assert!((c[0] - m.area).abs() < 1e-6 && (c[1] - m.volume).abs() < 1e-6, "C++ {c:?} vs {m:?}"); - } } diff --git a/crates/smlib/src/core/merge_tests.rs b/crates/smlib/src/core/merge_tests.rs index 25f0f8c003..54311e1649 100644 --- a/crates/smlib/src/core/merge_tests.rs +++ b/crates/smlib/src/core/merge_tests.rs @@ -1,7 +1,5 @@ //! Stitch and merge faces on solidcore, against closed forms: counts, volumes (or areas) and -//! whether the result closes, and every result exports to a record that validates. With SMLib -//! built, the same input converted to the C++ kernel gives the same result or the same error -//! (parity only: the expected values are the analytic ones). +//! whether the result closes, and every result exports to a record that validates. //! //! A stitch's input is six (or five) separate planar faces of a 10 mm cube, each its own face of //! one body with its own edges, none shared; a merge's is a cube with four faces cut across, or @@ -201,45 +199,7 @@ fn solidcore_case(name: &str) -> Got { judge(name, "solidcore", got) } -/// The case on the C++ kernel, called directly (whatever the choice), on the input converted -/// from solidcore's. -#[cfg(feature = "native")] -fn cpp_case(name: &str) -> Got { - use crate::ops::pick_error; - use crate::{Brep, error, ffi}; - crate::init().unwrap(); - let (op, _) = case(name); - let r = match op { - Op::Stitch(input, tol) => ffi::sheets::stitch(Brep::made_core(input.core()).cpp().unwrap(), tol).map_err(error), - Op::Merge(input, keep, tol) => { - ffi::ops::merge_faces(Brep::made_core(input.core()).cpp().unwrap(), keep, tol).map(|h| (h, false)).map_err(pick_error) - } - }; - let got = r.map(|(h, closed)| { - let size = if is_sheet(name) { ffi::measure::area(&h, 1e-8) } else { ffi::volume(&h, 1e-8) }.unwrap(); - let n = ffi::counts(&h).unwrap().map(|x| usize::try_from(x).unwrap()); - let bx = ffi::measure::bounds(&h).unwrap(); - let (rec, _) = native(&Brep::made(h).cpp_arrays().unwrap()).unwrap(); - rec.validate().unwrap(); - Measured { size, counts: n, closed, bounds: bx, kinds: kinds(&rec) } - }); - judge(name, "C++", got) -} -#[cfg(feature = "native")] -fn parity(name: &str) { - match (solidcore_case(name), cpp_case(name)) { - (Ok(a), Ok(b)) => { - assert!(rel(a.size, b.size) < 1e-9, "{name}: solidcore {} vs C++ {}", a.size, b.size); - assert_eq!(a.counts, b.counts, "{name}: counts (solidcore vs C++)"); - assert_eq!(a.closed, b.closed, "{name}: closed (solidcore vs C++)"); - assert_eq!(a.kinds, b.kinds, "{name}: faces by surface kind (solidcore vs C++)"); - let near = |x: [f64; 3], y: [f64; 3]| x.iter().zip(&y).all(|(p, q)| (p - q).abs() < 1e-6); - assert!(near(a.bounds.0, b.bounds.0) && near(a.bounds.1, b.bounds.1), "{name}: bounds {:?} vs C++ {:?}", a.bounds, b.bounds); - } - (a, b) => assert_eq!(a.err(), b.err(), "{name}: errors (solidcore vs C++)"), - } -} macro_rules! cases { ($($m:ident $(: $ign:meta)?),* $(,)?) => {$( @@ -249,19 +209,6 @@ macro_rules! cases { fn solidcore_matches_the_closed_form() { let _ = super::solidcore_case(stringify!($m)); } - #[cfg(feature = "native")] - #[test] - $(#[$ign])? - fn solidcore_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - super::parity(stringify!($m)); - } - #[cfg(feature = "native")] - #[test] - fn the_cpp_kernel_matches_the_closed_form() { - let _cpp = crate::route::on_cpp(); - let _ = super::cpp_case(stringify!($m)); - } } )*}; } @@ -398,24 +345,6 @@ fn a_flap_on_a_cube_edge_stays_beside_it() { assert!((v - 1000.0).abs() < 1e-6 && (a - 700.0).abs() < 1e-6, "volume {v}, area {a}"); } -/// The same three on C++, the same input converted: the fork's e5d9bba, as its capi tests say. -#[cfg(feature = "native")] -#[test] -fn sheets_on_a_cube_stitch_the_same_on_cpp() { - let _cpp = crate::route::on_cpp(); - use crate::{Brep, error, ffi}; - crate::init().unwrap(); - let sew = |b: Body| ffi::sheets::stitch(Brep::made_core(b).cpp().unwrap(), 1e-6).map_err(error); - let (cover, closed) = sew(cube_and_biquadratic_cover()).unwrap(); - assert!(closed); - assert_eq!(ffi::counts(&cover).unwrap()[0], 6); - assert!((ffi::volume(&cover, 1e-8).unwrap() - 1100.0).abs() < 1e-6); - assert_eq!(sew(cube_and_bicubic_cover()).err(), Some(Error::Unsupported)); - let (flap, closed) = sew(cube_and_flap()).unwrap(); - assert!(!closed); - assert_eq!(ffi::counts(&flap).unwrap()[0], 7); - assert!((ffi::volume(&flap, 1e-8).unwrap() - 1000.0).abs() < 1e-6); -} /// The inputs are never changed. #[test] @@ -429,29 +358,3 @@ fn the_body_stays_as_it_was() { assert_eq!(counts(&block).unwrap(), was); } -/// Face and edge tags (`Brep::tagged`) through a stitch and a merge on solidcore, as on -/// C++: the input is tagged on solidcore, converted to C++ and tagged the same way there. -#[cfg(feature = "native")] -mod history { - use crate::{Brep, ffi}; - - #[test] - fn tags_cross_a_stitch() { - let _cpp = crate::route::on_cpp(); - crate::init().unwrap(); - let a = Brep::made_core(super::loose_faces(super::ALL, 0.0)).tagged(0).unwrap(); - let cpp = Brep::made(ffi::sheets::stitch(a.cpp().unwrap(), super::TOL).unwrap().0).history().unwrap(); - let core = Brep::made_core(super::stitch(a.core().unwrap(), super::TOL).unwrap().0).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - } - - #[test] - fn tags_cross_a_merge() { - let _cpp = crate::route::on_cpp(); - crate::init().unwrap(); - let a = Brep::made_core(super::split_cube()).tagged(0).unwrap(); - let cpp = Brep::made(ffi::ops::merge_faces(a.cpp().unwrap(), &[], super::TOL).unwrap()).history().unwrap(); - let core = Brep::made_core(super::merge_faces(a.core().unwrap(), &[], super::TOL).unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - } -} diff --git a/crates/smlib/src/core/offset_tests.rs b/crates/smlib/src/core/offset_tests.rs index 6241ed1094..7a4874cea6 100644 --- a/crates/smlib/src/core/offset_tests.rs +++ b/crates/smlib/src/core/offset_tests.rs @@ -1,7 +1,5 @@ //! Shells, face offsets, thickening, sheet offsets and drafts on solidcore, against closed forms: -//! every result exports to a record that validates. With SMLib built, the same case on the C++ -//! kernel gives the same volume (or, for a sheet, area), counts and faces by surface kind, or the -//! same error (parity only: the expected values are the analytic ones). +//! every result exports to a record that validates. //! //! The bodies are solidcore's own boxes, cylinders and linear sweeps (and booleans of them). @@ -337,78 +335,7 @@ fn solidcore_case(name: &'static str) -> Got { }) } -/// The case on the C++ kernel, called directly (whatever the choice). -#[cfg(feature = "native")] -fn cpp_case(name: &str) -> Got { - use crate::ops::pick_error; - use crate::sweeps::DraftCorner; - use crate::{Brep, error, ffi}; - crate::init().unwrap(); - let line = |a: [f64; 3], b: [f64; 3]| crate::sweeps::Curve::Line { start: a, end: b }; - let make = |s: Shape| -> Brep { - match s { - Shape::Box(o, z) => ffi::make_box(&o, &z).map(Brep::made).unwrap(), - Shape::Cylinder(o, r, h) => ffi::make_cylinder(&o, r, h).map(Brep::made).unwrap(), - Shape::Sphere(o, r) => ffi::make_sphere(&o, r).map(Brep::made).unwrap(), - Shape::Frustum(o, a, b, h) => ffi::make_cone(&o, a, b, h).map(Brep::made).unwrap(), - Shape::LineSheet(a, b, dir, dist) => Brep::extrude(&[line(a, b)], dir, dist, 0.0, DraftCorner::default(), false).unwrap(), - Shape::ArcSheet(o, r, degrees, h) => { - let arc = crate::sweeps::Curve::Arc { - center: o, - axis: [0.0, 0.0, 1.0], - reference: [1.0, 0.0, 0.0], - radius: r, - start: 0.0, - end: degrees.to_radians(), - }; - Brep::extrude(&[arc], [0.0, 0.0, 1.0], h, 0.0, DraftCorner::default(), false).unwrap() - } - Shape::Notched => { - let block = ffi::make_box(&[0.0; 3], &[60.0, 40.0, 10.0]).unwrap(); - let pin = ffi::make_cylinder(&[22.5, 7.0, -5.0], 3.0, 20.0).unwrap(); - let notch = ffi::make_box(&[25.0, -5.0, -5.0], &[10.0, 10.0, 20.0]).unwrap(); - let a = ffi::ops::boolean(&block, &pin, ffi::ops::BOOLEAN_SUBTRACT).unwrap(); - Brep::made(ffi::ops::boolean(&a, ¬ch, ffi::ops::BOOLEAN_SUBTRACT).unwrap()) - } - } - }; - let c = case(name); - let tol = tolerance(name); - let r = match &c.op { - Op::Shell(s, points, t) => ffi::ops::pick_op(ffi::ops::Pick::Shell, make(*s).cpp().unwrap(), points, tol, *t).map_err(pick_error), - Op::OffsetFaces(s, points, d) => ffi::ops::pick_op(ffi::ops::Pick::OffsetFaces, make(*s).cpp().unwrap(), points, tol, *d).map_err(pick_error), - Op::Thicken(s, t, sym) => ffi::ops::thicken(make(*s).cpp().unwrap(), *t, *sym).map_err(error), - Op::OffsetSheet(s, d) => ffi::sheets::offset_sheet(make(*s).cpp().unwrap(), *d).map_err(error), - Op::Draft(s, points, o, n, pull, angle) => ffi::ops::draft(make(*s).cpp().unwrap(), points, tol, o, n, pull, *angle).map_err(pick_error), - }; - let sheet = is_sheet(&c.expect); - let got = r.map(|h| { - let size = if sheet { ffi::measure::area(&h, 1e-8) } else { ffi::volume(&h, 1e-8) }.unwrap(); - let n = ffi::counts(&h).unwrap().map(|x| usize::try_from(x).unwrap()); - let bx = ffi::measure::bounds(&h).unwrap(); - let b = Brep::made(h); - let (rec, _) = native(&b.cpp_arrays().unwrap()).unwrap(); - rec.validate().unwrap(); - Measured { size, bounds: bx, counts: n, kinds: kinds(&rec) } - }); - judge(name, "C++", &c.expect, got) -} -/// solidcore and C++ agree: volume (area) within 1e-9, the same counts and faces by kind, or the -/// same error. -#[cfg(feature = "native")] -fn parity(name: &'static str) { - let cpp = cpp_case(name); - let ours = solidcore_case(name); - match (ours, cpp) { - (Ok(a), Ok(b)) => { - assert!(rel(a.size, b.size) < 1e-9, "{name}: solidcore {} vs C++ {}", a.size, b.size); - assert_eq!(a.counts, b.counts, "{name}: face, edge, vertex counts (solidcore vs C++)"); - assert_eq!(a.kinds, b.kinds, "{name}: faces by surface kind (solidcore vs C++)"); - } - (a, b) => assert_eq!(a.err(), b.err(), "{name}: errors (solidcore vs C++)"), - } -} macro_rules! cases { ($($m:ident $(: $ign:meta)?),* $(,)?) => {$( @@ -418,19 +345,6 @@ macro_rules! cases { fn solidcore_matches_the_closed_form() { let _ = super::solidcore_case(stringify!($m)); } - #[cfg(feature = "native")] - #[test] - $(#[$ign])? - fn solidcore_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - super::parity(stringify!($m)); - } - #[cfg(feature = "native")] - #[test] - fn the_cpp_kernel_matches_the_closed_form() { - let _cpp = crate::route::on_cpp(); - let _ = super::cpp_case(stringify!($m)); - } } )*}; } @@ -612,207 +526,9 @@ fn a_draft_leaves_no_mark() { } } -/// `moved_faces` on both kernels: drafts so small they leave a plane or cylinder where it was -/// still moved a face (SMLib's mark and solidcore's both stay on it), an angle that brings a wall -/// onto the plane of the wall above it leaves the two faces apart, and no draft here is -/// SMLIB_ERR_NO_EFFECT on either: the same answer, volume and counts. The C++ check holds a local -/// operation to what it promised and no body found so far trips it. -#[cfg(feature = "native")] -#[test] -fn a_draft_moves_a_face_as_on_cpp() { - let _cpp = crate::route::on_cpp(); - use crate::sweeps::{Curve, DraftCorner}; - use crate::{Brep, ffi}; - crate::init().unwrap(); - let z = [0.0, 0.0, 1.0]; - let make = |h: ffi::Handle| Brep::made(h); - let plate = make(ffi::make_box(&[0.0; 3], &[40.0, 40.0, 10.0]).unwrap()); - let rod = make(ffi::make_cylinder(&[20.0, 20.0, -5.0], 5.0, 30.0).unwrap()); - let holed = make(ffi::ops::boolean(plate.cpp().unwrap(), rod.cpp().unwrap(), ffi::ops::BOOLEAN_SUBTRACT).unwrap()); - let pinned = make(ffi::ops::boolean(plate.cpp().unwrap(), rod.cpp().unwrap(), ffi::ops::BOOLEAN_UNION).unwrap()); - let t = 5f64.to_radians().tan(); - // A wall x = 20 up to z = 10, then a wall leaning in 5 degrees: the lower wall turned 5 - // degrees about z = 10 lies on the upper one's plane. - let p = [[0.0, 0.0, 0.0], [20.0, 0.0, 0.0], [20.0, 0.0, 10.0], [20.0 - 10.0 * t, 0.0, 20.0], [0.0, 0.0, 20.0]]; - let profile: Vec = (0..5).map(|i| Curve::Line { start: p[i], end: p[(i + 1) % 5] }).collect(); - crate::sweeps::ON_CPP.with(|c| c.set(true)); - let leaning = Brep::extrude(&profile, [0.0, 1.0, 0.0], 10.0, 0.0, DraftCorner::default(), true).unwrap(); - crate::sweeps::ON_CPP.with(|c| c.set(false)); - let five = 5f64.to_radians(); - let cases: [(&str, &Brep, [f64; 3], [f64; 3], f64); 7] = [ - ("tiny on a plane", &plate, [20.0, 0.0, 5.0], [0.0; 3], 1e-14), - ("tiny on a hole", &holed, [25.0, 20.0, 5.0], [0.0, 0.0, 5.0], 1e-14), - ("tiny on a pin", &pinned, [25.0, 20.0, 20.0], [0.0, 0.0, 5.0], -1e-14), - ("small on a hole", &holed, [25.0, 20.0, 5.0], [0.0, 0.0, 5.0], 1e-6), - ("onto the wall above", &leaning, [20.0, 5.0, 5.0], [0.0, 0.0, 10.0], five), - ("away from the wall above", &leaning, [20.0, 5.0, 5.0], [0.0, 0.0, 10.0], -five), - ("hinged far off", &plate, [20.0, 0.0, 5.0], [0.0, 0.0, 1000.0], five), - ]; - for (name, b, at, origin, angle) in cases { - let cpp = ffi::ops::draft(b.cpp().unwrap(), &[at], TOL, &origin, &z, &z, angle).unwrap(); - let (v, n) = (ffi::volume(&cpp, 1e-8).unwrap(), ffi::counts(&cpp).unwrap().map(|x| usize::try_from(x).unwrap())); - let core = draft(b.core().unwrap(), &[at], TOL, origin, z, z, angle).unwrap(); - let w = volume(&core, 1e-8).unwrap(); - assert!(rel(w, v) < 1e-9, "{name}: solidcore {w} vs C++ {v}"); - assert_eq!(counts(&core).unwrap(), n, "{name}"); - } -} -/// The tip cut off a triangular prism, drafted about a plane 100 mm off so that the whole face goes -/// out past where its neighbours meet: its edges vanish, `SM_ERR_OFFSET_EDGE_VANISHES` in SMLib, -/// which the capi's `status` answers as `SMLIB_ERR_UNSUPPORTED`, on both kernels. -#[cfg(feature = "native")] -#[test] -fn a_draft_past_the_neighbours_meeting_is_unsupported_as_on_cpp() { - let _cpp = crate::route::on_cpp(); - use crate::sweeps::{Curve, DraftCorner}; - use crate::{Brep, ffi}; - crate::init().unwrap(); - let p = [[0.0, 0.0, 0.0], [20.0, 0.0, 0.0], [11.0, 18.0, 0.0], [9.0, 18.0, 0.0]]; - let profile: Vec = (0..4).map(|i| Curve::Line { start: p[i], end: p[(i + 1) % 4] }).collect(); - crate::sweeps::ON_CPP.with(|c| c.set(true)); - let prism = Brep::extrude(&profile, [0.0, 0.0, 1.0], 10.0, 0.0, DraftCorner::default(), true).unwrap(); - crate::sweeps::ON_CPP.with(|c| c.set(false)); - let (tip, z, origin, angle) = ([[10.0, 18.0, 5.0]], [0.0, 0.0, 1.0], [0.0, 0.0, -100.0], (-2f64).to_radians()); - let cpp = ffi::ops::draft(prism.cpp().unwrap(), &tip, TOL, &origin, &z, &z, angle); - assert_eq!(cpp.err(), Some((ffi::ops::ERR_UNSUPPORTED, None))); - assert_eq!(draft(prism.core().unwrap(), &tip, TOL, origin, z, z, angle).err(), Some(Error::Unsupported)); -} -/// A flat L of two faces thickened by 2 with mitred walls: 2 x 10 x (20 + 22), as the C++ kernel -/// makes it. -#[cfg(feature = "native")] -#[test] -fn an_l_sheet_thickens_as_on_cpp() { - let _cpp = crate::route::on_cpp(); - use crate::sweeps::{Curve, DraftCorner}; - use crate::{Brep, ffi}; - crate::init().unwrap(); - let l = [Curve::Line { start: [0.0; 3], end: [20.0, 0.0, 0.0] }, Curve::Line { start: [20.0, 0.0, 0.0], end: [20.0, 20.0, 0.0] }]; - crate::sweeps::ON_CPP.with(|c| c.set(true)); - let sheet = Brep::extrude(&l, [0.0, 0.0, 1.0], 10.0, 0.0, DraftCorner::default(), false).unwrap(); - crate::sweeps::ON_CPP.with(|c| c.set(false)); - let cpp = ffi::ops::thicken(sheet.cpp().unwrap(), 2.0, false).unwrap(); - assert!(rel(ffi::volume(&cpp, 1e-8).unwrap(), 840.0) < 1e-9); - let core = thicken(sheet.core().unwrap(), 2.0, false).unwrap(); - assert!(rel(volume(&core, 1e-8).unwrap(), 840.0) < 1e-9); -} -/// Face and edge tags (`Brep::tagged`) through offsets on solidcore, as on C++: the box is -/// tagged on the C++ kernel, its solidcore body converted from that and tagged the same way, and -/// the two results' histories must be the same. -#[cfg(feature = "native")] -mod history { - use crate::{Brep, ffi}; - - fn tagged_box() -> Brep { - crate::init().unwrap(); - Brep::made(ffi::make_box(&[0.0; 3], &[20.0, 30.0, 10.0]).unwrap()).tagged(0).unwrap() - } - - /// The box hollowed to 2 mm walls, its top open: the five walls offset inward stand for the - /// faces they offset. - #[test] - fn tags_cross_a_shell() { - let _cpp = crate::route::on_cpp(); - let a = tagged_box(); - let top = [[10.0, 15.0, 10.0]]; - let cpp = Brep::made(ffi::ops::pick_op(ffi::ops::Pick::Shell, a.cpp().unwrap(), &top, super::TOL, -2.0).unwrap()).history().unwrap(); - let core = Brep::made_core(super::shell(a.core().unwrap(), &top, super::TOL, -2.0).unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - } - - /// The top moved down 2 mm: it stands for itself, its four neighbours are trimmed to meet it. - #[test] - fn tags_cross_a_face_offset() { - let _cpp = crate::route::on_cpp(); - let a = tagged_box(); - let top = [[10.0, 15.0, 10.0]]; - let cpp = Brep::made(ffi::ops::pick_op(ffi::ops::Pick::OffsetFaces, a.cpp().unwrap(), &top, super::TOL, -2.0).unwrap()).history().unwrap(); - let core = Brep::made_core(super::offset_faces(a.core().unwrap(), &top, super::TOL, -2.0).unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - } - - /// A flat sheet and a half-round open extrusion, tagged on the C++ kernel. - fn tagged_sheet(curved: bool) -> Brep { - use std::f64::consts::PI; - - use crate::sweeps::{Curve, DraftCorner}; - crate::init().unwrap(); - crate::sweeps::ON_CPP.with(|c| c.set(true)); - let b = if curved { - let arc = [Curve::Arc { center: [0.0; 3], axis: [0.0, 0.0, 1.0], reference: [1.0, 0.0, 0.0], radius: 10.0, start: 0.0, end: PI }]; - Brep::extrude(&arc, [0.0, 0.0, 1.0], 20.0, 0.0, DraftCorner::default(), false) - } else { - let c = [[0.0, 0.0, 0.0], [20.0, 0.0, 0.0], [20.0, 10.0, 0.0], [0.0, 10.0, 0.0]]; - let l: Vec = (0..4).map(|i| Curve::Line { start: c[i], end: c[(i + 1) % 4] }).collect(); - Brep::planar_faces(&l) - }; - crate::sweeps::ON_CPP.with(|c| c.set(false)); - b.unwrap().tagged(0).unwrap() - } - - /// A sheet thickened to one side and to both: both faces stand for the sheet's (the histories - /// `offset_backend` reads), its edges and their offsets for its edges, the walls for nothing. - fn thicken_parity(curved: bool) { - for symmetric in [false, true] { - let a = tagged_sheet(curved); - let cpp = Brep::made(ffi::ops::thicken(a.cpp().unwrap(), 2.0, symmetric).unwrap()).history().unwrap(); - let core = Brep::made_core(super::thicken(a.core().unwrap(), 2.0, symmetric).unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical(), "symmetric {symmetric}"); - } - } - - #[test] - fn tags_cross_a_thicken_of_a_curved_sheet() { - let _cpp = crate::route::on_cpp(); - thicken_parity(true); - } - - #[test] - fn tags_cross_a_thicken_of_a_flat_sheet() { - let _cpp = crate::route::on_cpp(); - thicken_parity(false); - } - - /// A sheet moved along its normal, flat and curved: each face stands for itself. - #[test] - fn tags_cross_a_sheet_offset() { - let _cpp = crate::route::on_cpp(); - for curved in [false, true] { - let a = tagged_sheet(curved); - let cpp = Brep::made(ffi::sheets::offset_sheet(a.cpp().unwrap(), 1.5).unwrap()).history().unwrap(); - let core = Brep::made_core(super::offset_sheet(a.core().unwrap(), 1.5).unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical(), "curved {curved}"); - } - } - - /// A cylinder hollowed, its top open: the walls round closed edges, on a curved face. - #[test] - fn tags_cross_a_shell_of_a_cylinder() { - let _cpp = crate::route::on_cpp(); - crate::init().unwrap(); - let a = Brep::made(ffi::make_cylinder(&[0.0; 3], 10.0, 20.0).unwrap()).tagged(0).unwrap(); - let top = [[0.0, 0.0, 20.0]]; - let cpp = Brep::made(ffi::ops::pick_op(ffi::ops::Pick::Shell, a.cpp().unwrap(), &top, super::TOL, -2.0).unwrap()).history().unwrap(); - let core = Brep::made_core(super::shell(a.core().unwrap(), &top, super::TOL, -2.0).unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - } - - /// A 5 degree draft of the front about the bottom plane: the front tilts, its neighbours' - /// edges move with it. - #[test] - fn tags_cross_a_draft() { - let _cpp = crate::route::on_cpp(); - let a = tagged_box(); - let front = [[10.0, 0.0, 5.0]]; - let (origin, normal, pull, angle) = ([0.0; 3], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], 5f64.to_radians()); - let cpp = ffi::ops::draft(a.cpp().unwrap(), &front, super::TOL, &origin, &normal, &pull, angle).unwrap(); - let cpp = Brep::made(cpp).history().unwrap(); - let core = Brep::made_core(super::draft(a.core().unwrap(), &front, super::TOL, origin, normal, pull, angle).unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - } -} /// A stepped block, 40 x 30 x 10 with its half x < 20 raised to 20 (rig case /// shell/cov-replace-face-step-onto-the-higher-face, the fork's `offset_faces_close_a_step_wall`): @@ -833,29 +549,15 @@ fn offset_faces_close_a_step_wall() { } } + /// Rig case shell/4492's rounded box: four mitred rounds, r 3, come off a 40 x 30 x 10 box's top /// together and follow it. From height 0 the outline is the 34 x 24 top moved out by the round's /// w(z) = sqrt(9 - (z - axis)^2), or 3 below the axis. With the fork's values at +19, and bodies /// whose rounds go all the way down from -7 and are cut short from -7.5 (solidcore#2027). At -9 /// solidcore refuses to put the rounds back (solidcore#2020, open): SolidCraft says it does not -/// make that move, where the C++ kernel gives the analytic value, also checked here. With SMLib -/// built, the C++ kernel gives the same at every other move. -#[cfg(feature = "native")] +/// make that move. #[test] fn mitred_rounds_follow_a_moved_top() { - let _cpp = crate::route::on_cpp(); - mitred_rounds_follow_a_moved_top_check(); -} - -/// The same on a build without the C++ kernel: solidcore's half alone. A test of its own, so that -/// tests/KNOWN_FAILING_TESTS's entry for the C++ comparison (mitred_rounds_follow_a_moved_top) is graded only where it runs. -#[cfg(not(feature = "native"))] -#[test] -fn mitred_rounds_follow_a_moved_top_on_solidcore() { - mitred_rounds_follow_a_moved_top_check(); -} - -fn mitred_rounds_follow_a_moved_top_check() { use super::blend::{Corners, blend}; use crate::Blend; let edges = [[20.0, 0.0, 10.0], [20.0, 30.0, 10.0], [0.0, 15.0, 10.0], [40.0, 15.0, 10.0]]; @@ -888,22 +590,60 @@ fn mitred_rounds_follow_a_moved_top_check() { assert!(rel(v, analytic(d)) < 1e-6, "d {d}: {v} vs {}", analytic(d)); } assert!(matches!(offset_faces(&rounded, &[top], 0.1, -9.0), Err(Error::Unsupported))); - #[cfg(feature = "native")] - { - use crate::ffi; - crate::init().unwrap(); - let b = ffi::make_box(&[0.0; 3], &[40.0, 30.0, 10.0]).unwrap(); - let round = ffi::ops::blend(ffi::ops::BLEND_ROUND, &b, &edges, &[3.0; 4], TOL).unwrap(); - for (d, _) in moves { - let cpp = ffi::ops::pick_op(ffi::ops::Pick::OffsetFaces, &round, &[top], 0.1, d).unwrap(); - let ours = offset_faces(&rounded, &[top], 0.1, d).unwrap(); - let (o, c) = (volume(&ours, 1e-8).unwrap(), ffi::volume(&cpp, 1e-8).unwrap()); - // Below -7 the rounds are cut short: both are within 1e-6 of the analytic value, and at - // -7.5 within 4.7e-9 of each other. - let agree = if d < -7.0 { 1e-8 } else { 1e-9 }; - assert!(rel(o, c) < agree, "d {d}: {o} vs the C++ kernel's {c}"); - } - // At -9 the C++ kernel at 26cc451 refuses it too. - assert!(ffi::ops::pick_op(ffi::ops::Pick::OffsetFaces, &round, &[top], 0.1, -9.0).is_err(), "d -9"); +} + +/// `moved_faces`: drafts so small they leave a plane or cylinder where it was still moved a face +/// (solidcore's mark stays on it), and an angle that brings a wall onto the plane of the wall above +/// it leaves the two faces apart: every draft here builds, none is "no effect". +#[test] +fn a_draft_moves_a_face() { + use crate::sweeps::{Curve, DraftCorner}; + let z = [0.0, 0.0, 1.0]; + let plate = make_box([0.0; 3], [40.0, 40.0, 10.0]).unwrap(); + let rod = make_cylinder([20.0, 20.0, -5.0], 5.0, 30.0).unwrap(); + let holed = super::boolean::boolean(&plate, &rod, BooleanOp::Subtract).unwrap().unwrap(); + let pinned = super::boolean::boolean(&plate, &rod, BooleanOp::Union).unwrap().unwrap(); + let t = 5f64.to_radians().tan(); + // A wall x = 20 up to z = 10, then a wall leaning in 5 degrees: the lower wall turned 5 + // degrees about z = 10 lies on the upper one's plane. + let p = [[0.0, 0.0, 0.0], [20.0, 0.0, 0.0], [20.0, 0.0, 10.0], [20.0 - 10.0 * t, 0.0, 20.0], [0.0, 0.0, 20.0]]; + let profile: Vec = (0..5).map(|i| Curve::Line { start: p[i], end: p[(i + 1) % 5] }).collect(); + let leaning = super::sweep::extrude(&profile, [0.0, 1.0, 0.0], 10.0, 0.0, DraftCorner::default(), true).unwrap(); + let five = 5f64.to_radians(); + let cases: [(&str, &Body, [f64; 3], [f64; 3], f64); 7] = [ + ("tiny on a plane", &plate, [20.0, 0.0, 5.0], [0.0; 3], 1e-14), + ("tiny on a hole", &holed, [25.0, 20.0, 5.0], [0.0, 0.0, 5.0], 1e-14), + ("tiny on a pin", &pinned, [25.0, 20.0, 20.0], [0.0, 0.0, 5.0], -1e-14), + ("small on a hole", &holed, [25.0, 20.0, 5.0], [0.0, 0.0, 5.0], 1e-6), + ("onto the wall above", &leaning, [20.0, 5.0, 5.0], [0.0, 0.0, 10.0], five), + ("away from the wall above", &leaning, [20.0, 5.0, 5.0], [0.0, 0.0, 10.0], -five), + ("hinged far off", &plate, [20.0, 0.0, 5.0], [0.0, 0.0, 1000.0], five), + ]; + for (name, b, at, origin, angle) in cases { + let r = draft(b, &[at], TOL, origin, z, z, angle).unwrap_or_else(|e| panic!("{name}: {e}")); + assert!(volume(&r, 1e-8).unwrap() > 0.0, "{name}"); } } + +/// The tip cut off a triangular prism, drafted about a plane 100 mm off so that the whole face goes +/// out past where its neighbours meet: its edges vanish, `SM_ERR_OFFSET_EDGE_VANISHES` in SMLib, +/// which the capi's `status` answers as `SMLIB_ERR_UNSUPPORTED`. +#[test] +fn a_draft_past_the_neighbours_meeting_is_unsupported() { + use crate::sweeps::{Curve, DraftCorner}; + let p = [[0.0, 0.0, 0.0], [20.0, 0.0, 0.0], [11.0, 18.0, 0.0], [9.0, 18.0, 0.0]]; + let profile: Vec = (0..4).map(|i| Curve::Line { start: p[i], end: p[(i + 1) % 4] }).collect(); + let prism = super::sweep::extrude(&profile, [0.0, 0.0, 1.0], 10.0, 0.0, DraftCorner::default(), true).unwrap(); + let (tip, z, origin, angle) = ([[10.0, 18.0, 5.0]], [0.0, 0.0, 1.0], [0.0, 0.0, -100.0], (-2f64).to_radians()); + assert_eq!(draft(&prism, &tip, TOL, origin, z, z, angle).err(), Some(Error::Unsupported)); +} + +/// A flat L of two faces thickened by 2 with mitred walls: 2 x 10 x (20 + 22). +#[test] +fn an_l_sheet_thickens() { + use crate::sweeps::{Curve, DraftCorner}; + let l = [Curve::Line { start: [0.0; 3], end: [20.0, 0.0, 0.0] }, Curve::Line { start: [20.0, 0.0, 0.0], end: [20.0, 20.0, 0.0] }]; + let sheet = super::sweep::extrude(&l, [0.0, 0.0, 1.0], 10.0, 0.0, DraftCorner::default(), false).unwrap(); + let core = thicken(&sheet, 2.0, false).unwrap(); + assert!(rel(volume(&core, 1e-8).unwrap(), 840.0) < 1e-9); +} diff --git a/crates/smlib/src/core/split.rs b/crates/smlib/src/core/split.rs index f39044404d..a7ed15dabc 100644 --- a/crates/smlib/src/core/split.rs +++ b/crates/smlib/src/core/split.rs @@ -59,8 +59,8 @@ use crate::{Error, Result}; use solidcore::topology::brep::p3::brep_copy; /// `SMLIB_SPLIT_MAX_FACES`, `SMLIB_SPLIT_MAX_TOOLS`. -pub(crate) const MAX_FACES: usize = crate::ffi::split::MAX_FACES; -pub(crate) const MAX_TOOLS: usize = crate::ffi::split::MAX_TOOLS; +pub(crate) const MAX_FACES: usize = 100_000; +pub(crate) const MAX_TOOLS: usize = 4096; /// `SM_ERR`. const SM_ERR: i32 = 1001; diff --git a/crates/smlib/src/core/split_tests.rs b/crates/smlib/src/core/split_tests.rs index f2c5c0de65..890794f0da 100644 --- a/crates/smlib/src/core/split_tests.rs +++ b/crates/smlib/src/core/split_tests.rs @@ -1,8 +1,6 @@ //! Split Face and Split Body on solidcore, against closed forms: face, edge and vertex counts, //! volumes, how many faces each chosen face became, and every result exports to a record that -//! validates. With SMLib built, the same input converted to the C++ kernel and the same tools -//! give the same result or the same error (parity only: the expected values are the analytic -//! ones). +//! validates. //! //! The body is a 10 mm cube [0, 10]^3 on solidcore; the tools are described as //! [`crate::ssi::Surface`] describes them. @@ -129,75 +127,8 @@ fn solidcore_case(name: &str) -> Got { judge(name, "solidcore", got) } -/// The C++ kernel's surface for `spec`. -#[cfg(feature = "native")] -fn surface(spec: &Spec) -> crate::ssi::Surface { - use crate::ssi::Surface; - match spec { - Spec::Plane { origin, xaxis, yaxis, u, v } => Surface::plane(*origin, *xaxis, *yaxis, *u, *v), - Spec::Cylinder { origin, xaxis, yaxis, radius, angle_deg, height } => { - Surface::cylinder(*origin, *xaxis, *yaxis, *radius, *angle_deg, *height) - } - _ => panic!("no such tool in these cases"), - } - .unwrap() -} -/// The case on the C++ kernel, called directly (whatever the choice), on the cube converted -/// from solidcore's. -#[cfg(feature = "native")] -fn cpp_case(name: &str) -> Got { - use crate::{Brep, error, ffi}; - crate::init().unwrap(); - let (op, _) = case(name); - let body = Brep::made_core(cube()); - let measure = |h: &ffi::Handle| { - let (rec, _) = native(&Brep::made(ffi::copy(h).unwrap()).cpp_arrays().unwrap()).unwrap(); - rec.validate().unwrap(); - (ffi::volume(h, 1e-8).unwrap(), ffi::counts(h).unwrap().map(|x| usize::try_from(x).unwrap())) - }; - let got = match &op { - Op::Faces(faces, tools) => { - let made: Vec = tools.iter().map(surface).collect(); - let handles: Vec<&ffi::ssi::SurfaceHandle> = made.iter().map(|s| s.handle().unwrap()).collect(); - let faces: Vec = faces.iter().map(|&f| f as i64).collect(); - match ffi::split::split_faces(body.cpp().unwrap(), &faces, &handles) { - Ok((h, made)) => { - let (volume, counts) = measure(&h); - Ok(Measured::Split { volume, counts, made: made.into_iter().map(|m| usize::try_from(m).unwrap()).collect() }) - } - Err(ffi::ops::ERR_NO_CUT) => Ok(Measured::NoCut), - Err(s) => Err(error(s)), - } - } - Op::Body(tool) => match ffi::split::split_body(body.cpp().unwrap(), surface(tool).handle().unwrap()) { - Ok((b, f)) => { - let (vb, nb) = measure(&b); - let (vf, nf) = measure(&f); - Ok(Measured::Halves { volumes: [vb, vf], counts: [nb, nf] }) - } - Err(ffi::ops::ERR_NO_CUT) => Ok(Measured::NoCut), - Err(s) => Err(error(s)), - }, - }; - judge(name, "C++", got) -} -#[cfg(feature = "native")] -fn parity(name: &str) { - match (solidcore_case(name), cpp_case(name)) { - (Ok(Measured::Split { volume: a, counts: na, made: ma }), Ok(Measured::Split { volume: b, counts: nb, made: mb })) => { - assert!(rel(a, b) < 1e-9, "{name}: solidcore {a} vs C++ {b}"); - assert_eq!(na, nb, "{name}: counts (solidcore vs C++)"); - assert_eq!(ma, mb, "{name}: faces made per face (solidcore vs C++)"); - } - (Ok(Measured::Halves { volumes: a, counts: na }), Ok(Measured::Halves { volumes: b, counts: nb })) => { - assert!(rel(a[0], b[0]) < 1e-9 && rel(a[1], b[1]) < 1e-9, "{name}: solidcore {a:?} vs C++ {b:?}"); - assert_eq!(na, nb, "{name}: counts (solidcore vs C++)"); - } - (a, b) => assert_eq!(a, b, "{name}: solidcore vs C++"), - } -} macro_rules! cases { ($($m:ident $(: $ign:meta)?),* $(,)?) => {$( @@ -207,19 +138,6 @@ macro_rules! cases { fn solidcore_matches_the_closed_form() { let _ = super::solidcore_case(stringify!($m)); } - #[cfg(feature = "native")] - #[test] - $(#[$ign])? - fn solidcore_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - super::parity(stringify!($m)); - } - #[cfg(feature = "native")] - #[test] - fn the_cpp_kernel_matches_the_closed_form() { - let _cpp = crate::route::on_cpp(); - let _ = super::cpp_case(stringify!($m)); - } } )*}; } @@ -308,8 +226,6 @@ fn lid(side: bool) -> Body { /// the sides are read from; on C++ too. #[test] fn split_body_by_an_l_sheet() { - #[cfg(feature = "native")] - let _cpp = crate::route::on_cpp(); let (c, sheet) = (cube(), lid(true)); for face in 0..2 { let (b, f) = split_body_by_sheet(&c, &sheet, face).unwrap().expect("the sheet divides the cube"); @@ -317,13 +233,6 @@ fn split_body_by_an_l_sheet() { valid("L sheet", &f); let (vb, vf) = (volume(&b, 1e-8).unwrap(), volume(&f, 1e-8).unwrap()); assert!(rel(vb.min(vf), 250.0) < 1e-9 && rel(vb.max(vf), 750.0) < 1e-9, "face {face}: {vb} + {vf}"); - #[cfg(feature = "native")] - { - let (body, tool) = (crate::Brep::made_core(cube()), crate::Brep::made_core(lid(true))); - let (bc, fc) = crate::ffi::split::split_body_by_sheet(body.cpp().unwrap(), tool.cpp().unwrap(), face as i64).unwrap(); - let (vbc, vfc) = (crate::ffi::volume(&bc, 1e-8).unwrap(), crate::ffi::volume(&fc, 1e-8).unwrap()); - assert!(rel(vb, vbc) < 1e-9 && rel(vf, vfc) < 1e-9, "face {face}: {vb} + {vf} vs C++ {vbc} + {vfc}"); - } } } @@ -349,25 +258,6 @@ fn the_body_stays_as_it_was() { assert!(rel(volume(&b, 1e-8).unwrap(), 1000.0) < 1e-12); } -/// Face and edge tags (`Brep::tagged`) through a face split on solidcore, as on C++. -#[cfg(feature = "native")] -mod history { - use crate::{Brep, ffi}; - - #[test] - fn tags_cross_a_face_split() { - let _cpp = crate::route::on_cpp(); - crate::init().unwrap(); - let a = Brep::made_core(super::cube()).tagged(0).unwrap(); - let p = super::plane_x(5.0); - let s = super::surface(&p); - let faces = [0i64, 1, 2, 3, 4, 5]; - let cpp = Brep::made(ffi::split::split_faces(a.cpp().unwrap(), &faces, &[s.handle().unwrap()]).unwrap().0).history().unwrap(); - let core = super::split_faces(a.core().unwrap(), &[0, 1, 2, 3, 4, 5], &[&p]).unwrap().unwrap().0; - let core = Brep::made_core(core).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - } -} /// A one-face sheet on a B-spline surface (as SolidCraft's extrude of a sketch spline makes it: /// the cubic through (0, 0), (13, -8), (27, 8), (40, 0) in x and z, straight along y over diff --git a/crates/smlib/src/core/ssi.rs b/crates/smlib/src/core/ssi.rs index f24b3f95f6..230bd8e3b6 100644 --- a/crates/smlib/src/core/ssi.rs +++ b/crates/smlib/src/core/ssi.rs @@ -15,8 +15,7 @@ use solidcore::surface::surface::p2::{SurfaceIntersection, TsectCurveType}; use super::convert::bspline_curve_part; use super::run; use super::split::{Tool, tool}; -use crate::ffi::ssi::Nurbs; -use crate::ssi::{CurveType, Method, Spec, SsiCurve, SsiPoint, SsiResult, curve2, curve3}; +use crate::ssi::{CurveType, Method, Nurbs, Spec, SsiCurve, SsiPoint, SsiResult, curve2, curve3}; use crate::{Error, Result}; /// The surface of `spec` with its natural UV domain (`construct`). diff --git a/crates/smlib/src/core/sweep.rs b/crates/smlib/src/core/sweep.rs index 4f02388baf..21c2180e7b 100644 --- a/crates/smlib/src/core/sweep.rs +++ b/crates/smlib/src/core/sweep.rs @@ -61,7 +61,8 @@ use super::blend::status; use super::offset::{face_miss, pick_faces}; use super::{Body, run, with_body}; use crate::sweeps::{Affine, Curve as Profile, DraftCorner, Ends, MAX_CURVES, MAX_STATION_CURVES, MAX_STATIONS, SweepFrame}; -use crate::{Error, Result, ffi}; +use crate::sweeps::{CURVE_MAX_DEGREE, CURVE_MAX_POINTS}; +use crate::{Error, Result}; /// The capi's statuses as SolidCraft errors: an operation's own result. type Out = std::result::Result; @@ -142,7 +143,7 @@ fn arc(k: &Kernel, center: [f64; 3], axis: [f64; 3], reference: [f64; 3], radius /// multiplicity degree + 1, inner ones at most degree), non-decreasing. fn nurbs(degree: usize, points: &[[f64; 4]], knots: &[f64]) -> Out { let count = points.len(); - if !(1..=ffi::sweeps::CURVE_MAX_DEGREE).contains(°ree) || count < degree + 1 || count > ffi::sweeps::CURVE_MAX_POINTS { + if !(1..=CURVE_MAX_DEGREE).contains(°ree) || count < degree + 1 || count > CURVE_MAX_POINTS { return Err(Error::Argument); } let mut ps = Vec::with_capacity(count); @@ -213,7 +214,7 @@ fn parse(k: &Kernel, curves: &[Profile]) -> Out> { } Profile::Nurbs { degree, points, knots } => { // `parse` reads the kinds first: at least two points. - if *degree < 1 || *degree > ffi::sweeps::CURVE_MAX_DEGREE || points.len() < 2 || points.len() > ffi::sweeps::CURVE_MAX_POINTS { + if *degree < 1 || *degree > CURVE_MAX_DEGREE || points.len() < 2 || points.len() > CURVE_MAX_POINTS { return Err(Error::Argument); } reals = reals.saturating_add(4 * points.len() + points.len() + degree + 1); @@ -519,16 +520,21 @@ pub(crate) fn extrude(profile: &[Profile], direction: [f64; 3], distance: f64, d }) } -/// A refused draft corner as the capi reports it: the reason's status with the corner's point, -/// [`Error::Corner`] when the point is finite and the status's own error otherwise. +/// The capi's statuses for a draft corner whose continued curves do not meet, and for a wall that +/// closes before the far end. +const ERR_CORNER_APART: i32 = -1108; +const ERR_WALL_CLOSES: i32 = -1109; + +/// A refused draft corner as the capi reports it: the reason with the corner's point, +/// [`Error::Corner`] when the point is finite and the reason's own error otherwise. fn refused(r: CornerRefusal) -> Error { - let (why, code) = match r.reason { - TaperCornerRefusal::Apart => (crate::sweeps::CornerRefusal::Apart, ffi::sweeps::ERR_CORNER_APART), - TaperCornerRefusal::WallCloses => (crate::sweeps::CornerRefusal::WallCloses, ffi::sweeps::ERR_WALL_CLOSES), - TaperCornerRefusal::NotExact => (crate::sweeps::CornerRefusal::NotExact, ffi::trim::ERR_NOT_EXACT), + let (why, otherwise) = match r.reason { + TaperCornerRefusal::Apart => (crate::sweeps::CornerRefusal::Apart, Error::Status(ERR_CORNER_APART)), + TaperCornerRefusal::WallCloses => (crate::sweeps::CornerRefusal::WallCloses, Error::Status(ERR_WALL_CLOSES)), + TaperCornerRefusal::NotExact => (crate::sweeps::CornerRefusal::NotExact, Error::NotExact), }; let at = [r.corner.x, r.corner.y, r.corner.z]; - if at.iter().all(|x| x.is_finite()) { Error::Corner { why, at } } else { crate::error(code) } + if at.iter().all(|x| x.is_finite()) { Error::Corner { why, at } } else { otherwise } } /// `smlib_revolve`: the planar `profile` turned `angle` radians about the axis through `origin` @@ -1588,9 +1594,9 @@ fn put_curve(c: &Curve) -> solidcore::Result> { knots.extend(std::iter::repeat_n(u, m)); } let clamped = can.knot_multiplicities.first().is_some_and(|&m| m > degree) && can.knot_multiplicities.last().is_some_and(|&m| m > degree); - if !(1..=ffi::sweeps::CURVE_MAX_DEGREE).contains(°ree) + if !(1..=CURVE_MAX_DEGREE).contains(°ree) || count < degree + 1 - || count > ffi::sweeps::CURVE_MAX_POINTS + || count > CURVE_MAX_POINTS || knots.len() != count + degree + 1 || !clamped { diff --git a/crates/smlib/src/core/sweep_tests.rs b/crates/smlib/src/core/sweep_tests.rs index 257ee4a182..79a9c5a9f7 100644 --- a/crates/smlib/src/core/sweep_tests.rs +++ b/crates/smlib/src/core/sweep_tests.rs @@ -1,7 +1,5 @@ //! Extrusions, revolutions, sweeps, lofts and planar faces on solidcore, against closed forms: -//! every result exports to a record that validates. With SMLib built, the same case on the C++ -//! kernel gives the same volume (or, for a sheet, area), counts and faces by surface kind, or the -//! same error (parity only: the expected values are the analytic ones). +//! every result exports to a record that validates. //! //! The profiles are built straight into solidcore curves from the curve lists. @@ -357,51 +355,7 @@ fn solidcore_case(name: &'static str) -> Got { }) } -/// The case on the C++ kernel, through the public sweeps forced onto it on this thread. -#[cfg(feature = "native")] -fn cpp_case(name: &str) -> Got { - use crate::{Brep, ffi}; - crate::sweeps::ON_CPP.with(|c| c.set(true)); - let c = case(name); - let r = match &c.op { - Op::PlanarFaces(l) => Brep::planar_faces(l), - Op::Extrude(p, d, dist, draft, corner, cap) => Brep::extrude(p, *d, *dist, *draft, *corner, *cap), - Op::Revolve(p, o, a, angle, cap) => Brep::revolve(p, *o, *a, *angle, *cap), - Op::Sweep(p, path, frame, cap) => Brep::sweep(p, path, *frame, *cap), - Op::Loft(s, d, cap) => Brep::loft(s, *d, *cap), - Op::LoftToPoint(s, apex) => Brep::loft_to_point(s, *apex), - Op::Stations(p, s, e) => Brep::sweep_stations(p, s, *e), - Op::Screw(p, o, a, pitch, turns, tol) => Brep::screw_sweep(p, *o, *a, *pitch, *turns, *tol), - Op::ScrewRadial(p, o, a, pitch, turns, law, tol) => Brep::screw_sweep_radial(p, *o, *a, *pitch, *turns, law, *tol), - }; - crate::sweeps::ON_CPP.with(|c| c.set(false)); - let sheet = matches!(c.expect, Expect::Sheet(_)); - let got = r.map(|b| { - let h = b.cpp().unwrap(); - let size = if sheet { ffi::measure::area(h, 1e-8) } else { ffi::volume(h, 1e-8) }.unwrap(); - let n = ffi::counts(h).unwrap().map(|x| usize::try_from(x).unwrap()); - let (rec, _) = native(&b.cpp_arrays().unwrap()).unwrap(); - rec.validate().unwrap(); - Measured { size, counts: n, kinds: kinds(&rec) } - }); - judge(name, "C++", &c.expect, got) -} -/// solidcore and C++ agree: volume (area) within 1e-9, the same counts and faces by kind, or the -/// same error. -#[cfg(feature = "native")] -fn parity(name: &'static str) { - let cpp = cpp_case(name); - let ours = solidcore_case(name); - match (ours, cpp) { - (Ok(a), Ok(b)) => { - assert!(rel(a.size, b.size) < 1e-9, "{name}: solidcore {} vs C++ {}", a.size, b.size); - assert_eq!(a.counts, b.counts, "{name}: face, edge, vertex counts (solidcore vs C++)"); - assert_eq!(a.kinds, b.kinds, "{name}: faces by surface kind (solidcore vs C++)"); - } - (a, b) => assert_eq!(a.err(), b.err(), "{name}: errors (solidcore vs C++)"), - } -} macro_rules! cases { ($($m:ident $([$pign:meta])? $(: $ign:meta)?),* $(,)?) => {$( @@ -411,26 +365,11 @@ macro_rules! cases { fn solidcore_matches_the_closed_form() { let _ = super::solidcore_case(stringify!($m)); } - #[cfg(feature = "native")] - #[test] - $(#[$ign])? - $(#[$pign])? - fn solidcore_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - super::parity(stringify!($m)); - } - #[cfg(feature = "native")] - #[test] - fn the_cpp_kernel_matches_the_closed_form() { - let _cpp = crate::route::on_cpp(); - let _ = super::cpp_case(stringify!($m)); - } } )*}; } -// Cases marked `[ignore = …]` match the closed form on solidcore but not the C++ kernel's -// numbers; `: ignore = …` ones fail on solidcore. +// `: ignore = …` cases fail on solidcore. cases!( planar_faces_with_a_hole, extrude_rectangle, @@ -461,16 +400,14 @@ cases!( /// in two pieces extrudes to the faces, edges and vertices of the whole circle (two caps and one /// cylinder: 3 faces, 3 edges with the seam, 2 vertices) and the area 2πr² + 2πrh; a half disc /// whose arc is in two quarters to its diameter's plane wall and one cylinder (4 faces, 6 edges, -/// 4 vertices) and the area πr² + πrh + 2rh. On solidcore, and with SMLib built on the C++ kernel. +/// 4 vertices) and the area πr² + πrh + 2rh. #[test] fn arcs_on_one_circle_sweep_one_wall() { - #[cfg(feature = "native")] - let _cpp = crate::route::on_cpp(); let cases = [ ("extrude_circle_in_two_halves", [3, 3, 2], 50.0 * PI + 100.0 * PI, true), ("extrude_circle_in_two_frames", [3, 3, 2], 50.0 * PI + 100.0 * PI, true), // solidcore sweeps a partial arc, merged or drawn whole, to a surface of extrusion (a - // rational spline in the record) where the C++ kernel makes a cylinder: one wall still. + // rational spline in the record) where SMLib made a cylinder: one wall still. ("extrude_half_disc_split_arc", [4, 6, 4], 25.0 * PI + 50.0 * PI + 100.0, false), ]; let walls = |kinds: &BTreeMap| kinds.iter().filter(|(k, _)| !k.starts_with("plane")).map(|(_, n)| *n).sum::(); @@ -486,25 +423,6 @@ fn arcs_on_one_circle_sweep_one_wall() { assert_eq!(kinds_got.get("cylinder"), Some(&1), "{name}: solidcore walls {kinds_got:?}"); } assert!(rel(area_got, want_area) < 1e-9, "{name}: solidcore area {area_got} vs analytic {want_area}"); - #[cfg(feature = "native")] - { - use crate::{Brep, ffi}; - crate::sweeps::ON_CPP.with(|c| c.set(true)); - let r = match case(name).op { - Op::Extrude(p, d, dist, draft, corner, cap) => Brep::extrude(&p, d, dist, draft, corner, cap), - op => panic!("{name}: {op:?}"), - }; - crate::sweeps::ON_CPP.with(|c| c.set(false)); - let b = r.unwrap_or_else(|e| panic!("{name}: C++: {e}")); - let h = b.cpp().unwrap(); - let n = ffi::counts(h).unwrap().map(|x| usize::try_from(x).unwrap()); - let (rec, _) = native(&b.cpp_arrays().unwrap()).unwrap(); - assert_eq!(n, want_counts, "{name}: C++ faces, edges, vertices"); - assert_eq!(walls(&kinds(&rec)), 1, "{name}: C++ curved walls {:?}", kinds(&rec)); - assert_eq!(kinds(&rec).get("cylinder"), Some(&1), "{name}: C++ walls {:?}", kinds(&rec)); - let a = ffi::measure::area(h, 1e-8).unwrap(); - assert!(rel(a, want_area) < 1e-9, "{name}: C++ area {a} vs analytic {want_area}"); - } } } @@ -553,20 +471,6 @@ fn sweep_placed_reports_the_motions() { check_motions(&turned, &quarter_turn()); } -/// The same motions on the C++ kernel. -#[cfg(feature = "native")] -#[test] -fn the_cpp_kernel_places_the_profile_alike() { - let _cpp = crate::route::on_cpp(); - crate::sweeps::ON_CPP.with(|c| c.set(true)); - let (p, up) = straight_up(); - let (q, arc) = round_z(); - let placed = crate::Brep::sweep_placed(&p, &up, SweepFrame::Follow, true).unwrap().1; - let turned = crate::Brep::sweep_placed(&q, &arc, SweepFrame::Follow, true).unwrap().1; - crate::sweeps::ON_CPP.with(|c| c.set(false)); - check_motions(&placed, &[0.0, 5.0, 10.0].map(|z| translate([0.0, 0.0, z]))); - check_motions(&turned, &quarter_turn()); -} /// A diagonal 10 above a 10 x 10 x 10 box's top, projected down onto the top: the same diagonal /// at z = 10. @@ -669,61 +573,6 @@ fn a_box_top_section_is_the_line_across_it() { assert!((len - 10.0).abs() < 1e-6, "{len} against 10"); } -/// The same sections on the C++ kernel: the lofted wall's line along the same points. On a box's -/// top and a cylinder's side, whose analytic intersectors make no UV curves, the capi leaves -/// `bForceUVClassify` FALSE and answers [`Error::Empty`] (solidcore#1769), where the port has the -/// section. -#[cfg(feature = "native")] -#[test] -fn the_cpp_kernel_sections_a_face_alike() { - let _cpp = crate::route::on_cpp(); - use crate::{Brep, ffi}; - type Case = (&'static str, Box Body>, Brep, [[f64; 3]; 3]); - let cases: [Case; 3] = [ - ( - "loft", - Box::new(|| loft(&square_frustum(), 1, true).unwrap()), - { - crate::sweeps::ON_CPP.with(|c| c.set(true)); - let b = Brep::loft(&square_frustum(), 1, true).unwrap(); - crate::sweeps::ON_CPP.with(|c| c.set(false)); - b - }, - [[5.0, 1.0, 5.0], [0.0, 0.0, 5.0], [0.0, 0.0, 1.0]], - ), - ( - "box", - Box::new(|| make_box([0.0; 3], [10.0; 3]).unwrap()), - ffi::make_box(&[0.0; 3], &[10.0; 3]).map(Brep::made).unwrap(), - [[5.0, 5.0, 10.0], [5.0, 5.0, 5.0], [1.0, 0.0, 0.0]], - ), - ( - "cylinder", - Box::new(|| make_cylinder([0.0; 3], 5.0, 10.0).unwrap()), - ffi::make_cylinder(&[0.0; 3], 5.0, 10.0).map(Brep::made).unwrap(), - [[5.0, 0.0, 5.0], [0.0, 0.0, 3.0], [0.0, 0.0, 1.0]], - ), - ]; - for (name, core, cpp, [face, through, n]) in cases { - crate::sweeps::ON_CPP.with(|c| c.set(true)); - let want = cpp.face_section(face, 0.01, through, n, 1e-6); - crate::sweeps::ON_CPP.with(|c| c.set(false)); - let got = face_section(&core(), face, 0.01, through, n, 1e-6); - let (got, want) = match (got, want) { - (Ok(g), Ok(w)) => (g, w), - (Ok(g), Err(Error::Empty)) if name != "loft" => { - assert!(!g.is_empty(), "{name}: the port has the section the capi drops"); - continue; - } - (g, w) => panic!("{name}: {:?} against {:?}", g.err(), w.err()), - }; - assert_eq!(got.len(), want.len(), "{name}: {got:?} against {want:?}"); - let (g, w) = (samples(&got, 64), samples(&want, 64)); - for (a, b) in g.iter().flatten().zip(w.iter().flatten()) { - assert!(a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-9), "{name}: {a:?} against {b:?}"); - } - } -} /// The capi's argument checks, before any kernel call. #[test] diff --git a/crates/smlib/src/core/transform_tests.rs b/crates/smlib/src/core/transform_tests.rs index afc373d890..ac4d216b44 100644 --- a/crates/smlib/src/core/transform_tests.rs +++ b/crates/smlib/src/core/transform_tests.rs @@ -1,8 +1,6 @@ //! Copies and affine transforms on solidcore, against closed forms: every result keeps its counts, //! its volume scales by the matrix's |det|, its bounds are the moved box's, and it exports to a -//! record that validates. With SMLib built, the same case on the C++ kernel gives the same -//! volume, counts, bounds and faces by surface kind, or the same error (parity only: the -//! expected values are the analytic ones). +//! record that validates. use std::collections::BTreeMap; use std::f64::consts::PI; @@ -176,43 +174,7 @@ fn solidcore_case(name: &str) -> Got { judge(name, "solidcore", got) } -/// The case on the C++ kernel, called directly (whatever the choice). -#[cfg(feature = "native")] -fn cpp_case(name: &str) -> Got { - use crate::{Brep, error, ffi}; - crate::init().unwrap(); - let c = case(name); - let body = match c.shape { - Shape::Box(o, s) => ffi::make_box(&o, &s).unwrap(), - Shape::Cylinder(o, r, h) => ffi::make_cylinder(&o, r, h).unwrap(), - }; - let got = ffi::transform(&body, &c.matrix).map_err(error).map(|h| { - let volume = ffi::volume(&h, 1e-8).unwrap(); - let n = ffi::counts(&h).unwrap().map(|x| usize::try_from(x).unwrap()); - let bx = ffi::measure::bounds(&h).unwrap(); - let (rec, _) = native(&Brep::made(h).cpp_arrays().unwrap()).unwrap(); - rec.validate().unwrap(); - Measured { volume, bounds: bx, counts: n, kinds: kinds(&rec) } - }); - judge(name, "C++", got) -} -/// solidcore and C++ agree: volume within 1e-9, the same counts, bounds and faces by kind, or the -/// same error. -#[cfg(feature = "native")] -fn parity(name: &str) { - let (ours, cpp) = (solidcore_case(name), cpp_case(name)); - match (ours, cpp) { - (Ok(a), Ok(b)) => { - assert!(rel(a.volume, b.volume) < 1e-9, "{name}: solidcore {} vs C++ {}", a.volume, b.volume); - assert_eq!(a.counts, b.counts, "{name}: counts (solidcore vs C++)"); - assert_eq!(a.kinds, b.kinds, "{name}: faces by surface kind (solidcore vs C++)"); - let near = |x: [f64; 3], y: [f64; 3]| x.iter().zip(&y).all(|(p, q)| (p - q).abs() < 1e-6); - assert!(near(a.bounds.0, b.bounds.0) && near(a.bounds.1, b.bounds.1), "{name}: bounds {:?} vs C++ {:?}", a.bounds, b.bounds); - } - (a, b) => assert_eq!(a.err(), b.err(), "{name}: errors (solidcore vs C++)"), - } -} macro_rules! cases { ($($m:ident $(: $ign:meta)?),* $(,)?) => {$( @@ -222,19 +184,6 @@ macro_rules! cases { fn solidcore_matches_the_closed_form() { let _ = super::solidcore_case(stringify!($m)); } - #[cfg(feature = "native")] - #[test] - $(#[$ign])? - fn solidcore_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - super::parity(stringify!($m)); - } - #[cfg(feature = "native")] - #[test] - fn the_cpp_kernel_matches_the_closed_form() { - let _cpp = crate::route::on_cpp(); - let _ = super::cpp_case(stringify!($m)); - } } )*}; } @@ -292,37 +241,3 @@ fn the_body_stays_as_it_was() { } } -/// Face and edge tags (`Brep::tagged`) through a copy and a transform on solidcore, as on -/// C++: the box is tagged on the C++ kernel, its solidcore body converted from that and tagged the -/// same way, and the two results' histories must be the same. -#[cfg(feature = "native")] -mod history { - use crate::{Brep, ffi}; - - fn tagged_box() -> Brep { - crate::init().unwrap(); - Brep::made(ffi::make_box(&[0.0; 3], &[20.0, 30.0, 10.0]).unwrap()).tagged(0).unwrap() - } - - #[test] - fn tags_cross_a_copy() { - let _cpp = crate::route::on_cpp(); - let a = tagged_box(); - let cpp = Brep::made(ffi::copy(a.cpp().unwrap()).unwrap()).history().unwrap(); - let core = Brep::made_core(super::copy(a.core().unwrap()).unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical()); - } - - #[test] - fn tags_cross_a_mirror_and_a_shear() { - let _cpp = crate::route::on_cpp(); - let a = tagged_box(); - let mirror = [[-1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0; 4]]; - let shear = [[1.0, 0.5, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [2.0, 3.0, 4.0, 1.0]]; - for m in [mirror, shear] { - let cpp = Brep::made(ffi::transform(a.cpp().unwrap(), &m).unwrap()).history().unwrap(); - let core = Brep::made_core(super::transform(a.core().unwrap(), &m).unwrap()).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical(), "{m:?}"); - } - } -} diff --git a/crates/smlib/src/core/trim_tests.rs b/crates/smlib/src/core/trim_tests.rs index 54f1077ba2..b04e2241a8 100644 --- a/crates/smlib/src/core/trim_tests.rs +++ b/crates/smlib/src/core/trim_tests.rs @@ -1,6 +1,5 @@ -//! Untrim and extend on solidcore, against closed forms: areas and counts, and every result exports to a -//! record that validates. With SMLib built, the same input converted to the C++ kernel gives the -//! same result or the same error (parity only: the expected values are the analytic ones). +//! Untrim and extend on solidcore, against closed forms: areas and counts, and every result exports +//! to a record that validates. //! //! An untrim's body is a 20 x 20 x 5 plate with a 3 mm through hole at (10, 10), made on //! solidcore; its top face is a square with a round hole. An extend's is a one-face sheet that is @@ -133,38 +132,7 @@ fn solidcore_case(name: &str) -> Got { judge(name, "solidcore", got) } -/// The case on the C++ kernel, called directly (whatever the choice), on the plate converted -/// from solidcore's. -#[cfg(feature = "native")] -fn cpp_case(name: &str) -> Got { - use crate::{Brep, error, ffi}; - crate::init().unwrap(); - let (c, _) = case(name); - let body = Brep::made_core(c.input.core()); - let r = ffi::trim::untrim_face(body.cpp().unwrap(), &c.point, TOL, c.holes, c.outer, c.in_place).map_err(|s| match s { - ffi::ops::ERR_PICK_NONE => Error::NoMatch { point: 0 }, - s => error(s), - }); - let got = r.map(|h| { - let a = ffi::measure::area(&h, 1e-8).unwrap(); - let n = ffi::counts(&h).unwrap().map(|x| usize::try_from(x).unwrap()); - let (rec, _) = native(&Brep::made(h).cpp_arrays().unwrap()).unwrap(); - rec.validate().unwrap(); - Measured { area: a, counts: n } - }); - judge(name, "C++", got) -} -#[cfg(feature = "native")] -fn parity(name: &str) { - match (solidcore_case(name), cpp_case(name)) { - (Ok(a), Ok(b)) => { - assert!(rel(a.area, b.area) < 1e-9, "{name}: solidcore {} vs C++ {}", a.area, b.area); - assert_eq!(a.counts, b.counts, "{name}: counts (solidcore vs C++)"); - } - (a, b) => assert_eq!(a.err(), b.err(), "{name}: errors (solidcore vs C++)"), - } -} macro_rules! cases { ($($m:ident $(: $ign:meta)?),* $(,)?) => {$( @@ -174,19 +142,6 @@ macro_rules! cases { fn solidcore_matches_the_closed_form() { let _ = super::solidcore_case(stringify!($m)); } - #[cfg(feature = "native")] - #[test] - $(#[$ign])? - fn solidcore_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - super::parity(stringify!($m)); - } - #[cfg(feature = "native")] - #[test] - fn the_cpp_kernel_matches_the_closed_form() { - let _cpp = crate::route::on_cpp(); - let _ = super::cpp_case(stringify!($m)); - } } )*}; } @@ -230,25 +185,6 @@ fn the_body_stays_as_it_was() { assert_eq!(counts(&b).unwrap(), was); } -/// Face and edge tags (`Brep::tagged`) through an untrim on solidcore, as on C++. -#[cfg(feature = "native")] -mod history { - use crate::{Brep, ffi}; - - #[test] - fn tags_cross_an_untrim() { - let _cpp = crate::route::on_cpp(); - crate::init().unwrap(); - let a = Brep::made_core(super::holed_sheet()).tagged(0).unwrap(); - for (holes, outer) in [(None, false), (Some(&[][..]), true)] { - let cpp = ffi::trim::untrim_face(a.cpp().unwrap(), &super::SHEET, super::TOL, holes, outer, true).unwrap(); - let cpp = Brep::made(cpp).history().unwrap(); - let core = super::untrim_face(a.core().unwrap(), super::SHEET, super::TOL, holes, outer, true).unwrap(); - let core = Brep::made_core(core).history().unwrap(); - assert_eq!(core.canonical(), cpp.canonical(), "holes {holes:?}, outer {outer}"); - } - } -} /// A one-face sheet on `surface` over `domain`, the face its whole box. fn whole_face(surface: solidcore::surface::surface::Surface) -> Body { @@ -339,41 +275,7 @@ fn extend_solidcore(name: &str) -> Got { extend_judge(name, "solidcore", got) } -#[cfg(feature = "native")] -fn extend_cpp(name: &str) -> Got { - use crate::{Brep, error, ffi}; - crate::init().unwrap(); - let (sheet, point, distance, how, _) = extend_case(name); - let mode = match how { - Continuation::Natural => ffi::trim::EXTEND_NATURAL, - Continuation::Tangent => ffi::trim::EXTEND_TANGENT, - Continuation::Curvature => ffi::trim::EXTEND_CURVATURE, - }; - let body = Brep::made_core(sheet.core()); - let r = ffi::trim::extend_sheet(body.cpp().unwrap(), &point, TOL, distance, mode).map_err(|s| match s { - ffi::ops::ERR_PICK_NONE => Error::NoMatch { point: 0 }, - s => error(s), - }); - let got = r.map(|h| { - let a = ffi::measure::area(&h, 1e-8).unwrap(); - let n = ffi::counts(&h).unwrap().map(|x| usize::try_from(x).unwrap()); - let (rec, _) = native(&Brep::made(h).cpp_arrays().unwrap()).unwrap(); - rec.validate().unwrap(); - Measured { area: a, counts: n } - }); - extend_judge(name, "C++", got) -} -#[cfg(feature = "native")] -fn extend_parity(name: &str) { - match (extend_solidcore(name), extend_cpp(name)) { - (Ok(a), Ok(b)) => { - assert!(rel(a.area, b.area) < 1e-9, "{name}: solidcore {} vs C++ {}", a.area, b.area); - assert_eq!(a.counts, b.counts, "{name}: counts (solidcore vs C++)"); - } - (a, b) => assert_eq!(a.err(), b.err(), "{name}: errors (solidcore vs C++)"), - } -} macro_rules! extend_cases { ($($m:ident $(: $ign:meta)?),* $(,)?) => {$( @@ -383,19 +285,6 @@ macro_rules! extend_cases { fn solidcore_matches_the_closed_form() { let _ = super::extend_solidcore(stringify!($m)); } - #[cfg(feature = "native")] - #[test] - $(#[$ign])? - fn solidcore_matches_the_cpp_kernel() { - let _cpp = crate::route::on_cpp(); - super::extend_parity(stringify!($m)); - } - #[cfg(feature = "native")] - #[test] - fn the_cpp_kernel_matches_the_closed_form() { - let _cpp = crate::route::on_cpp(); - let _ = super::extend_cpp(stringify!($m)); - } } )*}; } diff --git a/crates/smlib/src/export.rs b/crates/smlib/src/export.rs index bf085c6d47..3d17583fcb 100644 --- a/crates/smlib/src/export.rs +++ b/crates/smlib/src/export.rs @@ -45,67 +45,33 @@ use solidcraft_geom::brep as b; use crate::flat::{self, *}; use crate::history::{Order, Piece}; use crate::import::Arrays; -use crate::{Brep, Result, error, ffi, init}; +use crate::{Brep, Result}; impl Brep { /// A cylinder about +Z: base centre, radius and height (mm, > 0). pub fn make_cylinder(origin: [f64; 3], radius: f64, height: f64) -> Result { - if crate::route::backend() == crate::route::Backend::Solidcore { - return crate::core::make_cylinder(origin, radius, height).map(Brep::made_core); - } - init()?; - ffi::make_cylinder(&origin, radius, height).map(Brep::made).map_err(error) + crate::core::make_cylinder(origin, radius, height).map(Brep::made_core) } /// A sphere: centre and radius (mm, > 0). pub fn make_sphere(center: [f64; 3], radius: f64) -> Result { - if crate::route::backend() == crate::route::Backend::Solidcore { - return crate::core::make_sphere(center, radius).map(Brep::made_core); - } - init()?; - ffi::make_sphere(¢er, radius).map(Brep::made).map_err(error) + crate::core::make_sphere(center, radius).map(Brep::made_core) } /// A cone or frustum about +Z: base centre, base radius (> 0), top radius (≥ 0, not the base /// radius) and height (> 0), in mm. pub fn make_cone(origin: [f64; 3], base_radius: f64, top_radius: f64, height: f64) -> Result { - if crate::route::backend() == crate::route::Backend::Solidcore { - return crate::core::make_cone(origin, base_radius, top_radius, height).map(Brep::made_core); - } - init()?; - ffi::make_cone(&origin, base_radius, top_radius, height).map(Brep::made).map_err(error) + crate::core::make_cone(origin, base_radius, top_radius, height).map(Brep::made_core) } /// A ring torus about +Z: centre, major radius > minor radius > 0 (mm). pub fn make_torus(center: [f64; 3], major_radius: f64, minor_radius: f64) -> Result { - if crate::route::backend() == crate::route::Backend::Solidcore { - return crate::core::make_torus(center, major_radius, minor_radius).map(Brep::made_core); - } - init()?; - ffi::make_torus(¢er, major_radius, minor_radius).map(Brep::made).map_err(error) + crate::core::make_torus(center, major_radius, minor_radius).map(Brep::made_core) } - /// The flat arrays of this body that the mapping reads, from the kernel in force - /// ([`crate::route::backend`]). + /// The flat arrays of this body that the mapping reads. pub fn arrays(&self) -> Result { - match crate::route::backend() { - crate::route::Backend::Smlib => self.cpp_arrays(), - crate::route::Backend::Solidcore => crate::core::to_arrays(self.core()?), - } - } - - /// The flat arrays of the C++ kernel's body ([`Brep::export`]), every one copied out: the - /// mapping reads some, solidcore's import ([`crate::core::from_arrays`]) all of them. - pub(crate) fn cpp_arrays(&self) -> Result { - let f = self.export()?; - let mut out = Arrays::default(); - for id in REGION_SHELL_COUNT..=FACE_DEFINITION_REAL_COUNT { - out.ints.insert(id, f.get_i64(id)?); - } - for id in TOLERANCE..=FACE_DEFINITION_REALS { - out.reals.insert(id, f.get_f64(id)?); - } - Ok(out) + crate::core::to_arrays(self.core()) } /// This body as a schema 4 B-rep, validated. diff --git a/crates/smlib/src/export/tests.rs b/crates/smlib/src/export/tests.rs index 0b767d090a..5e14d5bf98 100644 --- a/crates/smlib/src/export/tests.rs +++ b/crates/smlib/src/export/tests.rs @@ -1,7 +1,5 @@ -#[cfg(feature = "native")] use super::*; -#[cfg(feature = "native")] mod native_tests { use super::*; use crate::{CheckLevel, MEASURE_ACCURACY}; diff --git a/crates/smlib/src/ffi.rs b/crates/smlib/src/ffi.rs deleted file mode 100644 index 477b103eef..0000000000 --- a/crates/smlib/src/ffi.rs +++ /dev/null @@ -1,405 +0,0 @@ -//! The C API of `capi/include/smlib.h` (tangency-labs/smlib), declared by hand. The only place in -//! the crate with `unsafe`: every function here is called through the safe wrappers in the crate -//! root, which check their arguments and own the handles. -#![allow(unsafe_code)] - -#[cfg(feature = "native")] -use std::ffi::{c_char, c_void}; - -/// `SMLIB_ABI_VERSION` this crate was written against. -pub const ABI_VERSION: i32 = 6; - -pub const OK: i32 = 0; -pub const ERR_EXCEPTION: i32 = -1000; -pub const ERR_BUFFER: i32 = -1001; -pub const ERR_ARG: i32 = -1002; -pub const ERR_EMPTY: i32 = -1003; - -/// `smlib_brep`, opaque. -#[repr(C)] -pub struct RawBrep { - _private: [u8; 0], -} - -#[cfg(feature = "native")] -pub type LogFn = extern "C" fn(user: *mut c_void, level: i32, line: *const c_char); - -#[cfg(feature = "native")] -unsafe extern "C" { - pub fn smlib_abi_version() -> i32; - pub fn smlib_init() -> i32; - pub fn smlib_set_log(log: Option, user: *mut c_void); - pub fn smlib_set_dump_dir(dir: *const c_char) -> i32; - pub fn smlib_current_call(buf: *mut c_char, cap: i64) -> i64; - pub fn smlib_brep_read(path: *const c_char, ascii: i32, out: *mut *mut RawBrep) -> i32; - pub fn smlib_brep_write(brep: *const RawBrep, path: *const c_char, ascii: i32) -> i32; - pub fn smlib_brep_free(brep: *mut RawBrep); - pub fn smlib_brep_copy(brep: *const RawBrep, out: *mut *mut RawBrep) -> i32; - pub fn smlib_brep_transform(brep: *const RawBrep, matrix: *const [f64; 4], out: *mut *mut RawBrep) -> i32; - pub fn smlib_make_box(origin: *const f64, size: *const f64, out: *mut *mut RawBrep) -> i32; - pub fn smlib_brep_volume(brep: *const RawBrep, relative_accuracy: f64, out: *mut f64) -> i32; - pub fn smlib_brep_counts(brep: *const RawBrep, out: *mut i64) -> i32; -} - -/// Returned by every call when the crate was built without the `native` feature. -pub const NOT_BUILT: i32 = -2000; - -/// An owned `smlib_brep`. Only made here, from a pointer the C API returned; freed once on drop. -#[cfg_attr(not(feature = "native"), allow(dead_code))] -pub struct Handle(std::ptr::NonNull); - -// SMLib serialises every call behind its own lock, and a handle is only ever used through it. -unsafe impl Send for Handle {} -unsafe impl Sync for Handle {} - -impl Drop for Handle { - fn drop(&mut self) { - #[cfg(feature = "native")] - unsafe { - smlib_brep_free(self.0.as_ptr()) - }; - } -} - -impl Handle { - #[cfg_attr(not(feature = "native"), allow(dead_code))] - fn raw(&self) -> *const RawBrep { - self.0.as_ptr() - } -} - -/// Runs a call that returns a new B-rep through `out`. -#[cfg(feature = "native")] -fn new_brep(f: impl FnOnce(*mut *mut RawBrep) -> i32) -> Result { - let mut out: *mut RawBrep = std::ptr::null_mut(); - let s = f(&mut out); - match (s, std::ptr::NonNull::new(out)) { - (OK, Some(p)) => Ok(Handle(p)), - (OK, None) => Err(ERR_EMPTY), - (s, _) => Err(s), - } -} - -#[cfg(feature = "native")] -mod calls { - use super::*; - use std::ffi::CStr; - - pub fn abi_version() -> i32 { - unsafe { smlib_abi_version() } - } - pub fn init() -> i32 { - unsafe { smlib_init() } - } - /// Routes SMLib's log lines to `sink` (`None`: stops them). - pub fn set_log(sink: Option) { - let log: Option = match sink { - Some(sink) => { - // The first sink stays: init sets it once, and lines keep a single destination. - let _ = SINK.set(sink); - Some(trampoline) - } - None => None, - }; - unsafe { smlib_set_log(log, std::ptr::null_mut()) } - } - pub fn set_dump_dir(dir: Option<&CStr>) -> i32 { - unsafe { smlib_set_dump_dir(dir.map_or(std::ptr::null(), CStr::as_ptr)) } - } - pub fn current_call(buf: &mut [u8]) -> usize { - let cap = i64::try_from(buf.len()).unwrap_or(i64::MAX); - let n = unsafe { smlib_current_call(buf.as_mut_ptr().cast(), cap) }; - usize::try_from(n).unwrap_or(0).min(buf.len().saturating_sub(1)) - } - pub fn read(path: &CStr, ascii: bool) -> Result { - new_brep(|out| unsafe { smlib_brep_read(path.as_ptr(), i32::from(ascii), out) }) - } - pub fn write(b: &Handle, path: &CStr, ascii: bool) -> i32 { - unsafe { smlib_brep_write(b.raw(), path.as_ptr(), i32::from(ascii)) } - } - pub fn copy(b: &Handle) -> Result { - new_brep(|out| unsafe { smlib_brep_copy(b.raw(), out) }) - } - pub fn transform(b: &Handle, matrix: &[[f64; 4]; 4]) -> Result { - new_brep(|out| unsafe { smlib_brep_transform(b.raw(), matrix.as_ptr(), out) }) - } - pub fn make_box(origin: &[f64; 3], size: &[f64; 3]) -> Result { - new_brep(|out| unsafe { smlib_make_box(origin.as_ptr(), size.as_ptr(), out) }) - } - pub fn volume(b: &Handle, relative_accuracy: f64) -> Result { - let mut v = 0.0; - match unsafe { smlib_brep_volume(b.raw(), relative_accuracy, &mut v) } { - OK => Ok(v), - s => Err(s), - } - } - pub fn counts(b: &Handle) -> Result<[i64; 3], i32> { - let mut c = [0i64; 3]; - match unsafe { smlib_brep_counts(b.raw(), c.as_mut_ptr()) } { - OK => Ok(c), - 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 std::ffi::CStr; - - pub fn abi_version() -> i32 { - NOT_BUILT - } - pub fn init() -> i32 { - NOT_BUILT - } - pub fn set_log(_: Option) {} - pub fn set_dump_dir(_: Option<&CStr>) -> i32 { - NOT_BUILT - } - pub fn current_call(_: &mut [u8]) -> usize { - 0 - } - pub fn read(_: &CStr, _: bool) -> Result { - Err(NOT_BUILT) - } - pub fn write(_: &Handle, _: &CStr, _: bool) -> i32 { - NOT_BUILT - } - pub fn copy(_: &Handle) -> Result { - Err(NOT_BUILT) - } - pub fn transform(_: &Handle, _: &[[f64; 4]; 4]) -> Result { - Err(NOT_BUILT) - } - pub fn make_box(_: &[f64; 3], _: &[f64; 3]) -> Result { - Err(NOT_BUILT) - } - pub fn volume(_: &Handle, _: f64) -> Result { - Err(NOT_BUILT) - } - pub fn counts(_: &Handle) -> Result<[i64; 3], i32> { - Err(NOT_BUILT) - } -} - -pub use calls::*; - -pub mod check; -pub mod flat; -pub mod history; -pub mod measure; -pub mod ops; -pub mod sheets; -pub mod split; -pub mod ssi; -pub mod sweeps; -pub mod trim; - -/// Where [`set_log`] sends SMLib's log lines. -#[cfg(feature = "native")] -static SINK: std::sync::OnceLock = std::sync::OnceLock::new(); - -/// The log callback the C API calls: `line` is NUL-terminated and valid for the call's duration -/// (smlib.h), so it is read here, inside the one module that touches raw pointers. -#[cfg(feature = "native")] -extern "C" fn trampoline(_: *mut c_void, level: i32, line: *const c_char) { - let text = if line.is_null() { String::new() } else { unsafe { std::ffi::CStr::from_ptr(line) }.to_string_lossy().into_owned() }; - if let Some(sink) = SINK.get() { - sink(level, text); - } -} - -// ---- Primitives (smlib_primitives.cpp) ----------------------------------------------------- - -#[cfg(feature = "native")] -unsafe extern "C" { - fn smlib_make_cylinder(origin: *const f64, radius: f64, height: f64, out: *mut *mut RawBrep) -> i32; - fn smlib_make_sphere(center: *const f64, radius: f64, out: *mut *mut RawBrep) -> i32; - fn smlib_make_cone(origin: *const f64, base_radius: f64, top_radius: f64, height: f64, out: *mut *mut RawBrep) -> i32; - fn smlib_make_torus(center: *const f64, major_radius: f64, minor_radius: f64, out: *mut *mut RawBrep) -> i32; -} - -#[cfg(feature = "native")] -mod primitive_calls { - use super::*; - - pub fn make_cylinder(origin: &[f64; 3], radius: f64, height: f64) -> Result { - new_brep(|out| unsafe { smlib_make_cylinder(origin.as_ptr(), radius, height, out) }) - } - pub fn make_sphere(center: &[f64; 3], radius: f64) -> Result { - new_brep(|out| unsafe { smlib_make_sphere(center.as_ptr(), radius, out) }) - } - pub fn make_cone(origin: &[f64; 3], base_radius: f64, top_radius: f64, height: f64) -> Result { - new_brep(|out| unsafe { smlib_make_cone(origin.as_ptr(), base_radius, top_radius, height, out) }) - } - pub fn make_torus(center: &[f64; 3], major_radius: f64, minor_radius: f64) -> Result { - new_brep(|out| unsafe { smlib_make_torus(center.as_ptr(), major_radius, minor_radius, out) }) - } -} - -/// The same calls when SMLib is not built: each one fails with [`NOT_BUILT`]. -#[cfg(not(feature = "native"))] -mod primitive_calls { - use super::*; - - pub fn make_cylinder(_: &[f64; 3], _: f64, _: f64) -> Result { - Err(NOT_BUILT) - } - pub fn make_sphere(_: &[f64; 3], _: f64) -> Result { - Err(NOT_BUILT) - } - pub fn make_cone(_: &[f64; 3], _: f64, _: f64, _: f64) -> Result { - Err(NOT_BUILT) - } - pub fn make_torus(_: &[f64; 3], _: f64, _: f64) -> Result { - Err(NOT_BUILT) - } -} - -pub use primitive_calls::*; - -// ---- WASI calls (wasm32-unknown-unknown) ------------------------------------------------------- - -/// In a browser build SMLib is compiled against wasi-sdk's C library (build.rs), which calls a few -/// WASI functions. The page has no WASI host, so they are defined here, where wasm-ld finds them -/// before it would leave them as imports. There is no file system or environment: those calls fail -/// with WASI's own error codes. The clocks are the page's (the C API times every call). What -/// C writes to stdout or stderr goes to the `log` crate (target `smlib`). -#[cfg(all(feature = "native", target_arch = "wasm32", target_os = "unknown"))] -mod wasi { - const SUCCESS: i32 = 0; - const BADF: i32 = 8; - const INVAL: i32 = 28; - const NOSYS: i32 = 52; - const NOTCAPABLE: i32 = 76; - - /// One `__wasi_ciovec_t`: a buffer and its length. - #[repr(C)] - struct Ciovec { - buf: *const u8, - len: usize, - } - - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_fd_write(fd: i32, iovs: *const Ciovec, n: usize, written: *mut usize) -> i32 { - if !(1..=2).contains(&fd) { - return BADF; - } - let mut text = Vec::new(); - for i in 0..n { - // libc passes `n` valid iovecs, each a valid buffer, for the call's duration. - let v = unsafe { &*iovs.add(i) }; - if !v.buf.is_null() { - text.extend_from_slice(unsafe { std::slice::from_raw_parts(v.buf, v.len) }); - } - } - for line in String::from_utf8_lossy(&text).lines().filter(|l| !l.trim().is_empty()) { - log::debug!(target: "smlib", "{line}"); - } - if !written.is_null() { - unsafe { *written = text.len() }; - } - SUCCESS - } - - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_environ_sizes_get(count: *mut usize, size: *mut usize) -> i32 { - if !count.is_null() && !size.is_null() { - unsafe { - *count = 0; - *size = 0; - } - } - SUCCESS - } - - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_environ_get(_: *mut *mut u8, _: *mut u8) -> i32 { - SUCCESS - } - - /// Clock 0 is the wall clock; the others (monotonic, process and thread time) all count from - /// the first call, through the page's monotonic clock. - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_clock_time_get(id: i32, _: i64, time: *mut u64) -> i32 { - static START: std::sync::OnceLock = std::sync::OnceLock::new(); - let d = match id { - 0 => match web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH) { - Ok(d) => d, - Err(_) => return NOSYS, - }, - 1..=3 => START.get_or_init(web_time::Instant::now).elapsed(), - _ => return INVAL, - }; - if time.is_null() { - return INVAL; - } - unsafe { *time = u64::try_from(d.as_nanos()).unwrap_or(u64::MAX) }; - SUCCESS - } - - /// No preopened directories: libc stops looking at the first descriptor that is not one. - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_fd_prestat_get(_: i32, _: *mut u8) -> i32 { - BADF - } - - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_fd_prestat_dir_name(_: i32, _: *mut u8, _: usize) -> i32 { - BADF - } - - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_fd_fdstat_get(_: i32, _: *mut u8) -> i32 { - BADF - } - - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_fd_fdstat_set_flags(_: i32, _: i32) -> i32 { - BADF - } - - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_fd_close(_: i32) -> i32 { - BADF - } - - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_fd_read(_: i32, _: *const Ciovec, _: usize, _: *mut usize) -> i32 { - BADF - } - - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_fd_seek(_: i32, _: i64, _: i32, _: *mut u64) -> i32 { - BADF - } - - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_fd_renumber(_: i32, _: i32) -> i32 { - BADF - } - - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_path_open( - _: i32, - _: i32, - _: *const u8, - _: usize, - _: i32, - _: i64, - _: i64, - _: i32, - _: *mut i32, - ) -> i32 { - NOTCAPABLE - } - - /// C's `exit` and `abort`: the C API catches everything, so this is a kernel defect; stop the - /// module as a native build would stop the process. - #[unsafe(no_mangle)] - extern "C" fn __imported_wasi_snapshot_preview1_proc_exit(_: i32) -> ! { - std::arch::wasm32::unreachable() - } -} diff --git a/crates/smlib/src/ffi/check.rs b/crates/smlib/src/ffi/check.rs deleted file mode 100644 index d285e8efbc..0000000000 --- a/crates/smlib/src/ffi/check.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! `smlib_brep_check` (the fork's `capi/src/smlib_check.cpp`), declared by hand. -//! Part of the FFI boundary, like its parent: called only through [`crate::check`]. - -#![allow(unsafe_code)] - -use super::Handle; -#[cfg(not(feature = "native"))] -use super::NOT_BUILT; -#[cfg(feature = "native")] -use super::{OK, RawBrep}; -#[cfg(feature = "native")] -use std::ffi::c_char; - -#[cfg(feature = "native")] -unsafe extern "C" { - pub fn smlib_brep_check(brep: *const RawBrep, level: i32, buf: *mut c_char, cap: i64, out_len: *mut i64, out_failures: *mut i64) -> i32; -} - -/// Runs the check into `buf`: the report's length and the failure count, or a status -/// (`ERR_BUFFER` with the needed length when `buf` is too small). -#[cfg(feature = "native")] -pub fn check(b: &Handle, level: i32, buf: &mut [u8]) -> Result<(usize, i64), (i32, usize)> { - let cap = i64::try_from(buf.len()).unwrap_or(i64::MAX); - let (mut len, mut failures) = (0i64, 0i64); - let s = unsafe { smlib_brep_check(b.raw(), level, buf.as_mut_ptr().cast(), cap, &mut len, &mut failures) }; - let len = usize::try_from(len).unwrap_or(0); - match s { - OK => Ok((len.min(buf.len()), failures)), - s => Err((s, len)), - } -} - -#[cfg(not(feature = "native"))] -pub fn check(_: &Handle, _: i32, _: &mut [u8]) -> Result<(usize, i64), (i32, usize)> { - Err((NOT_BUILT, 0)) -} diff --git a/crates/smlib/src/ffi/flat.rs b/crates/smlib/src/ffi/flat.rs deleted file mode 100644 index 8353e3bcd6..0000000000 --- a/crates/smlib/src/ffi/flat.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! 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/ffi/history.rs b/crates/smlib/src/ffi/history.rs deleted file mode 100644 index e3a0f24ac1..0000000000 --- a/crates/smlib/src/ffi/history.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! The history block of `capi/include/smlib.h` (`smlib_history_tag`, `smlib_history_read`), -//! declared by hand. Part of the FFI boundary, like its parent: called only through -//! [`crate::history`]. -#![allow(unsafe_code)] - -use super::Handle; -#[cfg(feature = "native")] -use super::{ERR_BUFFER, OK, RawBrep, new_brep}; - -/// `SMLIB_HISTORY_VERSION` this crate reads. -pub const VERSION: i64 = 1; - -#[cfg(feature = "native")] -unsafe extern "C" { - fn smlib_history_tag(brep: *const RawBrep, operand: i32, out: *mut *mut RawBrep) -> i32; - fn smlib_history_read(result: *const RawBrep, buf: *mut i64, cap: i64, out_len: *mut i64) -> i32; -} - -#[cfg(feature = "native")] -pub fn tag(b: &Handle, operand: i32) -> Result { - new_brep(|out| unsafe { smlib_history_tag(b.raw(), operand, out) }) -} - -/// The result's history records, sized by asking first. -#[cfg(feature = "native")] -pub fn read(b: &Handle) -> Result, i32> { - let mut len = 0i64; - let mut buf: Vec = Vec::new(); - loop { - let cap = i64::try_from(buf.len()).map_err(|_| ERR_BUFFER)?; - let ptr = if buf.is_empty() { std::ptr::null_mut() } else { buf.as_mut_ptr() }; - match unsafe { smlib_history_read(b.raw(), ptr, cap, &mut len) } { - OK => { - buf.truncate(usize::try_from(len).map_err(|_| ERR_BUFFER)?); - return Ok(buf); - } - // Asked again with room for what it said; a second shortfall would mean the body - // changed, which a handle never does. - ERR_BUFFER if buf.is_empty() => buf.resize(usize::try_from(len).map_err(|_| ERR_BUFFER)?, 0), - s => return Err(s), - } - } -} - -#[cfg(not(feature = "native"))] -pub fn tag(_: &Handle, _: i32) -> Result { - Err(super::NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn read(_: &Handle) -> Result, i32> { - Err(super::NOT_BUILT) -} diff --git a/crates/smlib/src/ffi/measure.rs b/crates/smlib/src/ffi/measure.rs deleted file mode 100644 index c178e84b00..0000000000 --- a/crates/smlib/src/ffi/measure.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! `smlib_brep_mass_properties`, `smlib_brep_area`, `smlib_brep_bounds`, -//! `smlib_brep_edge_face_distance` and `smlib_brep_classify_point` (the fork's `capi/src/smlib_measure.cpp`), declared by -//! hand. Part of the FFI boundary, like its parent: called only through [`crate::measure`]. - -#![allow(unsafe_code)] - -use super::Handle; -#[cfg(not(feature = "native"))] -use super::NOT_BUILT; -#[cfg(feature = "native")] -use super::{OK, RawBrep}; - -#[cfg(feature = "native")] -unsafe extern "C" { - pub fn smlib_brep_mass_properties(brep: *const RawBrep, relative_accuracy: f64, out: *mut f64) -> i32; - pub fn smlib_brep_area(brep: *const RawBrep, relative_accuracy: f64, out: *mut f64) -> i32; - pub fn smlib_brep_bounds(brep: *const RawBrep, lo: *mut f64, hi: *mut f64) -> i32; - pub fn smlib_brep_edge_face_distance(brep: *const RawBrep, edge: i64, face: i64, out: *mut f64) -> i32; - pub fn smlib_brep_classify_point(brep: *const RawBrep, x: f64, y: f64, z: f64, out: *mut i32) -> i32; -} - -/// `SMLIB_POINT_OUTSIDE`, `SMLIB_POINT_INSIDE` and `SMLIB_POINT_ON_BOUNDARY`. -pub const POINT_OUTSIDE: i32 = 0; -pub const POINT_INSIDE: i32 = 1; -pub const POINT_ON_BOUNDARY: i32 = 2; - -#[cfg(feature = "native")] -pub fn mass_properties(b: &Handle, relative_accuracy: f64) -> Result<[f64; 11], i32> { - let mut m = [0.0; 11]; - match unsafe { smlib_brep_mass_properties(b.raw(), relative_accuracy, m.as_mut_ptr()) } { - OK => Ok(m), - s => Err(s), - } -} - -#[cfg(feature = "native")] -pub fn area(b: &Handle, relative_accuracy: f64) -> Result { - let mut a = 0.0; - match unsafe { smlib_brep_area(b.raw(), relative_accuracy, &mut a) } { - OK => Ok(a), - s => Err(s), - } -} - -#[cfg(feature = "native")] -pub fn bounds(b: &Handle) -> Result<([f64; 3], [f64; 3]), i32> { - let (mut lo, mut hi) = ([0.0; 3], [0.0; 3]); - match unsafe { smlib_brep_bounds(b.raw(), lo.as_mut_ptr(), hi.as_mut_ptr()) } { - OK => Ok((lo, hi)), - s => Err(s), - } -} - -#[cfg(feature = "native")] -pub fn edge_face_distance(b: &Handle, edge: i64, face: i64) -> Result { - let mut d = 0.0; - match unsafe { smlib_brep_edge_face_distance(b.raw(), edge, face, &mut d) } { - OK => Ok(d), - s => Err(s), - } -} - -#[cfg(feature = "native")] -pub fn classify_point(b: &Handle, p: [f64; 3]) -> Result { - let mut c = -1; - match unsafe { smlib_brep_classify_point(b.raw(), p[0], p[1], p[2], &mut c) } { - OK => Ok(c), - s => Err(s), - } -} - -#[cfg(not(feature = "native"))] -pub fn classify_point(_: &Handle, _: [f64; 3]) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn edge_face_distance(_: &Handle, _: i64, _: i64) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn bounds(_: &Handle) -> Result<([f64; 3], [f64; 3]), i32> { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn mass_properties(_: &Handle, _: f64) -> Result<[f64; 11], i32> { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn area(_: &Handle, _: f64) -> Result { - Err(NOT_BUILT) -} diff --git a/crates/smlib/src/ffi/ops.rs b/crates/smlib/src/ffi/ops.rs deleted file mode 100644 index 6cdc07c44b..0000000000 --- a/crates/smlib/src/ffi/ops.rs +++ /dev/null @@ -1,563 +0,0 @@ -//! The operations block of `capi/include/smlib.h` (`smlib_boolean`, `smlib_boolean_union_all`, `smlib_boolean2d`, `smlib_fillet`, -//! `smlib_chamfer`, `smlib_blend`, `smlib_blend_dropping`, `smlib_blend_setback`, `smlib_blend_setback_open`, `smlib_blend_fit_size`, `smlib_fillet_variable`, `smlib_chamfer_sides`, `smlib_shell`, `smlib_offset_faces`, -//! `smlib_thicken`, `smlib_draft`, `smlib_merge_faces`, `smlib_brep_solid_count`, `smlib_imprint`; the fork's `capi/src/smlib_ops.cpp`), declared by -//! hand. Part of the FFI boundary, like its parent: called only through -//! [`crate::ops`]. - -#![allow(unsafe_code)] - -use super::Handle; -#[cfg(not(feature = "native"))] -use super::NOT_BUILT; -#[cfg(feature = "native")] -use super::{ERR_ARG, ERR_BUFFER, OK, RawBrep, new_brep}; - -pub const ERR_NOT_SOLID: i32 = -1100; -pub const ERR_PICK_NONE: i32 = -1101; -pub const ERR_PICK_AMBIGUOUS: i32 = -1102; -pub const ERR_NO_ROOM: i32 = -1103; -pub const ERR_NO_CUT: i32 = -1104; -pub const ERR_UNSUPPORTED: i32 = -1105; -pub const ERR_NO_EFFECT: i32 = -1107; -pub const ERR_WALLS_TOUCH: i32 = -1110; -pub const ERR_CORNER_UNSUPPORTED: i32 = -1111; - -pub const BOOLEAN_UNION: i32 = 0; -pub const BOOLEAN_SUBTRACT: i32 = 1; -pub const BOOLEAN_INTERSECT: i32 = 2; - -pub const BLEND_ROUND: i32 = 0; -pub const BLEND_CURVATURE: i32 = 1; -pub const BLEND_CHORD: i32 = 2; -pub const BLEND_CHAMFER: i32 = 3; - -pub const FIT_SIZE: i32 = 0; -pub const FIT_SETBACK: i32 = 1; -pub const FIT_SCALE: i32 = 2; -pub const FIT_SETBACK_AT_LEAST: i32 = 3; - -/// Which selection operation: the C functions share one signature. -#[derive(Clone, Copy)] -pub enum Pick { - Fillet, - Chamfer, - Shell, - OffsetFaces, -} - -/// A failed selection operation: the status, and the index of the point that selected nothing -/// or several things (only for `ERR_PICK_*`). -pub type PickError = (i32, Option); - -/// A failed blend with corner setbacks: the status, the index of the edge point that selected -/// nothing or several edges (only for `ERR_PICK_*`), and the index of the corner point whose -/// corner SMLib refused to set back (only for `ERR_UNSUPPORTED`). -pub type SetbackError = (i32, Option, Option); - -/// The [`SetbackError`] of status `s`, given what the C call wrote to its failed indices. -#[cfg(feature = "native")] -fn setback_error(s: i32, failed: i64, failed_corner: i64) -> SetbackError { - let at = (s == ERR_PICK_NONE || s == ERR_PICK_AMBIGUOUS).then(|| usize::try_from(failed).ok()).flatten(); - let corner = (s == ERR_UNSUPPORTED).then(|| usize::try_from(failed_corner).ok()).flatten(); - (s, at, corner) -} - -#[cfg(feature = "native")] -type PickFn = unsafe extern "C" fn(*const RawBrep, *const f64, i64, f64, f64, *mut *mut RawBrep, *mut i64) -> i32; - -#[cfg(feature = "native")] -unsafe extern "C" { - fn smlib_boolean(a: *const RawBrep, b: *const RawBrep, op: i32, out: *mut *mut RawBrep) -> i32; - fn smlib_boolean_union_all(breps: *const *const RawBrep, n: i64, out: *mut *mut RawBrep) -> i32; - fn smlib_boolean2d(a: *const RawBrep, b: *const RawBrep, op: i32, out: *mut *mut RawBrep) -> i32; - fn smlib_brep_solid_count(brep: *const RawBrep, out: *mut i64) -> i32; - fn smlib_imprint(body: *const RawBrep, tool: *const RawBrep, out: *mut *mut RawBrep) -> i32; - fn smlib_fillet( - brep: *const RawBrep, - points: *const f64, - n_points: i64, - tolerance: f64, - radius: f64, - out: *mut *mut RawBrep, - out_failed_point: *mut i64, - ) -> i32; - fn smlib_chamfer( - brep: *const RawBrep, - points: *const f64, - n_points: i64, - tolerance: f64, - distance: f64, - out: *mut *mut RawBrep, - out_failed_point: *mut i64, - ) -> i32; - fn smlib_blend( - brep: *const RawBrep, - kind: i32, - points: *const f64, - sizes: *const f64, - n_points: i64, - tolerance: f64, - out: *mut *mut RawBrep, - out_failed_point: *mut i64, - ) -> i32; - fn smlib_blend_fit_size(out_size: *mut f64, out_what: *mut i32) -> i32; - fn smlib_blend_dropping( - brep: *const RawBrep, - kind: i32, - points: *const f64, - sizes: *const f64, - n_points: i64, - tolerance: f64, - out: *mut *mut RawBrep, - out_failed_point: *mut i64, - out_dropped: *mut i64, - ) -> i32; - fn smlib_blend_setback( - brep: *const RawBrep, - kind: i32, - points: *const f64, - sizes: *const f64, - n_points: i64, - corners: *const f64, - setbacks: *const f64, - n_corners: i64, - tolerance: f64, - out: *mut *mut RawBrep, - out_failed_point: *mut i64, - out_failed_corner: *mut i64, - ) -> i32; - fn smlib_blend_setback_open( - brep: *const RawBrep, - kind: i32, - points: *const f64, - sizes: *const f64, - n_points: i64, - corners: *const f64, - setbacks: *const f64, - n_corners: i64, - tolerance: f64, - out: *mut *mut RawBrep, - holes: *mut i64, - cap: i64, - out_len: *mut i64, - out_failed_point: *mut i64, - out_failed_corner: *mut i64, - ) -> i32; - fn smlib_fillet_variable( - brep: *const RawBrep, - point: *const f64, - tolerance: f64, - start: *const f64, - degree: i32, - knots: *const f64, - n_knots: i64, - radii: *const f64, - n_radii: i64, - out: *mut *mut RawBrep, - ) -> i32; - fn smlib_chamfer_sides( - brep: *const RawBrep, - points: *const f64, - n_points: i64, - tolerance: f64, - distance: f64, - second: f64, - second_is_angle: i32, - first: *const f64, - out: *mut *mut RawBrep, - out_failed_point: *mut i64, - ) -> i32; - fn smlib_shell( - brep: *const RawBrep, - points: *const f64, - n_points: i64, - tolerance: f64, - thickness: f64, - out: *mut *mut RawBrep, - out_failed_point: *mut i64, - ) -> i32; - fn smlib_offset_faces( - brep: *const RawBrep, - points: *const f64, - n_points: i64, - tolerance: f64, - distance: f64, - out: *mut *mut RawBrep, - out_failed_point: *mut i64, - ) -> i32; - fn smlib_thicken(brep: *const RawBrep, thickness: f64, symmetric: i32, out: *mut *mut RawBrep) -> i32; - fn smlib_merge_faces( - brep: *const RawBrep, - keep: *const f64, - n_keep: i64, - tolerance: f64, - out: *mut *mut RawBrep, - out_failed_point: *mut i64, - ) -> i32; - fn smlib_draft( - brep: *const RawBrep, - points: *const f64, - n_points: i64, - tolerance: f64, - neutral_origin: *const f64, - neutral_normal: *const f64, - pull: *const f64, - angle: f64, - out: *mut *mut RawBrep, - out_failed_point: *mut i64, - ) -> i32; -} - -#[cfg(feature = "native")] -pub fn boolean(a: &Handle, b: &Handle, op: i32) -> Result { - new_brep(|out| unsafe { smlib_boolean(a.raw(), b.raw(), op, out) }) -} - -#[cfg(feature = "native")] -pub fn boolean_union_all(breps: &[&Handle]) -> Result { - let raw: Vec<*const RawBrep> = breps.iter().map(|h| h.raw()).collect(); - let n = i64::try_from(raw.len()).map_err(|_| ERR_ARG)?; - new_brep(|out| unsafe { smlib_boolean_union_all(raw.as_ptr(), n, out) }) -} - -#[cfg(feature = "native")] -pub fn boolean2d(a: &Handle, b: &Handle, op: i32) -> Result { - new_brep(|out| unsafe { smlib_boolean2d(a.raw(), b.raw(), op, out) }) -} - -#[cfg(feature = "native")] -pub fn imprint(body: &Handle, tool: &Handle) -> Result { - new_brep(|out| unsafe { smlib_imprint(body.raw(), tool.raw(), out) }) -} - -#[cfg(feature = "native")] -pub fn solid_count(b: &Handle) -> Result { - let mut n = 0i64; - match unsafe { smlib_brep_solid_count(b.raw(), &mut n) } { - OK => Ok(n), - s => Err(s), - } -} - -/// Runs a selection operation; `points` are passed as the flat x, y, z array the C API reads. -#[cfg(feature = "native")] -pub fn pick_op(kind: Pick, b: &Handle, points: &[[f64; 3]], tolerance: f64, size: f64) -> Result { - let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None))?; - let f: PickFn = match kind { - Pick::Fillet => smlib_fillet, - Pick::Chamfer => smlib_chamfer, - Pick::Shell => smlib_shell, - Pick::OffsetFaces => smlib_offset_faces, - }; - let flat = points.as_ptr().cast::(); - let mut failed = -1i64; - new_brep(|out| unsafe { f(b.raw(), flat, n, tolerance, size, out, &mut failed) }).map_err(|s| { - let at = (s == ERR_PICK_NONE || s == ERR_PICK_AMBIGUOUS).then(|| usize::try_from(failed).ok()).flatten(); - (s, at) - }) -} - -/// `smlib_blend_fit_size`: the size that builds, and what it sizes (`FIT_*`), for the last blend -/// call SMLib refused as too large or a setback as too short; `None` where it names none. -#[cfg(feature = "native")] -pub fn blend_fit_size() -> Option<(f64, i32)> { - let (mut size, mut what) = (0.0f64, -1i32); - (unsafe { smlib_blend_fit_size(&mut size, &mut what) } == OK).then_some((size, what)) -} - -#[cfg(not(feature = "native"))] -pub fn blend_fit_size() -> Option<(f64, i32)> { - None -} - -#[cfg(feature = "native")] -pub fn thicken(b: &Handle, thickness: f64, symmetric: bool) -> Result { - new_brep(|out| unsafe { smlib_thicken(b.raw(), thickness, i32::from(symmetric), out) }) -} - -#[cfg(not(feature = "native"))] -pub fn thicken(_: &Handle, _: f64, _: bool) -> Result { - Err(NOT_BUILT) -} - -/// `smlib_draft`: the faces under `points` tilted by `angle` radians about their lines on the -/// neutral plane (`origin`, `normal`), leaning in along `pull`. -#[cfg(feature = "native")] -pub fn draft( - b: &Handle, - points: &[[f64; 3]], - tolerance: f64, - origin: &[f64; 3], - normal: &[f64; 3], - pull: &[f64; 3], - angle: f64, -) -> Result { - let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None))?; - let flat = points.as_ptr().cast::(); - let mut failed = -1i64; - new_brep(|out| unsafe { smlib_draft(b.raw(), flat, n, tolerance, origin.as_ptr(), normal.as_ptr(), pull.as_ptr(), angle, out, &mut failed) }) - .map_err(|s| { - let at = (s == ERR_PICK_NONE || s == ERR_PICK_AMBIGUOUS).then(|| usize::try_from(failed).ok()).flatten(); - (s, at) - }) -} - -#[cfg(not(feature = "native"))] -pub fn draft(_: &Handle, _: &[[f64; 3]], _: f64, _: &[f64; 3], _: &[f64; 3], _: &[f64; 3], _: f64) -> Result { - Err((NOT_BUILT, None)) -} - -/// `smlib_merge_faces`: the body's topological edges and vertices removed, the edges under -/// `keep` kept. -#[cfg(feature = "native")] -pub fn merge_faces(b: &Handle, keep: &[[f64; 3]], tolerance: f64) -> Result { - let n = i64::try_from(keep.len()).map_err(|_| (ERR_ARG, None))?; - let flat = keep.as_ptr().cast::(); - let mut failed = -1i64; - new_brep(|out| unsafe { smlib_merge_faces(b.raw(), flat, n, tolerance, out, &mut failed) }).map_err(|s| { - let at = (s == ERR_PICK_NONE || s == ERR_PICK_AMBIGUOUS).then(|| usize::try_from(failed).ok()).flatten(); - (s, at) - }) -} - -#[cfg(not(feature = "native"))] -pub fn merge_faces(_: &Handle, _: &[[f64; 3]], _: f64) -> Result { - Err((NOT_BUILT, None)) -} - -/// `smlib_blend`: `sizes` holds one size per point (the caller checks the lengths match). -#[cfg(feature = "native")] -pub fn blend(kind: i32, b: &Handle, points: &[[f64; 3]], sizes: &[f64], tolerance: f64) -> Result { - let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None))?; - if sizes.len() != points.len() { - return Err((ERR_ARG, None)); - } - let flat = points.as_ptr().cast::(); - let mut failed = -1i64; - new_brep(|out| unsafe { smlib_blend(b.raw(), kind, flat, sizes.as_ptr(), n, tolerance, out, &mut failed) }).map_err(|s| { - let at = (s == ERR_PICK_NONE || s == ERR_PICK_AMBIGUOUS).then(|| usize::try_from(failed).ok()).flatten(); - (s, at) - }) -} - -/// `smlib_blend_setback`: `sizes` holds one size per point and `setbacks` one distance per -/// corner (the caller checks the lengths match). A failed pick names its point; a corner that -/// picks no vertex or several comes back with no index. -#[cfg(feature = "native")] -pub fn blend_setback( - kind: i32, - b: &Handle, - points: &[[f64; 3]], - sizes: &[f64], - corners: &[[f64; 3]], - setbacks: &[f64], - tolerance: f64, -) -> Result { - let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None, None))?; - let nc = i64::try_from(corners.len()).map_err(|_| (ERR_ARG, None, None))?; - if sizes.len() != points.len() || setbacks.len() != corners.len() { - return Err((ERR_ARG, None, None)); - } - let flat = points.as_ptr().cast::(); - let flat_corners = corners.as_ptr().cast::(); - let (mut failed, mut failed_corner) = (-1i64, -1i64); - new_brep(|out| unsafe { - smlib_blend_setback( - b.raw(), - kind, - flat, - sizes.as_ptr(), - n, - flat_corners, - setbacks.as_ptr(), - nc, - tolerance, - out, - &mut failed, - &mut failed_corner, - ) - }) - .map_err(|s| setback_error(s, failed, failed_corner)) -} - -#[cfg(not(feature = "native"))] -pub fn blend_setback(_: i32, _: &Handle, _: &[[f64; 3]], _: &[f64], _: &[[f64; 3]], _: &[f64], _: f64) -> Result { - Err((NOT_BUILT, None, None)) -} - -/// `smlib_blend_setback_open`: the sheet and the holes' values as the C function writes them -/// (per corner: n, then n (edge, face, forward) triples). The buffer starts at room for 64 -/// edges a corner and is grown once to the length a first call names. -#[cfg(feature = "native")] -pub fn blend_setback_open( - kind: i32, - b: &Handle, - points: &[[f64; 3]], - sizes: &[f64], - corners: &[[f64; 3]], - setbacks: &[f64], - tolerance: f64, -) -> Result<(Handle, Vec), SetbackError> { - let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None, None))?; - let nc = i64::try_from(corners.len()).map_err(|_| (ERR_ARG, None, None))?; - if sizes.len() != points.len() || setbacks.len() != corners.len() { - return Err((ERR_ARG, None, None)); - } - let flat = points.as_ptr().cast::(); - let flat_corners = corners.as_ptr().cast::(); - let mut holes = vec![0i64; corners.len().saturating_mul(1 + 3 * 64)]; - for _ in 0..2 { - let cap = i64::try_from(holes.len()).map_err(|_| (ERR_ARG, None, None))?; - let (mut failed, mut failed_corner, mut len) = (-1i64, -1i64, -1i64); - let made = new_brep(|out| unsafe { - smlib_blend_setback_open( - b.raw(), - kind, - flat, - sizes.as_ptr(), - n, - flat_corners, - setbacks.as_ptr(), - nc, - tolerance, - out, - holes.as_mut_ptr(), - cap, - &mut len, - &mut failed, - &mut failed_corner, - ) - }); - match made { - Ok(h) => { - holes.truncate(usize::try_from(len).map_err(|_| (ERR_BUFFER, None, None))?); - return Ok((h, holes)); - } - Err(ERR_BUFFER) if len > cap => holes.resize(usize::try_from(len).map_err(|_| (ERR_BUFFER, None, None))?, 0), - Err(s) => return Err(setback_error(s, failed, failed_corner)), - } - } - Err((ERR_BUFFER, None, None)) -} - -#[cfg(not(feature = "native"))] -pub fn blend_setback_open( - _: i32, - _: &Handle, - _: &[[f64; 3]], - _: &[f64], - _: &[[f64; 3]], - _: &[f64], - _: f64, -) -> Result<(Handle, Vec), SetbackError> { - Err((NOT_BUILT, None, None)) -} - -/// `smlib_fillet_variable`. -#[cfg(feature = "native")] -pub fn fillet_variable( - b: &Handle, - point: &[f64; 3], - tolerance: f64, - start: &[f64; 3], - degree: i32, - knots: &[f64], - radii: &[f64], -) -> Result { - let nk = i64::try_from(knots.len()).map_err(|_| ERR_ARG)?; - let nr = i64::try_from(radii.len()).map_err(|_| ERR_ARG)?; - new_brep(|out| unsafe { - smlib_fillet_variable(b.raw(), point.as_ptr(), tolerance, start.as_ptr(), degree, knots.as_ptr(), nk, radii.as_ptr(), nr, out) - }) -} - -/// `smlib_chamfer_sides`. -#[cfg(feature = "native")] -pub fn chamfer_sides( - b: &Handle, - points: &[[f64; 3]], - tolerance: f64, - distance: f64, - second: f64, - angle: bool, - first: &[f64; 3], -) -> Result { - let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None))?; - let flat = points.as_ptr().cast::(); - let mut failed = -1i64; - new_brep(|out| unsafe { smlib_chamfer_sides(b.raw(), flat, n, tolerance, distance, second, i32::from(angle), first.as_ptr(), out, &mut failed) }) - .map_err(|s| { - let at = (s == ERR_PICK_NONE || s == ERR_PICK_AMBIGUOUS).then(|| usize::try_from(failed).ok()).flatten(); - (s, at) - }) -} - -#[cfg(not(feature = "native"))] -pub fn chamfer_sides(_: &Handle, _: &[[f64; 3]], _: f64, _: f64, _: f64, _: bool, _: &[f64; 3]) -> Result { - Err((NOT_BUILT, None)) -} - -/// `smlib_blend_dropping`: `smlib_blend`, with whether each point picked an edge a larger, -/// overflowing round removed whole (and so not rounded). -#[cfg(feature = "native")] -pub fn blend_dropping(kind: i32, b: &Handle, points: &[[f64; 3]], sizes: &[f64], tolerance: f64) -> Result<(Handle, Vec), PickError> { - let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None))?; - if sizes.len() != points.len() { - return Err((ERR_ARG, None)); - } - let flat = points.as_ptr().cast::(); - let mut failed = -1i64; - let mut dropped = vec![0i64; points.len()]; - let out = - new_brep(|out| unsafe { smlib_blend_dropping(b.raw(), kind, flat, sizes.as_ptr(), n, tolerance, out, &mut failed, dropped.as_mut_ptr()) }) - .map_err(|s| { - let at = (s == ERR_PICK_NONE || s == ERR_PICK_AMBIGUOUS).then(|| usize::try_from(failed).ok()).flatten(); - (s, at) - })?; - Ok((out, dropped.iter().map(|d| *d != 0).collect())) -} - -#[cfg(not(feature = "native"))] -pub fn blend_dropping(_: i32, _: &Handle, _: &[[f64; 3]], _: &[f64], _: f64) -> Result<(Handle, Vec), PickError> { - Err((NOT_BUILT, None)) -} - -#[cfg(not(feature = "native"))] -pub fn blend(_: i32, _: &Handle, _: &[[f64; 3]], _: &[f64], _: f64) -> Result { - Err((NOT_BUILT, None)) -} - -#[cfg(not(feature = "native"))] -pub fn fillet_variable(_: &Handle, _: &[f64; 3], _: f64, _: &[f64; 3], _: i32, _: &[f64], _: &[f64]) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn boolean(_: &Handle, _: &Handle, _: i32) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn boolean_union_all(_: &[&Handle]) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn boolean2d(_: &Handle, _: &Handle, _: i32) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn imprint(_: &Handle, _: &Handle) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn solid_count(_: &Handle) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn pick_op(_: Pick, _: &Handle, _: &[[f64; 3]], _: f64, _: f64) -> Result { - Err((NOT_BUILT, None)) -} diff --git a/crates/smlib/src/ffi/sheets.rs b/crates/smlib/src/ffi/sheets.rs deleted file mode 100644 index 61f23eda8e..0000000000 --- a/crates/smlib/src/ffi/sheets.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! The sheets block of `capi/include/smlib.h` (`smlib_stitch`, `smlib_offset_sheet`; the fork's -//! `capi/src/smlib_sheets.cpp`), declared by hand. Part of the FFI boundary, like its parent: -//! called only through [`crate::sheets`]. - -#![allow(unsafe_code)] - -use super::Handle; -#[cfg(not(feature = "native"))] -use super::NOT_BUILT; -#[cfg(feature = "native")] -use super::{RawBrep, new_brep}; - -#[cfg(feature = "native")] -unsafe extern "C" { - fn smlib_stitch(brep: *const RawBrep, tolerance: f64, out_closed: *mut i32, out: *mut *mut RawBrep) -> i32; - fn smlib_offset_sheet(sheet: *const RawBrep, distance: f64, out: *mut *mut RawBrep) -> i32; -} - -/// The stitched body and whether it closed into a solid. -#[cfg(feature = "native")] -pub fn stitch(b: &Handle, tolerance: f64) -> Result<(Handle, bool), i32> { - let mut closed = 0i32; - let h = new_brep(|out| unsafe { smlib_stitch(b.raw(), tolerance, &mut closed, out) })?; - Ok((h, closed != 0)) -} - -#[cfg(feature = "native")] -pub fn offset_sheet(b: &Handle, distance: f64) -> Result { - new_brep(|out| unsafe { smlib_offset_sheet(b.raw(), distance, out) }) -} - -#[cfg(not(feature = "native"))] -pub fn stitch(_: &Handle, _: f64) -> Result<(Handle, bool), i32> { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn offset_sheet(_: &Handle, _: f64) -> Result { - Err(NOT_BUILT) -} diff --git a/crates/smlib/src/ffi/split.rs b/crates/smlib/src/ffi/split.rs deleted file mode 100644 index 6fb48ba69c..0000000000 --- a/crates/smlib/src/ffi/split.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! The split block of `capi/include/smlib.h` (`smlib_split_faces`, `smlib_split_body`, -//! `smlib_split_body_by_sheet`; the -//! fork's `capi/src/smlib_split.cpp`), declared by hand. Part of the FFI boundary, like its -//! parent: called only through [`crate::split`]. - -#![allow(unsafe_code)] - -use super::Handle; -#[cfg(not(feature = "native"))] -use super::NOT_BUILT; -use super::ssi::SurfaceHandle; -#[cfg(feature = "native")] -use super::{ERR_ARG, ERR_EMPTY, OK, RawBrep, new_brep, ssi::RawSurface}; - -/// `SMLIB_SPLIT_MAX_FACES`, `SMLIB_SPLIT_MAX_TOOLS`. -pub const MAX_FACES: usize = 100_000; -pub const MAX_TOOLS: usize = 4096; - -#[cfg(feature = "native")] -unsafe extern "C" { - fn smlib_split_faces( - body: *const RawBrep, - faces: *const i64, - n_faces: i64, - tools: *const *const RawSurface, - n_tools: i64, - out: *mut *mut RawBrep, - made: *mut i64, - ) -> i32; - fn smlib_split_body(body: *const RawBrep, tool: *const RawSurface, back: *mut *mut RawBrep, front: *mut *mut RawBrep) -> i32; - fn smlib_split_body_by_sheet(body: *const RawBrep, sheet: *const RawBrep, face: i64, back: *mut *mut RawBrep, front: *mut *mut RawBrep) -> i32; -} - -/// `body` with `faces` cut by `tools` in turn, and per face how many faces it became besides -/// itself. -#[cfg(feature = "native")] -pub fn split_faces(body: &Handle, faces: &[i64], tools: &[&SurfaceHandle]) -> Result<(Handle, Vec), i32> { - if faces.is_empty() || faces.len() > MAX_FACES || tools.is_empty() || tools.len() > MAX_TOOLS { - return Err(ERR_ARG); - } - let raw: Vec<*const RawSurface> = tools.iter().map(|t| t.raw()).collect(); - let mut made = vec![0i64; faces.len()]; - let n = i64::try_from(faces.len()).map_err(|_| ERR_ARG)?; - let m = i64::try_from(raw.len()).map_err(|_| ERR_ARG)?; - // SAFETY: the arrays outlive the call and hold `n` and `m` entries; the handles are live. - let h = new_brep(|out| unsafe { smlib_split_faces(body.raw(), faces.as_ptr(), n, raw.as_ptr(), m, out, made.as_mut_ptr()) })?; - Ok((h, made)) -} - -/// The parts of `body` behind and in front of `tool`. -#[cfg(feature = "native")] -pub fn split_body(body: &Handle, tool: &SurfaceHandle) -> Result<(Handle, Handle), i32> { - // SAFETY: the handles are live; the out-pointers are written only on success. - two_parts(|back, front| unsafe { smlib_split_body(body.raw(), tool.raw(), back, front) }) -} - -/// The parts of `body` behind and in front of `sheet`, its sides those of its face `face`. -#[cfg(feature = "native")] -pub fn split_body_by_sheet(body: &Handle, sheet: &Handle, face: i64) -> Result<(Handle, Handle), i32> { - // SAFETY: the handles are live; the out-pointers are written only on success. - two_parts(|back, front| unsafe { smlib_split_body_by_sheet(body.raw(), sheet.raw(), face, back, front) }) -} - -/// The two bodies a call writing a back and a front part gives, owned. -#[cfg(feature = "native")] -fn two_parts(call: impl FnOnce(*mut *mut RawBrep, *mut *mut RawBrep) -> i32) -> Result<(Handle, Handle), i32> { - let mut back: *mut RawBrep = std::ptr::null_mut(); - let mut front: *mut RawBrep = std::ptr::null_mut(); - let s = call(&mut back, &mut front); - let (b, f) = (std::ptr::NonNull::new(back), std::ptr::NonNull::new(front)); - match (s, b, f) { - (OK, Some(b), Some(f)) => Ok((Handle(b), Handle(f))), - (OK, b, f) => { - // Free whichever came back alone. - drop(b.map(Handle)); - drop(f.map(Handle)); - Err(ERR_EMPTY) - } - (s, _, _) => Err(s), - } -} - -#[cfg(not(feature = "native"))] -pub fn split_faces(_: &Handle, _: &[i64], _: &[&SurfaceHandle]) -> Result<(Handle, Vec), i32> { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn split_body(_: &Handle, _: &SurfaceHandle) -> Result<(Handle, Handle), i32> { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn split_body_by_sheet(_: &Handle, _: &Handle, _: i64) -> Result<(Handle, Handle), i32> { - Err(NOT_BUILT) -} diff --git a/crates/smlib/src/ffi/ssi.rs b/crates/smlib/src/ffi/ssi.rs deleted file mode 100644 index b62f10803f..0000000000 --- a/crates/smlib/src/ffi/ssi.rs +++ /dev/null @@ -1,378 +0,0 @@ -//! The surface/surface intersection block of `capi/include/smlib.h` (`smlib_surface_*`, -//! `smlib_ssi*`), declared by hand. Part of the FFI boundary, like its parent: called only -//! through [`crate::ssi`]. -#![allow(unsafe_code)] - -/// `smlib_surface`, opaque. -#[repr(C)] -pub struct RawSurface { - _private: [u8; 0], -} - -/// `smlib_ssi_result`, opaque. -#[repr(C)] -pub struct RawResult { - _private: [u8; 0], -} - -#[cfg(feature = "native")] -unsafe extern "C" { - fn smlib_surface_plane(origin: *const f64, xaxis: *const f64, yaxis: *const f64, u: *const f64, v: *const f64, out: *mut *mut RawSurface) -> i32; - fn smlib_surface_cylinder( - origin: *const f64, - xaxis: *const f64, - yaxis: *const f64, - radius: f64, - angle_deg: *const f64, - height: *const f64, - out: *mut *mut RawSurface, - ) -> i32; - fn smlib_surface_cone( - apex: *const f64, - xaxis: *const f64, - yaxis: *const f64, - half_angle_deg: f64, - height: *const f64, - out: *mut *mut RawSurface, - ) -> i32; - fn smlib_surface_sphere( - center: *const f64, - xaxis: *const f64, - yaxis: *const f64, - radius: f64, - lon_deg: *const f64, - lat_deg: *const f64, - out: *mut *mut RawSurface, - ) -> i32; - fn smlib_surface_torus( - center: *const f64, - xaxis: *const f64, - yaxis: *const f64, - major_radius: f64, - minor_radius: f64, - major_deg: *const f64, - minor_deg: *const f64, - out: *mut *mut RawSurface, - ) -> i32; - fn smlib_surface_nurbs( - degree_u: i32, - degree_v: i32, - nu: i32, - nv: i32, - knots_u: *const f64, - n_knots_u: i64, - knots_v: *const f64, - n_knots_v: i64, - points: *const f64, - weights: *const f64, - out: *mut *mut RawSurface, - ) -> i32; - fn smlib_surface_free(s: *mut RawSurface); - fn smlib_surface_domain(s: *const RawSurface, out: *mut f64) -> i32; - fn smlib_surface_evaluate(s: *const RawSurface, u: f64, v: f64, out: *mut f64) -> i32; - fn smlib_ssi(a: *const RawSurface, b: *const RawSurface, method: i32, approx_tol: f64, out: *mut *mut RawResult) -> i32; - fn smlib_ssi_result_free(r: *mut RawResult); - fn smlib_ssi_curve_count(r: *const RawResult, out: *mut i32) -> i32; - fn smlib_ssi_curve_info(r: *const RawResult, i: i32, ty: *mut i32, flags: *mut i32, deviation: *mut f64, interval: *mut f64) -> i32; - fn smlib_ssi_curve_nurbs( - r: *const RawResult, - i: i32, - which: i32, - degree: *mut i32, - n_points: *mut i64, - n_knots: *mut i64, - cap_points: i64, - points: *mut f64, - weights: *mut f64, - cap_knots: i64, - knots: *mut f64, - ) -> i32; - fn smlib_ssi_point_count(r: *const RawResult, out: *mut i32) -> i32; - fn smlib_ssi_point(r: *const RawResult, i: i32, xyz: *mut f64, uv_a: *mut f64, uv_b: *mut f64) -> i32; -} - -/// An owned `smlib_surface`, freed once on drop. -#[cfg_attr(not(feature = "native"), allow(dead_code))] -pub struct SurfaceHandle(std::ptr::NonNull); - -/// An owned `smlib_ssi_result`, freed once on drop. -#[cfg_attr(not(feature = "native"), allow(dead_code))] -pub struct ResultHandle(std::ptr::NonNull); - -// Only ever used through the C API, which serialises every call. -unsafe impl Send for SurfaceHandle {} -unsafe impl Sync for SurfaceHandle {} -unsafe impl Send for ResultHandle {} -unsafe impl Sync for ResultHandle {} - -impl SurfaceHandle { - #[cfg_attr(not(feature = "native"), allow(dead_code))] - pub(crate) fn raw(&self) -> *const RawSurface { - self.0.as_ptr() - } -} - -impl Drop for SurfaceHandle { - fn drop(&mut self) { - #[cfg(feature = "native")] - unsafe { - smlib_surface_free(self.0.as_ptr()) - }; - } -} - -impl Drop for ResultHandle { - fn drop(&mut self) { - #[cfg(feature = "native")] - unsafe { - smlib_ssi_result_free(self.0.as_ptr()) - }; - } -} - -/// One curve's `smlib_ssi_curve_info`. -#[derive(Clone, Copy, Debug)] -pub struct CurveInfo { - pub ty: i32, - pub deviation: f64, - pub interval: [f64; 2], -} - -/// `smlib_ssi_curve_nurbs` `which`: the 3D curve, the UV curve on a, on b. -pub const CURVE_3D: i32 = 0; -pub const CURVE_UV_A: i32 = 1; -pub const CURVE_UV_B: i32 = 2; - -/// One B-spline as `smlib_ssi_curve_nurbs` gives it: cartesian control points, `dim` values each, -/// one weight each, and every knot (as many as points + degree + 1). -#[derive(Clone, Debug, Default)] -pub struct Nurbs { - pub points: Vec, - pub weights: Vec, - pub knots: Vec, -} - -#[cfg(feature = "native")] -mod calls { - use super::*; - use crate::ffi::{ERR_ARG, ERR_EMPTY, OK}; - - fn new_surface(f: impl FnOnce(*mut *mut RawSurface) -> i32) -> Result { - let mut out: *mut RawSurface = std::ptr::null_mut(); - match (f(&mut out), std::ptr::NonNull::new(out)) { - (OK, Some(p)) => Ok(SurfaceHandle(p)), - (OK, None) => Err(ERR_EMPTY), - (s, _) => Err(s), - } - } - - pub fn plane(origin: &[f64; 3], xaxis: &[f64; 3], yaxis: &[f64; 3], u: &[f64; 2], v: &[f64; 2]) -> Result { - new_surface(|out| unsafe { smlib_surface_plane(origin.as_ptr(), xaxis.as_ptr(), yaxis.as_ptr(), u.as_ptr(), v.as_ptr(), out) }) - } - pub fn cylinder( - origin: &[f64; 3], - xaxis: &[f64; 3], - yaxis: &[f64; 3], - radius: f64, - angle_deg: &[f64; 2], - height: &[f64; 2], - ) -> Result { - new_surface(|out| unsafe { - smlib_surface_cylinder(origin.as_ptr(), xaxis.as_ptr(), yaxis.as_ptr(), radius, angle_deg.as_ptr(), height.as_ptr(), out) - }) - } - pub fn cone(apex: &[f64; 3], xaxis: &[f64; 3], yaxis: &[f64; 3], half_angle_deg: f64, height: &[f64; 2]) -> Result { - new_surface(|out| unsafe { smlib_surface_cone(apex.as_ptr(), xaxis.as_ptr(), yaxis.as_ptr(), half_angle_deg, height.as_ptr(), out) }) - } - pub fn sphere( - center: &[f64; 3], - xaxis: &[f64; 3], - yaxis: &[f64; 3], - radius: f64, - lon_deg: &[f64; 2], - lat_deg: &[f64; 2], - ) -> Result { - new_surface(|out| unsafe { - smlib_surface_sphere(center.as_ptr(), xaxis.as_ptr(), yaxis.as_ptr(), radius, lon_deg.as_ptr(), lat_deg.as_ptr(), out) - }) - } - pub fn torus( - center: &[f64; 3], - xaxis: &[f64; 3], - yaxis: &[f64; 3], - major_radius: f64, - minor_radius: f64, - major_deg: &[f64; 2], - minor_deg: &[f64; 2], - ) -> Result { - new_surface(|out| unsafe { - smlib_surface_torus( - center.as_ptr(), - xaxis.as_ptr(), - yaxis.as_ptr(), - major_radius, - minor_radius, - major_deg.as_ptr(), - minor_deg.as_ptr(), - out, - ) - }) - } - /// `points` holds `nu * nv` points (3 values each), `weights` `nu * nv` or none; the C side - /// checks every count against the degrees and sizes. - pub fn nurbs( - degree: [i32; 2], - nu: i32, - nv: i32, - knots_u: &[f64], - knots_v: &[f64], - points: &[f64], - weights: Option<&[f64]>, - ) -> Result { - let n = i64::from(nu).checked_mul(i64::from(nv)).ok_or(ERR_ARG)?; - let fits = |len: usize, per: i64| n.checked_mul(per).is_some_and(|need| i64::try_from(len).is_ok_and(|l| l == need)); - if !fits(points.len(), 3) || weights.is_some_and(|w| !fits(w.len(), 1)) { - return Err(ERR_ARG); - } - let ku = i64::try_from(knots_u.len()).map_err(|_| ERR_ARG)?; - let kv = i64::try_from(knots_v.len()).map_err(|_| ERR_ARG)?; - let w = weights.map_or(std::ptr::null(), <[f64]>::as_ptr); - new_surface(|out| unsafe { - smlib_surface_nurbs(degree[0], degree[1], nu, nv, knots_u.as_ptr(), ku, knots_v.as_ptr(), kv, points.as_ptr(), w, out) - }) - } - pub fn domain(s: &SurfaceHandle) -> Result<[f64; 4], i32> { - let mut d = [0.0; 4]; - match unsafe { smlib_surface_domain(s.0.as_ptr(), d.as_mut_ptr()) } { - OK => Ok(d), - st => Err(st), - } - } - pub fn evaluate(s: &SurfaceHandle, u: f64, v: f64) -> Result<[f64; 3], i32> { - let mut p = [0.0; 3]; - match unsafe { smlib_surface_evaluate(s.0.as_ptr(), u, v, p.as_mut_ptr()) } { - OK => Ok(p), - st => Err(st), - } - } - pub fn ssi(a: &SurfaceHandle, b: &SurfaceHandle, method: i32, approx_tol: f64) -> Result { - let mut out: *mut RawResult = std::ptr::null_mut(); - match (unsafe { smlib_ssi(a.0.as_ptr(), b.0.as_ptr(), method, approx_tol, &mut out) }, std::ptr::NonNull::new(out)) { - (OK, Some(p)) => Ok(ResultHandle(p)), - (OK, None) => Err(ERR_EMPTY), - (s, _) => Err(s), - } - } - pub fn curve_count(r: &ResultHandle) -> Result { - let mut n = 0; - match unsafe { smlib_ssi_curve_count(r.0.as_ptr(), &mut n) } { - OK => Ok(n), - s => Err(s), - } - } - pub fn curve_info(r: &ResultHandle, i: i32) -> Result { - let mut c = CurveInfo { ty: 0, deviation: 0.0, interval: [0.0; 2] }; - match unsafe { smlib_ssi_curve_info(r.0.as_ptr(), i, &mut c.ty, std::ptr::null_mut(), &mut c.deviation, c.interval.as_mut_ptr()) } { - OK => Ok(c), - s => Err(s), - } - } - /// Curve `i`'s `which` B-spline, `dim` coordinates per control point: the counts first, then - /// the arrays. - pub fn curve_nurbs(r: &ResultHandle, i: i32, which: i32, dim: usize) -> Result { - let (mut degree, mut np, mut nk) = (0i32, 0i64, 0i64); - let null = std::ptr::null_mut(); - let st = unsafe { smlib_ssi_curve_nurbs(r.0.as_ptr(), i, which, &mut degree, &mut np, &mut nk, 0, null, null, 0, null) }; - if st != OK { - return Err(st); - } - let (n, k) = (usize::try_from(np).map_err(|_| ERR_ARG)?, usize::try_from(nk).map_err(|_| ERR_ARG)?); - let degree = usize::try_from(degree).map_err(|_| ERR_ARG)?; - if !(n > degree && k == n + degree + 1) { - return Err(ERR_ARG); - } - let mut c = Nurbs { points: vec![0.0; n.checked_mul(dim).ok_or(ERR_ARG)?], weights: vec![0.0; n], knots: vec![0.0; k] }; - let st = unsafe { - smlib_ssi_curve_nurbs( - r.0.as_ptr(), - i, - which, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - np, - c.points.as_mut_ptr(), - c.weights.as_mut_ptr(), - nk, - c.knots.as_mut_ptr(), - ) - }; - if st == OK { Ok(c) } else { Err(st) } - } - pub fn point_count(r: &ResultHandle) -> Result { - let mut n = 0; - match unsafe { smlib_ssi_point_count(r.0.as_ptr(), &mut n) } { - OK => Ok(n), - s => Err(s), - } - } - pub fn point(r: &ResultHandle, i: i32) -> Result<([f64; 3], [f64; 2], [f64; 2]), i32> { - let (mut p, mut a, mut b) = ([0.0; 3], [0.0; 2], [0.0; 2]); - match unsafe { smlib_ssi_point(r.0.as_ptr(), i, p.as_mut_ptr(), a.as_mut_ptr(), b.as_mut_ptr()) } { - OK => Ok((p, a, b)), - 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 plane(_: &[f64; 3], _: &[f64; 3], _: &[f64; 3], _: &[f64; 2], _: &[f64; 2]) -> Result { - Err(NOT_BUILT) - } - pub fn cylinder(_: &[f64; 3], _: &[f64; 3], _: &[f64; 3], _: f64, _: &[f64; 2], _: &[f64; 2]) -> Result { - Err(NOT_BUILT) - } - pub fn cone(_: &[f64; 3], _: &[f64; 3], _: &[f64; 3], _: f64, _: &[f64; 2]) -> Result { - Err(NOT_BUILT) - } - pub fn sphere(_: &[f64; 3], _: &[f64; 3], _: &[f64; 3], _: f64, _: &[f64; 2], _: &[f64; 2]) -> Result { - Err(NOT_BUILT) - } - pub fn torus(_: &[f64; 3], _: &[f64; 3], _: &[f64; 3], _: f64, _: f64, _: &[f64; 2], _: &[f64; 2]) -> Result { - Err(NOT_BUILT) - } - pub fn nurbs(_: [i32; 2], _: i32, _: i32, _: &[f64], _: &[f64], _: &[f64], _: Option<&[f64]>) -> Result { - Err(NOT_BUILT) - } - pub fn domain(_: &SurfaceHandle) -> Result<[f64; 4], i32> { - Err(NOT_BUILT) - } - pub fn evaluate(_: &SurfaceHandle, _: f64, _: f64) -> Result<[f64; 3], i32> { - Err(NOT_BUILT) - } - pub fn ssi(_: &SurfaceHandle, _: &SurfaceHandle, _: i32, _: f64) -> Result { - Err(NOT_BUILT) - } - pub fn curve_count(_: &ResultHandle) -> Result { - Err(NOT_BUILT) - } - pub fn curve_info(_: &ResultHandle, _: i32) -> Result { - Err(NOT_BUILT) - } - pub fn curve_nurbs(_: &ResultHandle, _: i32, _: i32, _: usize) -> Result { - Err(NOT_BUILT) - } - pub fn point_count(_: &ResultHandle) -> Result { - Err(NOT_BUILT) - } - pub fn point(_: &ResultHandle, _: i32) -> Result<([f64; 3], [f64; 2], [f64; 2]), i32> { - Err(NOT_BUILT) - } -} - -pub use calls::*; diff --git a/crates/smlib/src/ffi/sweeps.rs b/crates/smlib/src/ffi/sweeps.rs deleted file mode 100644 index 9da3b75f6e..0000000000 --- a/crates/smlib/src/ffi/sweeps.rs +++ /dev/null @@ -1,431 +0,0 @@ -//! The sweeps block of `capi/include/smlib.h` (`smlib_extrude`, `smlib_revolve`, `smlib_sweep`, -//! `smlib_sweep_placed`, `smlib_project_curves`, `smlib_face_section` (with `smlib_curves_*`), `smlib_loft`, `smlib_loft_to_point`, `smlib_screw_sweep`, `smlib_screw_sweep_radial`, -//! `smlib_sweep_stations`, `smlib_planar_faces`; the fork's -//! `capi/src/smlib_sweeps.cpp`), declared by hand. -//! Part of the FFI boundary, like its parent: called only through [`crate::sweeps`], which builds -//! the curve lists. - -#![allow(unsafe_code)] - -use super::Handle; -#[cfg(not(feature = "native"))] -use super::NOT_BUILT; -#[cfg(feature = "native")] -use super::{ERR_ARG, RawBrep, new_brep}; - -pub const CURVE_LINE: i64 = 1; -pub const CURVE_ARC: i64 = 2; -pub const CURVE_NURBS: i64 = 3; -pub const CURVE_MAX_CURVES: usize = 16_384; -pub const CURVE_MAX_DEGREE: usize = 25; -pub const CURVE_MAX_POINTS: usize = 100_000; - -/// How a drafted extrusion closes a sharp corner (`SMLIB_DRAFT_CORNER_*`). -pub const DRAFT_CORNER_CURVATURE: i32 = 0; -pub const DRAFT_CORNER_TANGENT: i32 = 1; -pub const DRAFT_CORNER_ROUND: i32 = 2; -/// The continued curves do not meet at the corner (`SMLIB_ERR_CORNER_APART`). -pub const ERR_CORNER_APART: i32 = -1108; -/// A wall closes before the far end (`SMLIB_ERR_WALL_CLOSES`). -pub const ERR_WALL_CLOSES: i32 = -1109; - -pub const SWEEP_FOLLOW: i32 = 0; -pub const SWEEP_TRANSLATE: i32 = 1; - -pub const ENDS_OPEN: i32 = 0; -pub const ENDS_CAPPED: i32 = 1; -pub const ENDS_JOINED: i32 = 2; -pub const SWEEP_MAX_STATIONS: usize = 4096; -pub const SWEEP_MAX_STATION_CURVES: usize = 400_000; - -/// A curve list as the C API reads it: kinds (with a NURBS curve's degree and point count) and -/// each curve's numbers. -#[cfg(feature = "native")] -pub struct List<'a> { - kinds: &'a [i64], - reals: &'a [f64], -} - -/// Without SMLib nothing reads the list, so it carries nothing. -#[cfg(not(feature = "native"))] -pub struct List<'a>(std::marker::PhantomData<&'a ()>); - -impl<'a> List<'a> { - #[cfg(feature = "native")] - pub fn new(kinds: &'a [i64], reals: &'a [f64]) -> Self { - List { kinds, reals } - } - - #[cfg(not(feature = "native"))] - pub fn new(_: &'a [i64], _: &'a [f64]) -> Self { - List(std::marker::PhantomData) - } -} - -#[cfg(feature = "native")] -unsafe extern "C" { - fn smlib_planar_faces(kinds: *const i64, n_kinds: i64, reals: *const f64, n_reals: i64, out: *mut *mut RawBrep) -> i32; - fn smlib_extrude( - kinds: *const i64, - n_kinds: i64, - reals: *const f64, - n_reals: i64, - direction: *const f64, - distance: f64, - draft: f64, - corner: i32, - cap: i32, - out: *mut *mut RawBrep, - out_corner: *mut f64, - ) -> i32; - fn smlib_revolve( - kinds: *const i64, - n_kinds: i64, - reals: *const f64, - n_reals: i64, - origin: *const f64, - direction: *const f64, - angle: f64, - cap: i32, - out: *mut *mut RawBrep, - ) -> i32; - fn smlib_sweep( - kinds: *const i64, - n_kinds: i64, - reals: *const f64, - n_reals: i64, - path_kinds: *const i64, - n_path_kinds: i64, - path_reals: *const f64, - n_path_reals: i64, - frame: i32, - cap: i32, - out: *mut *mut RawBrep, - ) -> i32; - fn smlib_sweep_placed( - kinds: *const i64, - n_kinds: i64, - reals: *const f64, - n_reals: i64, - path_kinds: *const i64, - n_path_kinds: i64, - path_reals: *const f64, - n_path_reals: i64, - frame: i32, - cap: i32, - out: *mut *mut RawBrep, - placed: *mut f64, - ) -> i32; - fn smlib_project_curves( - brep: *const RawBrep, - face_point: *const f64, - pick_tolerance: f64, - kinds: *const i64, - n_kinds: i64, - reals: *const f64, - n_reals: i64, - direction: *const f64, - tolerance: f64, - out: *mut *mut RawCurves, - ) -> i32; - fn smlib_face_section( - brep: *const RawBrep, - face_point: *const f64, - pick_tolerance: f64, - plane_point: *const f64, - plane_normal: *const f64, - tolerance: f64, - out: *mut *mut RawCurves, - ) -> i32; - fn smlib_curves_size(curves: *const RawCurves, n_kinds: *mut i64, n_reals: *mut i64, n_curves: *mut i64) -> i32; - fn smlib_curves_read( - curves: *const RawCurves, - kinds: *mut i64, - n_kinds: i64, - reals: *mut f64, - n_reals: i64, - intervals: *mut f64, - n_intervals: i64, - ) -> i32; - fn smlib_curves_free(curves: *mut RawCurves); - fn smlib_loft( - kinds: *const i64, - n_kinds: i64, - reals: *const f64, - n_reals: i64, - section_sizes: *const i64, - n_sections: i64, - degree: i32, - cap: i32, - out: *mut *mut RawBrep, - ) -> i32; - fn smlib_loft_to_point(kinds: *const i64, n_kinds: i64, reals: *const f64, n_reals: i64, apex: *const f64, out: *mut *mut RawBrep) -> i32; - fn smlib_screw_sweep( - kinds: *const i64, - n_kinds: i64, - reals: *const f64, - n_reals: i64, - origin: *const f64, - direction: *const f64, - pitch: f64, - turns: f64, - tolerance: f64, - out: *mut *mut RawBrep, - ) -> i32; - fn smlib_screw_sweep_radial( - kinds: *const i64, - n_kinds: i64, - reals: *const f64, - n_reals: i64, - origin: *const f64, - direction: *const f64, - pitch: f64, - turns: f64, - law: *const f64, - n_law: i64, - tolerance: f64, - out: *mut *mut RawBrep, - ) -> i32; - fn smlib_sweep_stations( - kinds: *const i64, - n_kinds: i64, - reals: *const f64, - n_reals: i64, - maps: *const f64, - n_stations: i64, - ends: i32, - out: *mut *mut RawBrep, - ) -> i32; -} - -/// The list's arrays and lengths as the C API takes them. -#[cfg(feature = "native")] -fn raw(l: &List) -> Result<(*const i64, i64, *const f64, i64), i32> { - let nk = i64::try_from(l.kinds.len()).map_err(|_| ERR_ARG)?; - let nr = i64::try_from(l.reals.len()).map_err(|_| ERR_ARG)?; - Ok((l.kinds.as_ptr(), nk, l.reals.as_ptr(), nr)) -} - -#[cfg(feature = "native")] -pub fn planar_faces(loops: &List) -> Result { - let (k, nk, r, nr) = raw(loops)?; - new_brep(|out| unsafe { smlib_planar_faces(k, nk, r, nr, out) }) -} - -#[cfg(feature = "native")] -pub fn extrude(profile: &List, direction: &[f64; 3], distance: f64, draft: f64, corner: i32, cap: bool, at: &mut [f64; 3]) -> Result { - let (k, nk, r, nr) = raw(profile)?; - new_brep(|out| unsafe { smlib_extrude(k, nk, r, nr, direction.as_ptr(), distance, draft, corner, i32::from(cap), out, at.as_mut_ptr()) }) -} - -#[cfg(feature = "native")] -pub fn revolve(profile: &List, origin: &[f64; 3], direction: &[f64; 3], angle: f64, cap: bool) -> Result { - let (k, nk, r, nr) = raw(profile)?; - new_brep(|out| unsafe { smlib_revolve(k, nk, r, nr, origin.as_ptr(), direction.as_ptr(), angle, i32::from(cap), out) }) -} - -#[cfg(feature = "native")] -pub fn sweep(profile: &List, path: &List, frame: i32, cap: bool) -> Result { - let (k, nk, r, nr) = raw(profile)?; - let (pk, npk, pr, npr) = raw(path)?; - new_brep(|out| unsafe { smlib_sweep(k, nk, r, nr, pk, npk, pr, npr, frame, i32::from(cap), out) }) -} - -/// [`sweep`], with the three rigid motions placing the profile (12 numbers each: a matrix by -/// columns, then a translation) written to `placed`. -#[cfg(feature = "native")] -pub fn sweep_placed(profile: &List, path: &List, frame: i32, cap: bool, placed: &mut [f64; 36]) -> Result { - let (k, nk, r, nr) = raw(profile)?; - let (pk, npk, pr, npr) = raw(path)?; - let at = placed.as_mut_ptr(); - new_brep(|out| unsafe { smlib_sweep_placed(k, nk, r, nr, pk, npk, pr, npr, frame, i32::from(cap), out, at) }) -} - -/// The C API's `smlib_curves`. -#[cfg(feature = "native")] -#[repr(C)] -pub struct RawCurves { - _private: [u8; 0], -} - -/// Curves a call made, read out of the C API's `smlib_curves`: a curve list (kinds, reals) and -/// two interval numbers per curve. -pub struct OutCurves { - pub kinds: Vec, - pub reals: Vec, - pub intervals: Vec, -} - -/// `smlib_project_curves`: the curve list projected along `direction` onto the face of `brep` -/// nearest `face_point`. -#[cfg(feature = "native")] -pub fn project_curves( - brep: &Handle, - face_point: &[f64; 3], - pick_tolerance: f64, - curves: &List, - direction: &[f64; 3], - tolerance: f64, -) -> Result { - let (k, nk, r, nr) = raw(curves)?; - take_curves(|out| unsafe { - smlib_project_curves(brep.raw(), face_point.as_ptr(), pick_tolerance, k, nk, r, nr, direction.as_ptr(), tolerance, out) - }) -} - -/// `smlib_face_section`: the face of `brep` at `face_point` cut by a plane. -#[cfg(feature = "native")] -pub fn face_section( - brep: &Handle, - face_point: &[f64; 3], - pick_tolerance: f64, - plane_point: &[f64; 3], - plane_normal: &[f64; 3], - tolerance: f64, -) -> Result { - take_curves(|out| unsafe { - smlib_face_section(brep.raw(), face_point.as_ptr(), pick_tolerance, plane_point.as_ptr(), plane_normal.as_ptr(), tolerance, out) - }) -} - -/// Runs a call that makes an `smlib_curves`, reads it out and frees it. -#[cfg(feature = "native")] -fn take_curves(call: impl FnOnce(*mut *mut RawCurves) -> i32) -> Result { - let mut out: *mut RawCurves = std::ptr::null_mut(); - let s = call(&mut out); - if s != super::OK { - return Err(s); - } - if out.is_null() { - return Err(super::ERR_EMPTY); - } - let read = || -> Result { - let (mut nk, mut nr, mut nc) = (0i64, 0i64, 0i64); - let s = unsafe { smlib_curves_size(out, &mut nk, &mut nr, &mut nc) }; - if s != super::OK { - return Err(s); - } - let len = |n: i64| usize::try_from(n).map_err(|_| ERR_ARG); - let ni = nc.checked_mul(2).ok_or(ERR_ARG)?; - let mut c = OutCurves { kinds: vec![0; len(nk)?], reals: vec![0.0; len(nr)?], intervals: vec![0.0; len(ni)?] }; - let s = unsafe { smlib_curves_read(out, c.kinds.as_mut_ptr(), nk, c.reals.as_mut_ptr(), nr, c.intervals.as_mut_ptr(), ni) }; - if s != super::OK { - return Err(s); - } - Ok(c) - }; - let c = read(); - unsafe { smlib_curves_free(out) }; - c -} - -#[cfg(feature = "native")] -pub fn loft(sections: &List, sizes: &[i64], degree: i32, cap: bool) -> Result { - let (k, nk, r, nr) = raw(sections)?; - let ns = i64::try_from(sizes.len()).map_err(|_| ERR_ARG)?; - new_brep(|out| unsafe { smlib_loft(k, nk, r, nr, sizes.as_ptr(), ns, degree, i32::from(cap), out) }) -} - -#[cfg(feature = "native")] -pub fn loft_to_point(section: &List, apex: &[f64; 3]) -> Result { - let (k, nk, r, nr) = raw(section)?; - new_brep(|out| unsafe { smlib_loft_to_point(k, nk, r, nr, apex.as_ptr(), out) }) -} - -#[cfg(feature = "native")] -pub fn screw_sweep(profile: &List, origin: &[f64; 3], direction: &[f64; 3], pitch: f64, turns: f64, tolerance: f64) -> Result { - let (k, nk, r, nr) = raw(profile)?; - new_brep(|out| unsafe { smlib_screw_sweep(k, nk, r, nr, origin.as_ptr(), direction.as_ptr(), pitch, turns, tolerance, out) }) -} - -/// `law` holds (turn, offset) pairs. -#[cfg(feature = "native")] -pub fn screw_sweep_radial( - profile: &List, - origin: &[f64; 3], - direction: &[f64; 3], - pitch: f64, - turns: f64, - law: &[f64], - tolerance: f64, -) -> Result { - let (k, nk, r, nr) = raw(profile)?; - if !law.len().is_multiple_of(2) { - return Err(ERR_ARG); - } - let n = i64::try_from(law.len() / 2).map_err(|_| ERR_ARG)?; - new_brep(|out| unsafe { - smlib_screw_sweep_radial(k, nk, r, nr, origin.as_ptr(), direction.as_ptr(), pitch, turns, law.as_ptr(), n, tolerance, out) - }) -} - -/// `maps` holds 12 numbers per station (a matrix by columns, then a translation). -#[cfg(feature = "native")] -pub fn sweep_stations(profile: &List, maps: &[f64], ends: i32) -> Result { - let (k, nk, r, nr) = raw(profile)?; - if !maps.len().is_multiple_of(12) { - return Err(ERR_ARG); - } - let n = i64::try_from(maps.len() / 12).map_err(|_| ERR_ARG)?; - new_brep(|out| unsafe { smlib_sweep_stations(k, nk, r, nr, maps.as_ptr(), n, ends, out) }) -} - -#[cfg(not(feature = "native"))] -pub fn planar_faces(_: &List) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn extrude(_: &List, _: &[f64; 3], _: f64, _: f64, _: i32, _: bool, _: &mut [f64; 3]) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn revolve(_: &List, _: &[f64; 3], _: &[f64; 3], _: f64, _: bool) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn sweep(_: &List, _: &List, _: i32, _: bool) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn sweep_placed(_: &List, _: &List, _: i32, _: bool, _: &mut [f64; 36]) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn project_curves(_: &Handle, _: &[f64; 3], _: f64, _: &List, _: &[f64; 3], _: f64) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn face_section(_: &Handle, _: &[f64; 3], _: f64, _: &[f64; 3], _: &[f64; 3], _: f64) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn loft(_: &List, _: &[i64], _: i32, _: bool) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn loft_to_point(_: &List, _: &[f64; 3]) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn screw_sweep(_: &List, _: &[f64; 3], _: &[f64; 3], _: f64, _: f64, _: f64) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn screw_sweep_radial(_: &List, _: &[f64; 3], _: &[f64; 3], _: f64, _: f64, _: &[f64], _: f64) -> Result { - Err(NOT_BUILT) -} - -#[cfg(not(feature = "native"))] -pub fn sweep_stations(_: &List, _: &[f64], _: i32) -> Result { - Err(NOT_BUILT) -} diff --git a/crates/smlib/src/ffi/trim.rs b/crates/smlib/src/ffi/trim.rs deleted file mode 100644 index a4fca195a0..0000000000 --- a/crates/smlib/src/ffi/trim.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! The untrim and extend blocks of `capi/include/smlib.h` (`smlib_untrim_face`, -//! `smlib_extend_sheet`; the fork's `capi/src/smlib_untrim.cpp` and `smlib_extend.cpp`) and -//! `smlib_face_points` (`smlib_imprint.cpp`, the pieces a trim chooses from), declared by hand. Part of the FFI boundary, like its parent: called only through [`crate::trim`]. - -#![allow(unsafe_code)] - -use super::Handle; -#[cfg(not(feature = "native"))] -use super::NOT_BUILT; -#[cfg(feature = "native")] -use super::{RawBrep, new_brep}; - -/// The answer is not exact in the input's geometry (`SMLIB_ERR_NOT_EXACT`). -pub const ERR_NOT_EXACT: i32 = -1106; - -pub const EXTEND_NATURAL: i32 = 0; -pub const EXTEND_TANGENT: i32 = 1; -pub const EXTEND_CURVATURE: i32 = 2; - -#[cfg(feature = "native")] -unsafe extern "C" { - fn smlib_untrim_face( - body: *const RawBrep, - point: *const f64, - tol: f64, - holes: *const f64, - n_holes: i64, - outer: i32, - in_place: i32, - out: *mut *mut RawBrep, - ) -> i32; - fn smlib_face_points(body: *const RawBrep, out: *mut f64, n_faces: i64) -> i32; - fn smlib_extend_sheet(sheet: *const RawBrep, point: *const f64, tol: f64, distance: f64, mode: i32, out: *mut *mut RawBrep) -> i32; -} - -#[cfg(feature = "native")] -pub fn extend_sheet(sheet: &Handle, point: &[f64; 3], tol: f64, distance: f64, mode: i32) -> Result { - new_brep(|out| unsafe { smlib_extend_sheet(sheet.raw(), point.as_ptr(), tol, distance, mode, out) }) -} - -#[cfg(not(feature = "native"))] -pub fn extend_sheet(_: &Handle, _: &[f64; 3], _: f64, _: f64, _: i32) -> Result { - Err(NOT_BUILT) -} - -/// `holes`: `None` for every hole. -#[cfg(feature = "native")] -pub fn untrim_face(body: &Handle, point: &[f64; 3], tol: f64, holes: Option<&[[f64; 3]]>, outer: bool, in_place: bool) -> Result { - let (ptr, n) = match holes { - Some(h) => (h.as_ptr().cast::(), i64::try_from(h.len()).map_err(|_| super::ERR_ARG)?), - None => (std::ptr::null(), -1), - }; - new_brep(|out| unsafe { smlib_untrim_face(body.raw(), point.as_ptr(), tol, ptr, n, i32::from(outer), i32::from(in_place), out) }) -} - -#[cfg(not(feature = "native"))] -pub fn untrim_face(_: &Handle, _: &[f64; 3], _: f64, _: Option<&[[f64; 3]]>, _: bool, _: bool) -> Result { - Err(NOT_BUILT) -} - -#[cfg(feature = "native")] -pub fn face_points(body: &Handle, n_faces: usize) -> Result, i32> { - let n = i64::try_from(n_faces).map_err(|_| super::ERR_ARG)?; - let mut out = vec![[0.0f64; 3]; n_faces]; - let s = unsafe { smlib_face_points(body.raw(), out.as_mut_ptr().cast::(), n) }; - if s == super::OK { Ok(out) } else { Err(s) } -} - -#[cfg(not(feature = "native"))] -pub fn face_points(_: &Handle, _: usize) -> Result, i32> { - Err(NOT_BUILT) -} diff --git a/crates/smlib/src/flat.rs b/crates/smlib/src/flat.rs index f7bcdf8005..08a471dd23 100644 --- a/crates/smlib/src/flat.rs +++ b/crates/smlib/src/flat.rs @@ -1,8 +1,8 @@ //! 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`]. +//! int64 and double arrays laid out like a member of USD's BrepArray schema, which solidcore's body +//! is converted to and from ([`crate::import::Arrays`]). -use crate::{Brep, Result, error, ffi, init}; +use crate::{Brep, Result}; // Array ids and kind values, as in smlib.h. pub const REGION_SHELL_COUNT: i32 = 1; @@ -129,65 +129,9 @@ 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::made).map_err(error) - } -} - impl Brep { - /// This body as flat arrays. - pub fn export(&self) -> Result { - ffi::flat::export(self.cpp()?).map(Flat).map_err(error) - } - /// Whether SMLib finds this body one closed, manifold solid. pub fn is_manifold_solid(&self) -> Result { - if crate::route::backend() == crate::route::Backend::Solidcore { - return crate::core::is_manifold_solid(self.core()?); - } - ffi::flat::is_manifold_solid(self.cpp()?).map_err(error) + crate::core::is_manifold_solid(self.core()) } } diff --git a/crates/smlib/src/history.rs b/crates/smlib/src/history.rs index bee815bfec..a584e99821 100644 --- a/crates/smlib/src/history.rs +++ b/crates/smlib/src/history.rs @@ -14,7 +14,7 @@ use std::collections::BTreeSet; use solidcraft_geom::brep::history::{Change, EdgeOrigin, EdgeProv, FaceOrigin, FaceProv, Failed, History, Side}; -use crate::{Brep, Error, Result, core, error, ffi, route}; +use crate::{Brep, Result, core}; /// A result face as SMLib reports it: how it relates to its origins, and the (operand, face) /// pairs it came from, sorted. @@ -67,96 +67,18 @@ impl Brep { /// A copy whose face i and edge i are tagged as face i and edge i of operand `operand` (in /// SMLib's order, which the copy keeps). Run an operation on tagged copies, then read its /// result's [`Brep::history`]. - /// - /// The tags are made in the kernel the body is in (solidcore's when it has its body, - /// [`crate::core::history`]); converted for the other, the body is tagged there the same way. pub fn tagged(&self, operand: usize) -> Result { - let mut t = match self.core.get() { - Some(Ok(b)) => Brep::made_core(core::history::tag(b, operand)?), - _ => { - let k = i32::try_from(operand).map_err(|_| Error::Argument)?; - Brep::made(ffi::history::tag(self.cpp()?, k).map_err(error)?) - } - }; - t.operand = Some(operand); - Ok(t) + core::history::tag(self.core(), operand).map(Brep::made_core) } /// The history of this body, made by an operation from tagged operands: each face's and - /// edge's origins among them. Faces and edges no tag reached have none. Read in the kernel - /// that made the body: the tags never cross between them. + /// edge's origins among them. Faces and edges no tag reached have none. pub fn history(&self) -> Result { - if self.maker == route::Backend::Solidcore { - return core::history::read(self.core()?); - } - let v = ffi::history::read(self.cpp()?).map_err(error)?; - Raw::decode(&v).ok_or(Error::Exception) + core::history::read(self.core()) } } impl Raw { - /// This history with its faces in a canonical order (by origins, then change) and each edge - /// naming its faces by that order, the edges sorted: two kernels' histories of one result - /// compare equal whatever order each lists its faces and edges in. - #[cfg(all(test, feature = "native"))] - pub(crate) fn canonical(&self) -> Raw { - let mut order: Vec = (0..self.faces.len()).collect(); - order.sort_by(|a, b| { - let (fa, fb) = (&self.faces[*a], &self.faces[*b]); - (&fa.origins, fa.change as u8).cmp(&(&fb.origins, fb.change as u8)) - }); - let mut at = vec![0; order.len()]; - for (new, old) in order.iter().enumerate() { - at[*old] = new; - } - let faces = order.iter().map(|i| self.faces[*i].clone()).collect(); - let mut edges: Vec = self - .edges - .iter() - .map(|e| { - let mut f: Vec = e.faces.iter().map(|i| at[*i]).collect(); - f.sort(); - RawEdge { faces: f, origins: e.origins.clone() } - }) - .collect(); - edges.sort_by(|a, b| (&a.origins, &a.faces).cmp(&(&b.origins, &b.faces))); - Raw { faces, edges } - } - - /// Decodes `smlib_history_read`'s records; `None` where they don't read as its layout. - fn decode(v: &[i64]) -> Option { - let mut r = Reader(v.iter()); - if r.next()? != usize::try_from(ffi::history::VERSION).ok()? { - return None; - } - let (nf, ne) = (r.next()?, r.next()?); - // Every entry takes at least two values: no more entries than values. - if nf.saturating_add(ne) > v.len() { - return None; - } - let mut faces = Vec::with_capacity(nf); - for _ in 0..nf { - let change = match r.next()? { - 0 => Change::Kept, - 1 => Change::Modified, - 2 => Change::Split, - 3 => Change::Merged, - 4 => Change::Generated, - _ => return None, - }; - let n = r.next()?; - faces.push(RawFace { change, origins: r.pairs(n)? }); - } - let mut edges = Vec::with_capacity(ne); - for _ in 0..ne { - let m = r.next()?; - let bounds = (0..m).map(|_| r.next()).collect::>>()?; - let n = r.next()?; - edges.push(RawEdge { faces: bounds, origins: r.pairs(n)? }); - } - r.0.next().is_none().then_some(Raw { faces, edges }) - } - /// As a [`History`] over `operands.len()` operands, every index renumbered into canonical /// order: operand k's through `operands[k]`, the result's through `result`. An edge with no /// origin is where its two faces meet; one bounding other than two faces, or a face new to @@ -300,23 +222,5 @@ pub fn blends(h: &mut History, operand: &[EdgeProv], picked: &[usize], taken_up: } } -/// Reads non-negative values off the records. -struct Reader<'a>(std::slice::Iter<'a, i64>); - -impl Reader<'_> { - fn next(&mut self) -> Option { - self.0.next().and_then(|x| usize::try_from(*x).ok()) - } - - /// `n` pairs; `None` past the end (so a huge `n` costs nothing). - fn pairs(&mut self, n: usize) -> Option> { - let mut v = Vec::with_capacity(n.min(self.0.len() / 2)); - for _ in 0..n { - v.push((self.next()?, self.next()?)); - } - Some(v) - } -} - #[cfg(test)] mod tests; diff --git a/crates/smlib/src/history/tests.rs b/crates/smlib/src/history/tests.rs index 7282aa61d5..3cb8e0687b 100644 --- a/crates/smlib/src/history/tests.rs +++ b/crates/smlib/src/history/tests.rs @@ -1,49 +1,32 @@ use super::*; +use crate::Error; use solidcraft_geom::brep::history::{Change, EdgeOrigin, EdgeProv, FaceOrigin, FaceProv, History}; fn face(operand: usize, face: usize) -> FaceOrigin { FaceOrigin::Face { operand, face } } -/// Two boxes joined across a face, as `smlib_history_read` lays it out: face 0 is operand 0's -/// face 2 kept, face 1 operand 1's face 4 trimmed, face 2 both boxes' coplanar faces 0 merged. -/// Edge 0 is new between faces 0 and 1, edge 1 is operand 0's edge 7, edge 2 joins edge 3 of -/// each box and bounds faces 1 and 2. -const RECORDS: &[i64] = &[ - 1, 3, 3, // - 0, 1, 0, 2, // - 1, 1, 1, 4, // - 3, 2, 0, 0, 1, 0, // - 2, 0, 1, 0, // - 2, 0, 2, 1, 0, 7, // - 2, 1, 2, 2, 0, 3, 1, 3, -]; - -#[test] -fn decodes_the_c_layout() { - let raw = Raw::decode(RECORDS).unwrap(); - assert_eq!(raw.faces.len(), 3); - assert_eq!(raw.faces[2], RawFace { change: Change::Merged, origins: vec![(0, 0), (1, 0)] }); - assert_eq!(raw.edges[0], RawEdge { faces: vec![0, 1], origins: vec![] }); - assert_eq!(raw.edges[2].origins, vec![(0, 3), (1, 3)]); -} - -#[test] -fn hostile_records_are_refused() { - // Wrong version, truncated, trailing values, counts far past the data, an unknown change. - assert_eq!(Raw::decode(&[2, 0, 0]), None); - assert_eq!(Raw::decode(&RECORDS[..RECORDS.len() - 1]), None); - assert_eq!(Raw::decode(&[RECORDS, &[0]].concat()), None); - assert_eq!(Raw::decode(&[1, i64::MAX, 0]), None); - assert_eq!(Raw::decode(&[1, 1, 0, 0, i64::MAX]), None); - assert_eq!(Raw::decode(&[1, 1, 0, 9, 0]), None); - assert_eq!(Raw::decode(&[1, 1, 0, 0, -1]), None); - assert_eq!(Raw::decode(&[]), None); +/// Two boxes joined across a face: face 0 is operand 0's face 2 kept, face 1 operand 1's face 4 +/// trimmed, face 2 both boxes' coplanar faces 0 merged. Edge 0 is new between faces 0 and 1, edge +/// 1 is operand 0's edge 7, edge 2 joins edge 3 of each box and bounds faces 1 and 2. +fn joined() -> Raw { + Raw { + faces: vec![ + RawFace { change: Change::Kept, origins: vec![(0, 2)] }, + RawFace { change: Change::Modified, origins: vec![(1, 4)] }, + RawFace { change: Change::Merged, origins: vec![(0, 0), (1, 0)] }, + ], + edges: vec![ + RawEdge { faces: vec![0, 1], origins: vec![] }, + RawEdge { faces: vec![0, 2], origins: vec![(0, 7)] }, + RawEdge { faces: vec![1, 2], origins: vec![(0, 3), (1, 3)] }, + ], + } } #[test] fn new_edges_are_where_their_faces_meet() { - let raw = Raw::decode(RECORDS).unwrap(); + let raw = joined(); let h = raw.history(&[Order::default(), Order::default()], &Order::default()).unwrap(); assert_eq!(h.operands, 2); assert_eq!(h.faces[0].origins, vec![face(0, 2)]); @@ -59,7 +42,8 @@ fn new_edges_are_where_their_faces_meet() { /// pieces of one face, where nothing met, so it comes from nothing. #[test] fn a_split_line_comes_from_nothing() { - let raw = Raw::decode(&[1, 2, 1, 2, 1, 0, 0, 2, 1, 0, 0, 2, 0, 1, 0]).unwrap(); + let piece = RawFace { change: Change::Split, origins: vec![(0, 0)] }; + let raw = Raw { faces: vec![piece.clone(), piece], edges: vec![RawEdge { faces: vec![0, 1], origins: vec![] }] }; let h = raw.history(&[Order::default()], &Order::default()).unwrap(); assert_eq!(h.faces.iter().map(|f| (f.origins.clone(), f.change)).collect::>(), vec![(vec![face(0, 0)], Change::Split); 2]); assert_eq!(h.edges[0].faces, vec![0, 1]); @@ -68,7 +52,7 @@ fn a_split_line_comes_from_nothing() { #[test] fn renumbers_into_canonical_order() { - let raw = Raw::decode(RECORDS).unwrap(); + let raw = joined(); // Operand 0's SMLib face 2 is its canonical face 5, edge 7 its edge 0; the result's faces // and edges are listed backwards. let a = Order { faces: vec![1, 2, 5, 0, 3, 4], edges: vec![1, 2, 3, 4, 5, 6, 7, 0], pieces: vec![] }; @@ -85,7 +69,7 @@ fn renumbers_into_canonical_order() { #[test] fn bad_orders_are_errors() { - let raw = Raw::decode(RECORDS).unwrap(); + let raw = joined(); let ok = Order::default(); // An origin on an operand that isn't there. assert!(raw.history(std::slice::from_ref(&ok), &ok).is_err()); @@ -112,7 +96,6 @@ fn a_fillet_face_comes_from_its_edge() { assert!(h.faces[..6].iter().all(|f| f.origins.len() == 1)); } -#[cfg(feature = "native")] #[test] fn a_tagged_body_is_its_own_history() { let b = Brep::make_box([0.0; 3], [1.0, 2.0, 3.0]).unwrap(); diff --git a/crates/smlib/src/import.rs b/crates/smlib/src/import.rs index aa346229c9..5968f057dd 100644 --- a/crates/smlib/src/import.rs +++ b/crates/smlib/src/import.rs @@ -1,6 +1,6 @@ //! 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`. +//! API's flat arrays (`capi/include/smlib.h`, "Flat B-rep arrays"), which solidcore builds into +//! its B-rep ([`crate::core::from_arrays`]). //! //! 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 @@ -666,12 +666,9 @@ mod analytic; mod tests; impl crate::Brep { - /// A body of the kernel in force ([`crate::route::backend`]), from flat arrays. + /// A body from flat arrays. pub(crate) fn from_arrays(a: &Arrays) -> crate::Result { - match crate::route::backend() { - crate::route::Backend::Smlib => crate::flat::Flat::from_arrays(a)?.import(), - crate::route::Backend::Solidcore => crate::core::from_arrays(a).map(crate::Brep::made_core), - } + crate::core::from_arrays(a).map(crate::Brep::made_core) } /// The SMLib B-rep of a schema 4 record. `trusted` as in [`arrays`]. diff --git a/crates/smlib/src/import/tests.rs b/crates/smlib/src/import/tests.rs index cf96de0d29..8730f29b5f 100644 --- a/crates/smlib/src/import/tests.rs +++ b/crates/smlib/src/import/tests.rs @@ -117,7 +117,6 @@ fn records_smlib_cannot_take_are_errors() { 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(); @@ -126,7 +125,6 @@ fn a_cube_record_imports_into_smlib() { assert!(b.is_manifold_solid().unwrap()); } -#[cfg(feature = "native")] #[test] fn a_plane_on_other_axes_goes_as_smlibs_plane() { // A tetrahedron: its faces' natural frames are unit and orthogonal, so make one plane's axes diff --git a/crates/smlib/src/lib.rs b/crates/smlib/src/lib.rs index 213ac9708c..daf70944d1 100644 --- a/crates/smlib/src/lib.rs +++ b/crates/smlib/src/lib.rs @@ -1,33 +1,21 @@ -//! NVIDIA's SMLib B-rep kernel (tangency-labs/smlib, Apache-2.0), statically linked behind its C -//! API, with a safe Rust wrapper. Only `crates/kernel` uses this crate (ADR 0002). +//! The B-rep kernel, solidcore (tangency-labs/solidcore, the Rust port of NVIDIA's SMLib, +//! Apache-2.0), behind a safe wrapper. Only `crates/kernel` uses this crate (ADR 0002). //! -//! The C API never lets a C++ exception, type or pointer out, never changes its inputs, and -//! serialises every call behind one lock (SMLib keeps process-global state). This wrapper owns -//! the handles, turns status codes into [`Error`], and sends SMLib's one-line-per-call log to the -//! `log` crate (target `smlib`). -//! -//! The `native` feature (the apps' `smlib`, off by default) compiles and links the C++ kernel. -//! Without it the crate needs no C++ toolchain, no SMLib sources and no prebuilt library: solidcore -//! answers every operation, and asking for the C++ kernel (`SOLIDCRAFT_KERNEL=smlib`) makes every -//! call that would reach it return [`Error::NotBuilt`], never the other kernel's answer. +//! Every call runs in a solidcore kernel of its own, never changes its inputs, turns solidcore's +//! errors into [`Error`] and catches its panics ([`Error::Internal`]). #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use std::ffi::CString; use std::path::Path; -use std::sync::atomic::{AtomicI32, Ordering}; -use std::sync::{Once, OnceLock}; mod check; mod core; pub mod export; -mod ffi; pub mod flat; pub mod history; pub mod import; mod measure; mod ops; pub mod pcurves; -pub mod route; mod sheets; mod split; pub mod ssi; @@ -36,7 +24,7 @@ pub mod trim; pub use check::{CheckLevel, CheckReport}; pub use measure::{MEASURE_ACCURACY, MassProperties, PointClass}; -pub use ops::{Blend, BooleanOp, ChamferSecond, Fit, Hole, HoleEdge, MAX_POINTS, MAX_UNION_BREPS, blend_fit}; +pub use ops::{Blend, BooleanOp, ChamferSecond, Hole, HoleEdge, MAX_POINTS, MAX_UNION_BREPS}; /// Why an SMLib call failed. #[derive(Clone, Debug, PartialEq)] @@ -75,21 +63,17 @@ pub enum Error { NotExact, /// A drafted extrusion could not close the corner (or wall) at `at` on its profile, for `why`. Corner { why: sweeps::CornerRefusal, at: [f64; 3] }, - /// A C++ exception (most likely out of memory) was caught at the boundary. + /// solidcore found its own state inconsistent part way through a call (a hole it rebuilt did + /// not close). Exception, - /// The C API is a different version from the one this crate was written against. - Version(i32), - /// The C++ kernel was asked for on a build without it (the `native` feature, which the apps' - /// `smlib` feature turns on, is off): [`NOT_BUILT`]. - NotBuilt, /// A B-rep record SMLib cannot take (see [`import::Unsupported`]). Record(import::Unsupported), /// solidcore reported an error (the call, then its message). Solidcore(String), /// solidcore panicked; the guard caught it (the call, then the panic's message). Internal(String), - /// A step of the C++ kernel solidcore has not ported yet, refused rather than built another - /// way: what is missing, and the solidcore issue that ports it. + /// A step of SMLib solidcore has not ported yet, refused rather than built another way: what + /// is missing, and the solidcore issue that ports it. Unported(&'static str), } @@ -125,8 +109,6 @@ 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_BUILT}"), Error::Record(u) => write!(f, "{u}"), Error::Solidcore(e) => write!(f, "solidcore: {e}"), Error::Internal(e) => write!(f, "solidcore failed internally in {e}"), @@ -139,137 +121,19 @@ impl std::error::Error for Error {} pub type Result = std::result::Result; -/// What [`Error::NotBuilt`] says: the C++ kernel is opt-in at build time, and how to opt in. -pub const NOT_BUILT: &str = "this build doesn't include the C++ SMLib kernel; build with --features smlib"; - -/// Whether this build links the C++ SMLib kernel (the `native` feature). -pub const fn built() -> bool { - cfg!(feature = "native") -} - -fn error(s: i32) -> Error { - match s { - ffi::ERR_ARG | ffi::ERR_BUFFER => Error::Argument, - ffi::ERR_EMPTY => Error::Empty, - ffi::ops::ERR_NOT_SOLID => Error::NotSolid, - ffi::ops::ERR_NO_ROOM => Error::NoRoom, - ffi::ops::ERR_WALLS_TOUCH => Error::WallsTouch, - ffi::ops::ERR_UNSUPPORTED => Error::Unsupported, - ffi::ops::ERR_CORNER_UNSUPPORTED => Error::CornerUnsupported, - ffi::ops::ERR_NO_EFFECT => Error::NoEffect, - ffi::trim::ERR_NOT_EXACT => Error::NotExact, - ffi::ERR_EXCEPTION => Error::Exception, - ffi::NOT_BUILT => Error::NotBuilt, - s => Error::Status(s), - } -} - -fn check(s: i32) -> Result<()> { - if s == ffi::OK { Ok(()) } else { Err(error(s)) } -} - -fn c_path(p: &Path) -> Result { - CString::new(p.to_string_lossy().as_bytes()).map_err(|_| Error::Argument) -} - -/// One C API call's log line, `smlib_fn(args) -> status (1.234 ms)`: the function and the time -/// the C API measured for it (inside its lock, so waiting for another thread's call is not in it). -fn call_time(line: &str) -> Option<(&str, std::time::Duration)> { - let name = line.get(..line.find('(')?)?; - if !name.starts_with("smlib_") || !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') { - return None; - } - let tail = line.strip_suffix(" ms)")?; - let ms: f64 = tail.get(tail.rfind(" (")? + 2..)?.parse().ok()?; - (ms.is_finite() && ms >= 0.0).then(|| (name, std::time::Duration::from_secs_f64(ms / 1000.0))) -} - -fn log_line(level: i32, text: String) { - // Every call's log line carries the C API's own timing: the profiler's SMLib spans. - if solidcraft_profile::enabled() - && let Some((name, took)) = call_time(&text) - { - solidcraft_profile::record("smlib", solidcraft_profile::intern(name), took); - } - if level == 0 { - log::debug!(target: "smlib", "{text}"); - } else { - log::warn!(target: "smlib", "{text}"); - } -} - -static INIT: Once = Once::new(); -/// What [`init`] found, kept for every later call (0: ready). -static INIT_RESULT: AtomicI32 = AtomicI32::new(0); - -/// Checks the C API's version, creates SMLib's context and routes its log to the `log` crate. -/// Every other call runs it first; calling it at start-up keeps the cost out of the first operation. -pub fn init() -> Result<()> { - #[cfg(test)] - route::reached_cpp(); - INIT.call_once(|| { - let v = ffi::abi_version(); - let result = if v == ffi::NOT_BUILT { - v - } else if v != ffi::ABI_VERSION { - v.saturating_add(10_000) - } else { - ffi::set_log(Some(log_line)); - ffi::init() - }; - INIT_RESULT.store(result, Ordering::Release); - }); - match INIT_RESULT.load(Ordering::Acquire) { - ffi::OK => Ok(()), - v if v >= 10_000 => Err(Error::Version(v - 10_000)), - s => Err(error(s)), - } -} - -/// Where failing calls leave their inputs for replay, on either kernel (see the C API's -/// `smlib_set_dump_dir`); `None` turns that off. A build without the C++ kernel has only solidcore's. -pub fn set_dump_dir(dir: Option<&Path>) -> Result<()> { - let c = dir.map(c_path).transpose()?; +/// Where failing calls leave their inputs for replay ([`core::dump`]); `None` turns that off. +pub fn set_dump_dir(dir: Option<&Path>) { core::dump::set_dir(dir.map(Path::to_path_buf)); - // A build or a run on solidcore alone never starts the C++ kernel to tell it. - if route::backend() != route::Backend::Smlib { - return Ok(()); - } - match init() { - Ok(()) => check(ffi::set_dump_dir(c.as_deref())), - Err(Error::NotBuilt) => Ok(()), - Err(e) => Err(e), - } } -/// The kernel call running right now, on either kernel, if any: for crash reports. Takes no lock. +/// The kernel call running right now, if any: for crash reports. Takes no lock. pub fn current_call() -> Option { - if let Some(call) = core::dump::current() { - return Some(call); - } - if route::backend() != route::Backend::Smlib { - return None; - } - let mut buf = [0u8; 512]; - let n = ffi::current_call(&mut buf); - let s = buf.get(..n).map(|b| String::from_utf8_lossy(b).into_owned()).unwrap_or_default(); - (!s.is_empty()).then_some(s) + core::dump::current() } -/// A B-rep in either kernel ([`route`]). Immutable: operations return new bodies. A body made -/// by one kernel is converted for the other the first time an operation there needs it, through the -/// flat arrays both understand, and keeps both. A process has one kernel at a time, and this stays -/// because that kernel can change under a live body: changing the preference or `diagnostics.kernel` -/// leaves bodies already computed as they are, so the next operation on one needs it on the new -/// kernel. The parity tests take solidcore's bodies to the C++ kernel the same way. +/// A B-rep. Immutable: operations return new bodies. pub struct Brep { - cpp: OnceLock>, - core: OnceLock>, - /// The kernel that made it, whose tags [`Brep::history`] reads (the arrays carry none). - maker: route::Backend, - /// `Some(k)` for [`Brep::tagged`]`(k)`: its faces and edges stand for their own positions in - /// operand `k`, so the body converted for the other kernel is tagged the same way there. - operand: Option, + body: core::Body, } impl std::fmt::Debug for Brep { @@ -279,83 +143,34 @@ impl std::fmt::Debug for Brep { } impl Brep { - /// A body the C++ kernel made. - fn made(h: ffi::Handle) -> Brep { - #[cfg(test)] - route::reached_cpp(); - Brep { cpp: OnceLock::from(Ok(h)), core: OnceLock::new(), maker: route::Backend::Smlib, operand: None } - } - /// A body solidcore made. - fn made_core(b: core::Body) -> Brep { - Brep { cpp: OnceLock::new(), core: OnceLock::from(Ok(b)), maker: route::Backend::Solidcore, operand: None } + fn made_core(body: core::Body) -> Brep { + Brep { body } } - /// The C++ kernel's body: converted from solidcore's the first time it is asked for (and - /// tagged as solidcore's is, for a [`Brep::tagged`] body). - fn cpp(&self) -> Result<&ffi::Handle> { - #[cfg(test)] - route::reached_cpp(); - let made = self.cpp.get_or_init(|| { - let core = self.core.get().ok_or(Error::Empty)?.as_ref().map_err(Clone::clone)?; - let a = core::to_arrays(core)?; - let h = flat::Flat::from_arrays(&a)?.import()?.cpp.into_inner().unwrap_or(Err(Error::Empty))?; - match self.operand { - Some(k) => ffi::history::tag(&h, i32::try_from(k).map_err(|_| Error::Argument)?).map_err(error), - None => Ok(h), - } - }); - made.as_ref().map_err(Clone::clone) - } - - /// solidcore's body: converted from the C++ kernel's the first time it is asked for (and - /// tagged as the C++ kernel's is, for a [`Brep::tagged`] body). - fn core(&self) -> Result<&core::Body> { - let made = self.core.get_or_init(|| { - self.cpp.get().ok_or(Error::Empty)?.as_ref().map_err(Clone::clone)?; - let b = core::from_arrays(&self.cpp_arrays()?)?; - match self.operand { - Some(k) => core::history::tag(&b, k), - None => Ok(b), - } - }); - made.as_ref().map_err(Clone::clone) + /// solidcore's body. + fn core(&self) -> &core::Body { + &self.body } /// An axis-aligned box from its lower corner, with positive edge lengths (mm). pub fn make_box(origin: [f64; 3], size: [f64; 3]) -> Result { - if route::backend() == route::Backend::Solidcore { - return core::make_box(origin, size).map(Brep::made_core); - } - init()?; - ffi::make_box(&origin, &size).map(Brep::made).map_err(error) + core::make_box(origin, size).map(Brep::made_core) } /// Reads a native SMLib `.smb` file. pub fn read(path: &Path, ascii: bool) -> Result { - if route::backend() == route::Backend::Solidcore { - c_path(path)?; - return core::file::read(path, ascii).map(Brep::made_core); - } - init()?; - ffi::read(&c_path(path)?, ascii).map(Brep::made).map_err(error) + core::file::read(path, ascii).map(Brep::made_core) } /// Writes this body to a native SMLib `.smb` file. pub fn write(&self, path: &Path, ascii: bool) -> Result<()> { - if route::backend() == route::Backend::Solidcore { - c_path(path)?; - return core::file::write(self.core()?, path, ascii); - } - check(ffi::write(self.cpp()?, &c_path(path)?, ascii)) + core::file::write(self.core(), path, ascii) } /// A deep copy. pub fn try_clone(&self) -> Result { - if route::backend() == route::Backend::Solidcore { - return core::transform::copy(self.core()?).map(Brep::made_core); - } - ffi::copy(self.cpp()?).map(Brep::made).map_err(error) + core::transform::copy(self.core()).map(Brep::made_core) } /// This body moved by the affine `matrix` (column-major: columns 0–2 the images of x, y and @@ -363,27 +178,17 @@ impl Brep { /// mirror it, a non-uniform scale or a shear turns its geometry into NURBS, tolerances scale /// with it. pub fn transformed(&self, matrix: &[[f64; 4]; 4]) -> Result { - if route::backend() == route::Backend::Solidcore { - return core::transform::transform(self.core()?, matrix).map(Brep::made_core); - } - ffi::transform(self.cpp()?, matrix).map(Brep::made).map_err(error) + core::transform::transform(self.core(), matrix).map(Brep::made_core) } /// The enclosed volume (mm³) at `relative_accuracy` (SMLib clamps it to [1e-8, 1e-1]). pub fn volume(&self, relative_accuracy: f64) -> Result { - if route::backend() == route::Backend::Solidcore { - return core::volume(self.core()?, relative_accuracy); - } - ffi::volume(self.cpp()?, relative_accuracy).map_err(error) + core::volume(self.core(), relative_accuracy) } /// Face, edge and vertex counts. pub fn counts(&self) -> Result<[usize; 3]> { - if route::backend() == route::Backend::Solidcore { - return core::counts(self.core()?); - } - let c = ffi::counts(self.cpp()?).map_err(error)?; - Ok(c.map(|n| usize::try_from(n).unwrap_or(0))) + core::counts(self.core()) } } diff --git a/crates/smlib/src/measure.rs b/crates/smlib/src/measure.rs index 0a878ec562..71e2b664ba 100644 --- a/crates/smlib/src/measure.rs +++ b/crates/smlib/src/measure.rs @@ -2,12 +2,7 @@ //! surfaces rather than a tessellation; the exact box; how far an edge lies from a face's surface; //! where a point lies against a body. -use crate::{Brep, Result, core, error, ffi, route}; - -/// Whether the Measure family runs on solidcore ([`route`]). -fn on_core() -> bool { - route::backend() == route::Backend::Solidcore -} +use crate::{Brep, Result, core}; /// The accuracy [`Brep::mass_properties`] and [`Brep::area`] use unless told otherwise: the /// finest SMLib allows. Analytic primitives then come out within 2e-7 of their closed forms. @@ -55,16 +50,7 @@ impl Brep { /// else inside a material region or outside. A body with no material (a sheet, an open shell) /// has no inside. A non-finite coordinate is [`Error::Argument`](crate::Error::Argument). pub fn classify_point(&self, p: [f64; 3]) -> Result { - if on_core() { - return core::classify_point(self.core()?, p); - } - match ffi::measure::classify_point(self.cpp()?, p).map_err(error)? { - ffi::measure::POINT_OUTSIDE => Ok(PointClass::Outside), - ffi::measure::POINT_INSIDE => Ok(PointClass::Inside), - ffi::measure::POINT_ON_BOUNDARY => Ok(PointClass::OnBoundary), - // The C API writes only the three above; any other value is passed on as it came. - other => Err(crate::Error::Status(other)), - } + core::classify_point(self.core(), p) } /// Mass properties at `relative_accuracy` (clamped to [1e-8, 1e-1]; [`MEASURE_ACCURACY`] is @@ -72,21 +58,13 @@ impl Brep { /// touching along edges or at points, measure too. A body with no material (a sheet, an open /// shell) has volume 0 and no inertia, its faces' area and the centroid of that area. pub fn mass_properties(&self, relative_accuracy: f64) -> Result { - if on_core() { - return core::mass_properties(self.core()?, relative_accuracy); - } - let m = ffi::measure::mass_properties(self.cpp()?, relative_accuracy).map_err(error)?; - let [area, volume, cx, cy, cz, ixx, iyy, izz, iyz, izx, ixy] = m; - Ok(MassProperties { area, volume, centroid: [cx, cy, cz], moments: [ixx, iyy, izz], products: [iyz, izx, ixy] }) + core::mass_properties(self.core(), relative_accuracy) } /// The exact axis-aligned box, least corner then greatest: the extrema of every vertex, edge /// and face on its exact curve or surface, not grown by tolerances or control hulls. pub fn bounds(&self) -> Result<([f64; 3], [f64; 3])> { - if on_core() { - return core::bounds(self.core()?); - } - ffi::measure::bounds(self.cpp()?).map_err(error) + core::bounds(self.core()) } /// How far edge `edge` (its curve over its interval) lies from the surface of face `face`, @@ -96,23 +74,16 @@ impl Brep { /// goes; SMLib keeps such an edge whole when making its UV trim only when its zone tolerance /// (half the flat arrays' `EDGE_TOLERANCE`) covers this. pub fn edge_face_distance(&self, edge: usize, face: usize) -> Result { - if on_core() { - return core::edge_face_distance(self.core()?, edge, face); - } - let (Ok(e), Ok(f)) = (i64::try_from(edge), i64::try_from(face)) else { return Err(error(ffi::ERR_ARG)) }; - ffi::measure::edge_face_distance(self.cpp()?, e, f).map_err(error) + core::edge_face_distance(self.core(), edge, face) } /// Total area of every face, each counted once, so sheets and open shells too. pub fn area(&self, relative_accuracy: f64) -> Result { - if on_core() { - return core::area(self.core()?, relative_accuracy); - } - ffi::measure::area(self.cpp()?, relative_accuracy).map_err(error) + core::area(self.core(), relative_accuracy) } } -/// The closed forms of the primitives' measures, for the tests of both kernels. +/// The closed forms of the primitives' measures, for the tests. #[cfg(test)] pub(crate) mod closed { use std::f64::consts::PI; @@ -206,7 +177,7 @@ pub(crate) mod closed { } } -#[cfg(all(test, feature = "native"))] +#[cfg(test)] mod tests { use super::*; use crate::Error; @@ -266,7 +237,7 @@ mod tests { use closed::Closed; - /// A primitive the C++ kernel made, with its closed forms. + /// A primitive with its closed forms. struct Expected { name: &'static str, body: Brep, diff --git a/crates/smlib/src/ops.rs b/crates/smlib/src/ops.rs index f782ee6594..9e5fb506ee 100644 --- a/crates/smlib/src/ops.rs +++ b/crates/smlib/src/ops.rs @@ -6,41 +6,7 @@ //! edge (face) nearest to it, which must lie within `tolerance` of it and be nearer than any other //! by more than the body's modelling tolerance ([`Error::NoMatch`], [`Error::Ambiguous`]). -use crate::{Brep, Error, Result, core, error, ffi, init, route}; - -/// What size builds a blend SMLib refused ([`blend_fit`]): from the refusal's own cause, never by -/// building again. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Fit { - /// The largest size (a round's radius, a chamfer's distance, a chord's width) that builds. - Size(f64), - /// The largest setback of a set-back corner that fits (the one asked for is too long). - Setback(f64), - /// The factor (< 1) a two-distance chamfer's distances are both scaled by to build. - Scale(f64), - /// The smallest setback that builds the corner (the one asked for is too short: its fill is - /// certified convex only from there). - SetbackAtLeast(f64), -} - -/// The size that builds the last blend this process's SMLib refused as too large -/// ([`Error::NoRoom`]) or whose setback it refused as too short; `None` where it names no single -/// size. Read it right after the refused call: SMLib keeps only the last one. -pub fn blend_fit() -> Option { - // solidcore names no size that builds yet: its refusals keep their own words, and the C++ - // kernel's last refusal is never read for one of solidcore's. - if route::backend() == route::Backend::Solidcore { - return None; - } - let (size, what) = ffi::ops::blend_fit_size()?; - match what { - ffi::ops::FIT_SIZE => Some(Fit::Size(size)), - ffi::ops::FIT_SETBACK => Some(Fit::Setback(size)), - ffi::ops::FIT_SCALE => Some(Fit::Scale(size)), - ffi::ops::FIT_SETBACK_AT_LEAST => Some(Fit::SetbackAtLeast(size)), - _ => None, - } -} +use crate::{Brep, Error, Result, core}; /// How [`Brep::boolean`] combines two bodies. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -75,27 +41,12 @@ pub enum ChamferSecond { Angle(f64), } -/// Most bodies [`Brep::union_all`] takes (the C API's limit). +/// Most bodies [`Brep::union_all`] takes. pub const MAX_UNION_BREPS: usize = 100_000; -/// Most points one selection may hold (the C API's limit). +/// Most points one selection may hold. pub const MAX_POINTS: usize = 4096; -pub(crate) fn pick_error((s, at): ffi::ops::PickError) -> Error { - match (s, at) { - (ffi::ops::ERR_PICK_NONE, Some(point)) => Error::NoMatch { point }, - (ffi::ops::ERR_PICK_AMBIGUOUS, Some(point)) => Error::Ambiguous { point }, - (s, _) => error(s), - } -} - -pub(crate) fn setback_error((s, at, corner): ffi::ops::SetbackError) -> Error { - match corner { - Some(corner) => Error::CornerRefused { corner }, - None => pick_error((s, at)), - } -} - /// One edge round an opened corner's hole ([`Brep::blend_setback_open`]): its index in the /// sheet's edge order, the face beside it in its face order, and whether that face, run /// counter-clockwise about its outward normal, runs the edge from its start vertex to its end. @@ -112,44 +63,13 @@ pub struct Hole { pub edges: Vec, } -/// The holes `smlib_blend_setback_open` wrote: `corners` of them, each n and n triples. -fn holes(values: &[i64], corners: usize) -> Option> { - let mut at = 0usize; - let mut out = Vec::with_capacity(corners); - for _ in 0..corners { - let n = usize::try_from(*values.get(at)?).ok()?; - at += 1; - let mut edges = Vec::with_capacity(n); - for _ in 0..n { - let (e, f, fw) = (values.get(at)?, values.get(at + 1)?, values.get(at + 2)?); - edges.push(HoleEdge { edge: usize::try_from(*e).ok()?, face: usize::try_from(*f).ok()?, forward: *fw != 0 }); - at += 3; - } - out.push(Hole { edges }); - } - (at == values.len()).then_some(out) -} - impl Brep { /// `self` and `other` combined by `op`: `None` when the result is empty (disjoint bodies /// intersected, a body subtracted by one that encloses it). Several separate solids come back /// as one body, counted by [`Brep::solid_count`]. Tagged operands ([`Brep::tagged`]) give a /// result whose [`Brep::history`] says where each face and edge came from. pub fn boolean(&self, other: &Brep, op: BooleanOp) -> Result> { - if route::backend() == route::Backend::Solidcore { - return core::boolean::boolean(self.core()?, other.core()?, op).map(|r| r.map(Brep::made_core)); - } - init()?; - let op = match op { - BooleanOp::Union => ffi::ops::BOOLEAN_UNION, - BooleanOp::Subtract => ffi::ops::BOOLEAN_SUBTRACT, - BooleanOp::Intersect => ffi::ops::BOOLEAN_INTERSECT, - }; - match ffi::ops::boolean(self.cpp()?, other.cpp()?, op) { - Ok(h) => Ok(Some(Brep::made(h))), - Err(ffi::ERR_EMPTY) => Ok(None), - Err(s) => Err(error(s)), - } + core::boolean::boolean(self.core(), other.core(), op).map(|r| r.map(Brep::made_core)) } /// The union of `breps` in one call: they are unioned pairwise, level by level, so none takes @@ -157,81 +77,43 @@ impl Brep { /// tagged each with its own number ([`Brep::tagged`]) give a result whose [`Brep::history`] /// names them. At least one body and at most [`MAX_UNION_BREPS`]. pub fn union_all(breps: &[&Brep]) -> Result { - if route::backend() == route::Backend::Solidcore { - if breps.is_empty() || breps.len() > MAX_UNION_BREPS { - return Err(Error::Argument); - } - let bodies = breps.iter().map(|b| b.core()).collect::>>()?; - return core::boolean::union_all(&bodies).map(Brep::made_core); - } - init()?; if breps.is_empty() || breps.len() > MAX_UNION_BREPS { - return Err(error(ffi::ERR_ARG)); + return Err(Error::Argument); } - let handles = breps.iter().map(|b| b.cpp()).collect::>>()?; - ffi::ops::boolean_union_all(&handles).map(Brep::made).map_err(error) + let bodies: Vec<&core::Body> = breps.iter().map(|b| b.core()).collect(); + core::boolean::union_all(&bodies).map(Brep::made_core) } /// `self` and `other`, sheets of planar faces on one plane ([`Brep::planar_faces`]), combined /// as regions of that plane by `op`: `None` when the result is empty. The result's edges are /// the inputs' own curves, split where they cross. pub fn boolean2d(&self, other: &Brep, op: BooleanOp) -> Result> { - if route::backend() == route::Backend::Solidcore { - return core::boolean::boolean2d(self.core()?, other.core()?, op).map(|r| r.map(Brep::made_core)); - } - init()?; - let op = match op { - BooleanOp::Union => ffi::ops::BOOLEAN_UNION, - BooleanOp::Subtract => ffi::ops::BOOLEAN_SUBTRACT, - BooleanOp::Intersect => ffi::ops::BOOLEAN_INTERSECT, - }; - match ffi::ops::boolean2d(self.cpp()?, other.cpp()?, op) { - Ok(h) => Ok(Some(Brep::made(h))), - Err(ffi::ERR_EMPTY) => Ok(None), - Err(s) => Err(error(s)), - } + core::boolean::boolean2d(self.core(), other.core(), op).map(|r| r.map(Brep::made_core)) } /// `self` with each face `tool` meets cut along the intersection, the pieces kept as faces of /// one body: `None` when the tool meets no face. Tagged operands give the pieces' history /// (each piece is a split of the face it was cut from). pub fn imprint(&self, tool: &Brep) -> Result> { - if route::backend() == route::Backend::Solidcore { - return core::boolean::imprint(self.core()?, tool.core()?).map(|r| r.map(Brep::made_core)); - } - init()?; - match ffi::ops::imprint(self.cpp()?, tool.cpp()?) { - Ok(h) => Ok(Some(Brep::made(h))), - Err(ffi::ops::ERR_NO_CUT) => Ok(None), - Err(s) => Err(error(s)), - } + core::boolean::imprint(self.core(), tool.core()).map(|r| r.map(Brep::made_core)) } /// The number of separate solid regions (voids not counted). pub fn solid_count(&self) -> Result { - if route::backend() == route::Backend::Solidcore { - return crate::core::solid_count(self.core()?); - } - ffi::ops::solid_count(self.cpp()?).map(|n| usize::try_from(n).unwrap_or(0)).map_err(error) + crate::core::solid_count(self.core()) } /// The edges nearest to `edges` (each within `tolerance`), exactly those, rounded /// at `radius`. pub fn fillet(&self, edges: &[[f64; 3]], tolerance: f64, radius: f64) -> Result { - if route::backend() == route::Backend::Solidcore { - return self.core_blend(Blend::Round, edges, &[radius], tolerance, &[], &[], core::blend::Corners::Filled); - } - self.pick_op(ffi::ops::Pick::Fillet, edges, tolerance, radius, false) + self.core_blend(Blend::Round, edges, &[radius], tolerance, &[], &[], core::blend::Corners::Filled) } /// The edges nearest to `edges` (each within `tolerance`), exactly those, cut by /// an equal-distance chamfer `distance` back along each face (measured at each edge's /// midpoint), with bevelled corners. pub fn chamfer(&self, edges: &[[f64; 3]], tolerance: f64, distance: f64) -> Result { - if route::backend() == route::Backend::Solidcore { - return self.core_blend(Blend::Chamfer, edges, &[distance], tolerance, &[], &[], core::blend::Corners::Filled); - } - self.pick_op(ffi::ops::Pick::Chamfer, edges, tolerance, distance, false) + self.core_blend(Blend::Chamfer, edges, &[distance], tolerance, &[], &[], core::blend::Corners::Filled) } /// The edges nearest to `edges` (each within `tolerance`), exactly those, blended @@ -240,17 +122,7 @@ impl Brep { if edges.len() > MAX_POINTS || edges.is_empty() || sizes.len() != edges.len() { return Err(Error::Argument); } - if route::backend() == route::Backend::Solidcore { - return self.core_blend(kind, edges, sizes, tolerance, &[], &[], core::blend::Corners::Filled); - } - init()?; - let kind = match kind { - Blend::Round => ffi::ops::BLEND_ROUND, - Blend::Curvature => ffi::ops::BLEND_CURVATURE, - Blend::Chord => ffi::ops::BLEND_CHORD, - Blend::Chamfer => ffi::ops::BLEND_CHAMFER, - }; - ffi::ops::blend(kind, self.cpp()?, edges, sizes, tolerance).map(Brep::made).map_err(pick_error) + self.core_blend(kind, edges, sizes, tolerance, &[], &[], core::blend::Corners::Filled) } /// [`Brep::blend`], with whether each point picked an edge that a larger round, overflowing @@ -260,17 +132,7 @@ impl Brep { if edges.len() > MAX_POINTS || edges.is_empty() || sizes.len() != edges.len() { return Err(Error::Argument); } - if route::backend() == route::Backend::Solidcore { - return core::blend::blend_dropping(self.core()?, kind, edges, sizes, tolerance).map(|(b, d)| (Brep::made_core(b), d)); - } - init()?; - let kind = match kind { - Blend::Round => ffi::ops::BLEND_ROUND, - Blend::Curvature => ffi::ops::BLEND_CURVATURE, - Blend::Chord => ffi::ops::BLEND_CHORD, - Blend::Chamfer => ffi::ops::BLEND_CHAMFER, - }; - ffi::ops::blend_dropping(kind, self.cpp()?, edges, sizes, tolerance).map(|(b, d)| (Brep::made(b), d)).map_err(pick_error) + core::blend::blend_dropping(self.core(), kind, edges, sizes, tolerance).map(|(b, d)| (Brep::made_core(b), d)) } /// [`Brep::blend`] with corner setbacks: each of `corners` picks the vertex nearest it @@ -296,17 +158,7 @@ impl Brep { { return Err(Error::Argument); } - if route::backend() == route::Backend::Solidcore { - return self.core_blend(kind, edges, sizes, tolerance, corners, setbacks, core::blend::Corners::Filled); - } - init()?; - let kind = match kind { - Blend::Round => ffi::ops::BLEND_ROUND, - Blend::Curvature => ffi::ops::BLEND_CURVATURE, - Blend::Chord => ffi::ops::BLEND_CHORD, - Blend::Chamfer => ffi::ops::BLEND_CHAMFER, - }; - ffi::ops::blend_setback(kind, self.cpp()?, edges, sizes, corners, setbacks, tolerance).map(Brep::made).map_err(setback_error) + self.core_blend(kind, edges, sizes, tolerance, corners, setbacks, core::blend::Corners::Filled) } /// [`Brep::blend_setback`] with each set-back corner left open: SMLib's patch between the @@ -332,21 +184,8 @@ impl Brep { { return Err(Error::Argument); } - if route::backend() == route::Backend::Solidcore { - return core::blend::blend_holes(self.core()?, kind, edges, sizes, tolerance, corners, setbacks, core::blend::Corners::Open) - .map(|(b, holes)| (Brep::made_core(b), holes)); - } - init()?; - let kind = match kind { - Blend::Round => ffi::ops::BLEND_ROUND, - Blend::Curvature => ffi::ops::BLEND_CURVATURE, - Blend::Chord => ffi::ops::BLEND_CHORD, - Blend::Chamfer => ffi::ops::BLEND_CHAMFER, - }; - let (h, values) = ffi::ops::blend_setback_open(kind, self.cpp()?, edges, sizes, corners, setbacks, tolerance).map_err(setback_error)?; - // A buffer that does not read as the C API documents it is a buffer error. - let holes = holes(&values, corners.len()).ok_or_else(|| error(ffi::ERR_BUFFER))?; - Ok((Brep::made(h), holes)) + core::blend::blend_holes(self.core(), kind, edges, sizes, tolerance, corners, setbacks, core::blend::Corners::Open) + .map(|(b, holes)| (Brep::made_core(b), holes)) } /// The one edge nearest to `edge` (within `tolerance`) rounded with a radius @@ -354,16 +193,7 @@ impl Brep { /// `radii` over the position along the edge, 0 at its end nearest `start` and 1 at the other /// (the edge curve's parameter scaled to 0..1). pub fn fillet_variable(&self, edge: [f64; 3], tolerance: f64, start: [f64; 3], degree: usize, knots: &[f64], radii: &[f64]) -> Result { - if route::backend() == route::Backend::Solidcore { - return core::blend::fillet_variable(self.core()?, edge, tolerance, start, degree, knots, radii).map(Brep::made_core); - } - init()?; - let degree = i32::try_from(degree).map_err(|_| Error::Argument)?; - ffi::ops::fillet_variable(self.cpp()?, &edge, tolerance, &start, degree, knots, radii).map(Brep::made).map_err(|s| match s { - ffi::ops::ERR_PICK_NONE => Error::NoMatch { point: 0 }, - ffi::ops::ERR_PICK_AMBIGUOUS => Error::Ambiguous { point: 0 }, - s => error(s), - }) + core::blend::fillet_variable(self.core(), edge, tolerance, start, degree, knots, radii).map(Brep::made_core) } /// The edges nearest to `edges` (each within `tolerance`), exactly those, cut by @@ -374,15 +204,7 @@ impl Brep { if edges.is_empty() || edges.len() > MAX_POINTS { return Err(Error::Argument); } - if route::backend() == route::Backend::Solidcore { - return core::blend::chamfer_sides(self.core()?, edges, tolerance, distance, second, first).map(Brep::made_core); - } - init()?; - let (value, angle) = match second { - ChamferSecond::Distance(d) => (d, false), - ChamferSecond::Angle(a) => (a, true), - }; - ffi::ops::chamfer_sides(self.cpp()?, edges, tolerance, distance, value, angle, &first).map(Brep::made).map_err(pick_error) + core::blend::chamfer_sides(self.core(), edges, tolerance, distance, second, first).map(Brep::made_core) } /// Hollowed to a wall of `thickness` (negative: inside the original surface, positive: @@ -391,10 +213,7 @@ impl Brep { /// whose walls would just meet (a hole exactly twice the wall from a side) is /// [`Error::WallsTouch`]. pub fn shell(&self, open_faces: &[[f64; 3]], tolerance: f64, thickness: f64) -> Result { - if route::backend() == route::Backend::Solidcore { - return core::offset::shell(self.core()?, open_faces, tolerance, thickness).map(Brep::made_core); - } - self.pick_op(ffi::ops::Pick::Shell, open_faces, tolerance, thickness, true) + core::offset::shell(self.core(), open_faces, tolerance, thickness).map(Brep::made_core) } /// The solid with the faces nearest to `faces` (each within `tolerance`) moved `distance` @@ -402,21 +221,14 @@ impl Brep { /// faces around them extended or trimmed to meet them. Faces moved in past all of the body /// behind them are [`Error::NoRoom`]. pub fn offset_faces(&self, faces: &[[f64; 3]], tolerance: f64, distance: f64) -> Result { - if route::backend() == route::Backend::Solidcore { - return core::offset::offset_faces(self.core()?, faces, tolerance, distance).map(Brep::made_core); - } - self.pick_op(ffi::ops::Pick::OffsetFaces, faces, tolerance, distance, false) + core::offset::offset_faces(self.core(), faces, tolerance, distance).map(Brep::made_core) } /// A sheet thickened into a solid by `thickness` along its surfaces' normals (negative: /// against them), or by half each way when `symmetric`, with ruled walls along its open /// edges. A closed solid is [`Error::Argument`]. pub fn thicken(&self, thickness: f64, symmetric: bool) -> Result { - if route::backend() == route::Backend::Solidcore { - return core::offset::thicken(self.core()?, thickness, symmetric).map(Brep::made_core); - } - init()?; - ffi::ops::thicken(self.cpp()?, thickness, symmetric).map(Brep::made).map_err(error) + core::offset::thicken(self.core(), thickness, symmetric).map(Brep::made_core) } /// This body with its topological edges and vertices removed (SMLib's @@ -428,11 +240,7 @@ impl Brep { if keep.len() > MAX_POINTS { return Err(Error::Argument); } - if route::backend() == route::Backend::Solidcore { - return core::merge::merge_faces(self.core()?, keep, tolerance).map(Brep::made_core); - } - init()?; - ffi::ops::merge_faces(self.cpp()?, keep, tolerance).map(Brep::made).map_err(pick_error) + core::merge::merge_faces(self.core(), keep, tolerance).map(Brep::made_core) } /// The flat faces nearest to `faces` (each within `tolerance`) tilted by `angle` radians @@ -452,11 +260,7 @@ impl Brep { if faces.is_empty() || faces.len() > MAX_POINTS { return Err(Error::Argument); } - if route::backend() == route::Backend::Solidcore { - return core::offset::draft(self.core()?, faces, tolerance, neutral_origin, neutral_normal, pull, angle).map(Brep::made_core); - } - init()?; - ffi::ops::draft(self.cpp()?, faces, tolerance, &neutral_origin, &neutral_normal, &pull, angle).map(Brep::made).map_err(pick_error) + core::offset::draft(self.core(), faces, tolerance, neutral_origin, neutral_normal, pull, angle).map(Brep::made_core) } /// [`Brep::blend`] and its kin on solidcore (`core::blend::blend`). @@ -474,16 +278,9 @@ impl Brep { if edges.len() > MAX_POINTS || edges.is_empty() { return Err(Error::Argument); } - core::blend::blend(self.core()?, kind, edges, sizes, tolerance, corners, setbacks, open).map(Brep::made_core) + core::blend::blend(self.core(), kind, edges, sizes, tolerance, corners, setbacks, open).map(Brep::made_core) } - fn pick_op(&self, kind: ffi::ops::Pick, points: &[[f64; 3]], tolerance: f64, size: f64, allow_none: bool) -> Result { - init()?; - if points.len() > MAX_POINTS || (points.is_empty() && !allow_none) { - return Err(Error::Argument); - } - ffi::ops::pick_op(kind, self.cpp()?, points, tolerance, size).map(Brep::made).map_err(pick_error) - } } #[cfg(test)] diff --git a/crates/smlib/src/ops_tests.rs b/crates/smlib/src/ops_tests.rs index 785b4f9b73..f52e242b48 100644 --- a/crates/smlib/src/ops_tests.rs +++ b/crates/smlib/src/ops_tests.rs @@ -1,6 +1,5 @@ -//! The operations against analytic volumes and topology counts (built with SMLib only). `SMLIB_OPS_SHOTS=` also +//! The operations against analytic volumes and topology counts. `SMLIB_OPS_SHOTS=` also //! writes every result there as a native `.smb`, to render. -#![cfg(feature = "native")] use super::*; use crate::CheckLevel; @@ -397,20 +396,14 @@ fn draft_tilts_a_side_about_its_line_on_the_neutral_plane() { fn a_setback_corner_left_open_returns_its_hole() { let b = Brep::make_box([0.0; 3], [60.0, 40.0, 20.0]).unwrap(); let edges = [[30.0, 40.0, 20.0], [60.0, 20.0, 20.0], [60.0, 40.0, 10.0]]; - solidcraft_profile::set_enabled(true); - let mark = solidcraft_profile::mark(); let (open, holes) = b.blend_setback_open(Blend::Round, &edges, &[5.0; 3], &[[60.0, 40.0, 20.0]], &[8.0], 0.1).unwrap(); - // The call is a profiler span like every other, timed by the C API. - let report = mark.report(); - let on_cpp = crate::route::backend() == crate::route::Backend::Smlib; - assert!(!on_cpp || report.rows.iter().any(|r| r.category == "smlib" && r.name == "smlib_blend_setback_open"), "{report:?}"); shot(&open, "setback_open"); assert_eq!(open.counts().unwrap(), [9, 30, 22]); assert!(!open.is_manifold_solid().unwrap()); let [hole] = holes.as_slice() else { panic!("one corner: {holes:?}") }; assert_eq!(hole.edges.len(), 12); - let flat = open.export().unwrap(); - let ends = flat.get_i64(crate::flat::EDGE_VERTICES).unwrap(); + let arrays = open.arrays().unwrap(); + let ends = &arrays.ints[&crate::flat::EDGE_VERTICES]; let end = |h: &HoleEdge, last: bool| ends[2 * h.edge + usize::from(h.forward == last)]; for (k, h) in hole.edges.iter().enumerate() { let next = &hole.edges[(k + 1) % 12]; diff --git a/crates/smlib/src/route.rs b/crates/smlib/src/route.rs deleted file mode 100644 index 60cff4d62e..0000000000 --- a/crates/smlib/src/route.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! Which kernel this process runs on: solidcore, the Rust port of SMLib, unless the C++ SMLib -//! fork is asked for. One kernel answers every operation. -//! -//! The C++ kernel is an opt-in: for a test, a rig or a user who wants the old behaviour. A call that -//! fails on solidcore fails, and a path solidcore has no answer for says it is not supported: there is -//! no fallback to the other kernel at run time, which would hide its bugs. Asking for the C++ kernel on -//! a build without it (the default, wasm included) stays on it too: every call answers -//! [`crate::Error::NotBuilt`], which says to build with `--features smlib`. -//! -//! `SOLIDCRAFT_KERNEL=smlib|solidcore` chooses for the whole process (the rig, the test matrix); -//! otherwise the preference ([`set_choice`]) does, and it defaults to solidcore. - -use std::sync::OnceLock; -use std::sync::atomic::{AtomicBool, Ordering}; - -/// A kernel the process runs on. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Backend { - /// The C++ SMLib fork (tangency-labs/smlib) behind its C API. - Smlib, - /// solidcore (tangency-labs/solidcore), the Rust port of SMLib. - Solidcore, -} - -impl Backend { - /// The id `SOLIDCRAFT_KERNEL`, preferences and reports spell it with. - pub fn id(self) -> &'static str { - match self { - Backend::Smlib => "smlib", - Backend::Solidcore => "solidcore", - } - } - - /// Back from an id; anything else is `None`. - pub fn from_id(id: &str) -> Option { - [Backend::Smlib, Backend::Solidcore].into_iter().find(|b| b.id() == id) - } -} - -#[cfg(test)] -thread_local! { - /// Tests: this thread's operations run on the C++ kernel whatever the choice. - static ON_CPP: std::cell::Cell = const { std::cell::Cell::new(false) }; - /// Tests: how many times this thread has touched a C++ body, and called solidcore. - static REACHED: std::cell::Cell<(u64, u64)> = const { std::cell::Cell::new((0, 0)) }; -} - -/// Tests: this thread has touched a C++ body. -#[cfg(test)] -pub(crate) fn reached_cpp() { - REACHED.with(|r| r.set((r.get().0 + 1, r.get().1))); -} - -/// Tests: this thread has called solidcore. -#[cfg(test)] -pub(crate) fn reached_core() { - REACHED.with(|r| r.set((r.get().0, r.get().1 + 1))); -} - -/// Tests: how many times this thread has touched a C++ body and called solidcore since the last -/// time this was asked. -#[cfg(test)] -pub(crate) fn take_reached() -> (u64, u64) { - REACHED.with(|r| r.replace((0, 0))) -} - -/// Tests that exercise the C++ kernel opt in: until the guard drops, this thread's operations run -/// on it. Thread by thread, so that the other tests in the binary stay on the process's kernel. The -/// guard fails a test that never touched a C++ body, so a parity test can't quietly compare -/// solidcore with itself. -#[cfg(test)] -#[cfg_attr(not(feature = "native"), allow(dead_code))] -pub(crate) fn on_cpp() -> impl Drop { - struct Guard; - impl Drop for Guard { - fn drop(&mut self) { - ON_CPP.with(|c| c.set(false)); - let (cpp, core) = REACHED.with(std::cell::Cell::get); - if std::env::var_os("SOLIDCRAFT_TEST_REACHED").is_some() { - eprintln!("REACHED {} cpp={cpp} core={core}", std::thread::current().name().unwrap_or("?")); - } - assert!(cpp > 0 || std::thread::panicking(), "this test opted in to the C++ kernel and never reached it"); - } - } - ON_CPP.with(|c| c.set(true)); - REACHED.with(|r| r.set((0, 0))); - Guard -} - -/// The kernel that answers now: `SOLIDCRAFT_KERNEL`, else the preference, else solidcore. -pub fn backend() -> Backend { - #[cfg(test)] - if ON_CPP.with(std::cell::Cell::get) { - return Backend::Smlib; - } - choice() -} - -/// `SOLIDCRAFT_KERNEL`, read once. An unknown value is no override (and says so in the log). -fn environment() -> Option { - static ENV: OnceLock> = OnceLock::new(); - *ENV.get_or_init(|| { - let v = std::env::var("SOLIDCRAFT_KERNEL").ok()?; - let c = Backend::from_id(&v); - if c.is_none() { - log::warn!(target: "smlib", "SOLIDCRAFT_KERNEL={v}: not smlib or solidcore; ignored"); - } - if c == Some(Backend::Smlib) && !crate::built() { - log::warn!(target: "smlib", "SOLIDCRAFT_KERNEL=smlib: {}; every operation will be refused", crate::NOT_BUILT); - } - c - }) -} - -/// The preference: false for solidcore, true for the C++ kernel. -static PREFERS_CPP: AtomicBool = AtomicBool::new(false); - -/// The choice in force: `SOLIDCRAFT_KERNEL` when it is set, else the preference. -pub fn choice() -> Backend { - environment().unwrap_or_else(|| if PREFERS_CPP.load(Ordering::Relaxed) { Backend::Smlib } else { Backend::Solidcore }) -} - -/// Sets the preference. `SOLIDCRAFT_KERNEL`, when set, still wins. -pub fn set_choice(b: Backend) { - PREFERS_CPP.store(b == Backend::Smlib, Ordering::Relaxed); -} - -/// Whether `SOLIDCRAFT_KERNEL` overrides the preference in this process. -pub fn overridden() -> bool { - environment().is_some() -} - -#[cfg(test)] -mod tests { - use super::*; - - /// The two ids round trip; `routed`, the old default, is not one of them. - #[test] - fn ids_round_trip() { - for b in [Backend::Smlib, Backend::Solidcore] { - assert_eq!(Backend::from_id(b.id()), Some(b)); - } - for other in ["routed", "truck", "SMLIB", ""] { - assert_eq!(Backend::from_id(other), None, "{other}"); - } - } - - /// With nothing chosen the process is on solidcore. (Only solidcore is set here: the other tests - /// in this binary build bodies meanwhile.) - #[test] - fn the_default_is_solidcore() { - if overridden() { - return; - } - set_choice(Backend::Solidcore); - assert_eq!((choice(), backend()), (Backend::Solidcore, Backend::Solidcore)); - } - - /// The guard moves this thread alone onto the C++ kernel, and back when it drops. - #[test] - fn a_test_opts_in_to_the_cpp_kernel_thread_by_thread() { - let before = backend(); - { - let _cpp = on_cpp(); - reached_cpp(); - assert_eq!(backend(), Backend::Smlib); - assert_eq!(choice(), before); - } - assert_eq!(backend(), before); - } -} diff --git a/crates/smlib/src/sheets.rs b/crates/smlib/src/sheets.rs index b2540bdb4f..c7347995d9 100644 --- a/crates/smlib/src/sheets.rs +++ b/crates/smlib/src/sheets.rs @@ -3,27 +3,19 @@ //! tagged inputs ([`Brep::tagged`]) give results whose [`Brep::history`] says where each face and //! edge came from. -use crate::{Brep, Result, core, error, ffi, init, route}; +use crate::{Brep, Result, core}; impl Brep { /// The faces sewn along the vertices and edges that coincide within `tolerance` mm, oriented /// outward where they close into a solid, else only made to agree across their shared edges; /// with whether the result is a closed solid. pub fn stitch(&self, tolerance: f64) -> Result<(Brep, bool)> { - if route::backend() == route::Backend::Solidcore { - return core::merge::stitch(self.core()?, tolerance).map(|(b, closed)| (Brep::made_core(b), closed)); - } - init()?; - ffi::sheets::stitch(self.cpp()?, tolerance).map(|(h, closed)| (Brep::made(h), closed)).map_err(error) + core::merge::stitch(self.core(), tolerance).map(|(b, closed)| (Brep::made_core(b), closed)) } /// This sheet with every face moved onto its surface's offset by `distance` along its normal /// (negative: against it). A closed solid is [`crate::Error::Argument`]. pub fn offset_sheet(&self, distance: f64) -> Result { - if route::backend() == route::Backend::Solidcore { - return core::offset::offset_sheet(self.core()?, distance).map(Brep::made_core); - } - init()?; - ffi::sheets::offset_sheet(self.cpp()?, distance).map(Brep::made).map_err(error) + core::offset::offset_sheet(self.core(), distance).map(Brep::made_core) } } diff --git a/crates/smlib/src/split.rs b/crates/smlib/src/split.rs index a3dbba8766..f23e51351f 100644 --- a/crates/smlib/src/split.rs +++ b/crates/smlib/src/split.rs @@ -3,7 +3,7 @@ //! taken over its whole domain. use crate::ssi::Surface; -use crate::{Brep, Error, Result, core, error, ffi, init, route}; +use crate::{Brep, Error, Result, core}; impl Brep { /// `self` with faces `faces` (indices in SMLib's face order, distinct) cut where each of @@ -11,21 +11,11 @@ impl Brep { /// for each face how many faces it became besides itself (0: no tool split it). `None` when /// no face is split. Tagged operands ([`Brep::tagged`]) give the pieces' history. pub fn split_faces(&self, faces: &[usize], tools: &[&Surface]) -> Result)>> { - if faces.is_empty() || faces.len() > ffi::split::MAX_FACES || tools.is_empty() || tools.len() > ffi::split::MAX_TOOLS { + if faces.is_empty() || faces.len() > core::split::MAX_FACES || tools.is_empty() || tools.len() > core::split::MAX_TOOLS { return Err(Error::Argument); } - if route::backend() == route::Backend::Solidcore { - let specs: Vec<&crate::ssi::Spec> = tools.iter().map(|t| t.spec()).collect(); - return core::split::split_faces(self.core()?, faces, &specs).map(|r| r.map(|(b, made)| (Brep::made_core(b), made))); - } - init()?; - let faces: Vec = faces.iter().map(|f| i64::try_from(*f).map_err(|_| Error::Argument)).collect::>()?; - let handles: Vec<&ffi::ssi::SurfaceHandle> = tools.iter().map(|t| t.handle()).collect::>()?; - match ffi::split::split_faces(self.cpp()?, &faces, &handles) { - Ok((h, made)) => Ok(Some((Brep::made(h), made.into_iter().map(|m| usize::try_from(m).unwrap_or(0)).collect()))), - Err(ffi::ops::ERR_NO_CUT) => Ok(None), - Err(s) => Err(error(s)), - } + let specs: Vec<&crate::ssi::Spec> = tools.iter().map(|t| t.spec()).collect(); + core::split::split_faces(self.core(), faces, &specs).map(|r| r.map(|(b, made)| (Brep::made_core(b), made))) } /// The manifold solid `self` cut in two by `tool` (whose domain must reach past it): the @@ -34,15 +24,7 @@ impl Brep { /// leaves both parts the whole body, with no face from the tool (tag `self` to tell); `None` /// when a side is left empty. pub fn split_body(&self, tool: &Surface) -> Result> { - if route::backend() == route::Backend::Solidcore { - return core::split::split_body(self.core()?, tool.spec()).map(|r| r.map(|(b, f)| (Brep::made_core(b), Brep::made_core(f)))); - } - init()?; - match ffi::split::split_body(self.cpp()?, tool.handle()?) { - Ok((b, f)) => Ok(Some((Brep::made(b), Brep::made(f)))), - Err(ffi::ops::ERR_NO_CUT) => Ok(None), - Err(s) => Err(error(s)), - } + core::split::split_body(self.core(), tool.spec()).map(|r| r.map(|(b, f)| (Brep::made_core(b), Brep::made_core(f)))) } /// The manifold solid `self` cut in two by the sheet `sheet` as it is (its faces joined edge @@ -51,16 +33,7 @@ impl Brep { /// normal and the part in front. A `sheet` that is no sheet, or a `face` it does not have, is /// [`Error::Argument`]. pub fn split_body_by_sheet(&self, sheet: &Brep, face: usize) -> Result> { - if route::backend() == route::Backend::Solidcore { - return core::split::split_body_by_sheet(self.core()?, sheet.core()?, face) - .map(|r| r.map(|(b, f)| (Brep::made_core(b), Brep::made_core(f)))); - } - init()?; - let face = i64::try_from(face).map_err(|_| Error::Argument)?; - match ffi::split::split_body_by_sheet(self.cpp()?, sheet.cpp()?, face) { - Ok((b, f)) => Ok(Some((Brep::made(b), Brep::made(f)))), - Err(ffi::ops::ERR_NO_CUT) => Ok(None), - Err(s) => Err(error(s)), - } + core::split::split_body_by_sheet(self.core(), sheet.core(), face) + .map(|r| r.map(|(b, f)| (Brep::made_core(b), Brep::made_core(f)))) } } diff --git a/crates/smlib/src/ssi.rs b/crates/smlib/src/ssi.rs index 040a244e38..0f06b24072 100644 --- a/crates/smlib/src/ssi.rs +++ b/crates/smlib/src/ssi.rs @@ -10,12 +10,11 @@ use solidcraft_geom::brep as b; use std::sync::OnceLock; -use crate::{Error, Result, error, ffi, init, route}; +use crate::{Error, Result}; /// A surface with its UV domain: the description it was made from, from which solidcore makes its -/// own ([`crate::core::split`]), and the C side's, made when the C++ kernel needs it. +/// own ([`crate::core::split`]). pub struct Surface { - handle: OnceLock>, /// solidcore's, made when it is first needed. core: OnceLock>, spec: Spec, @@ -39,11 +38,7 @@ impl std::fmt::Debug for Surface { } } -fn int(n: usize) -> Result { - i32::try_from(n).map_err(|_| Error::Argument) -} - -/// The largest coordinate, degree and control net the C API takes (`SMLIB_SURFACE_MAX_*`). +/// The largest coordinate, degree and control net a surface may have (`SMLIB_SURFACE_MAX_*`). const MAX_COORD: f64 = 1e8; const MAX_DEGREE: usize = 25; const MAX_POLES: usize = 1_000_000; @@ -87,7 +82,7 @@ fn knots(k: &[f64], n: usize, p: usize) -> bool { impl Spec { /// The checks each of the C API's constructors makes of its arguments, so that a surface is - /// refused alike on either kernel, and without the C++ kernel. + /// refused before solidcore sees it. fn check(&self) -> Result<()> { let whole = |r: &[f64; 2]| range(r, -MAX_COORD, MAX_COORD, 2.0 * MAX_COORD); let turn = |r: &[f64; 2]| range(r, -360.0, 360.0, 360.0); @@ -129,40 +124,9 @@ impl Spec { }; if ok { Ok(()) } else { Err(Error::Argument) } } - - /// The C side's surface. - fn on_cpp(&self) -> Result { - init()?; - match self { - Spec::Plane { origin, xaxis, yaxis, u, v } => ffi::ssi::plane(origin, xaxis, yaxis, u, v), - Spec::Cylinder { origin, xaxis, yaxis, radius, angle_deg, height } => { - ffi::ssi::cylinder(origin, xaxis, yaxis, *radius, angle_deg, height) - } - Spec::Cone { apex, xaxis, yaxis, half_angle_deg, height } => ffi::ssi::cone(apex, xaxis, yaxis, *half_angle_deg, height), - Spec::Sphere { center, xaxis, yaxis, radius, lon_deg, lat_deg } => ffi::ssi::sphere(center, xaxis, yaxis, *radius, lon_deg, lat_deg), - Spec::Torus { center, xaxis, yaxis, major_radius, minor_radius, major_deg, minor_deg } => { - ffi::ssi::torus(center, xaxis, yaxis, *major_radius, *minor_radius, major_deg, minor_deg) - } - Spec::Nurbs { degree, nu, nv, knots_u, knots_v, points, weights } => { - let flat: Vec = points.iter().flatten().copied().collect(); - ffi::ssi::nurbs([int(degree[0])?, int(degree[1])?], int(*nu)?, int(*nv)?, knots_u, knots_v, &flat, weights.as_deref()) - } - } - .map_err(error) - } -} - -/// Whether intersections run on the C++ kernel ([`route::backend`]). -fn ssi_on_cpp() -> bool { - route::backend() == route::Backend::Smlib } impl Surface { - /// The C side's surface, made the first time the C++ kernel needs it. - pub(crate) fn handle(&self) -> Result<&ffi::ssi::SurfaceHandle> { - self.handle.get_or_init(|| self.spec.on_cpp()).as_ref().map_err(Clone::clone) - } - /// solidcore's surface, made the first time it is needed. fn core(&self) -> Result<&crate::core::split::Tool> { self.core.get_or_init(|| crate::core::ssi::surface(&self.spec)).as_ref().map_err(Clone::clone) @@ -172,15 +136,10 @@ impl Surface { &self.spec } - /// A surface from its checked description. On the C++ - /// kernel, its side is made now, so that what it refuses is refused here. + /// A surface from its checked description. fn made(spec: Spec) -> Result { spec.check()?; - let s = Surface { handle: OnceLock::new(), core: OnceLock::new(), spec }; - if ssi_on_cpp() { - s.handle()?; - } - Ok(s) + Ok(Surface { core: OnceLock::new(), spec }) } /// `origin + u*x + v*y` (x, y the axes made unit), for u in `u`, v in `v`. @@ -298,18 +257,12 @@ impl Surface { /// The UV domain the intersection runs within, as `[u0, u1, v0, v1]`. pub fn domain(&self) -> Result<[f64; 4]> { - if !ssi_on_cpp() { - return self.core().map(crate::core::ssi::domain); - } - ffi::ssi::domain(self.handle()?).map_err(error) + self.core().map(crate::core::ssi::domain) } /// The surface's point at (u, v). pub fn evaluate(&self, u: f64, v: f64) -> Result<[f64; 3]> { - if !ssi_on_cpp() { - return crate::core::ssi::evaluate(self.core()?, u, v); - } - ffi::ssi::evaluate(self.handle()?, u, v).map_err(error) + crate::core::ssi::evaluate(self.core()?, u, v) } } @@ -322,15 +275,6 @@ pub enum Method { Advanced, } -impl Method { - fn code(self) -> i32 { - match self { - Method::Global => 0, - Method::Advanced => 1, - } - } -} - /// A curve's type, SMLib's `SmTsectCurveType`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CurveType { @@ -349,24 +293,6 @@ pub enum CurveType { NearTangent, /// Bounds a region where the surfaces coincide. RegionBoundary, - /// A value this crate does not know. - Other(i32), -} - -impl CurveType { - fn from_code(c: i32) -> CurveType { - match c { - -1 => CurveType::None, - 0 => CurveType::Unknown, - 1 => CurveType::Touching, - 2 => CurveType::Crossing, - 3 => CurveType::Tangent, - 4 => CurveType::Coincident, - 5 => CurveType::NearTangent, - 6 => CurveType::RegionBoundary, - c => CurveType::Other(c), - } - } } /// One intersection curve as SMLib returned it: its 3D curve and, where the method returned @@ -402,8 +328,16 @@ pub struct SsiResult { pub points: Vec, } +/// A (rational) B-spline's coordinates, weights and knots, as the capi hands an intersection curve +/// out: `points` 3 (or 2, in UV) reals each. +pub(crate) struct Nurbs { + pub points: Vec, + pub weights: Vec, + pub knots: Vec, +} + /// The 3D B-spline: polynomial where every weight is 1. -pub(crate) fn curve3(c: ffi::ssi::Nurbs) -> b::Curve { +pub(crate) fn curve3(c: Nurbs) -> b::Curve { let points = c.points.as_chunks::<3>().0; if c.weights.iter().all(|w| *w == 1.0) { b::Curve::Spline { knots: c.knots, points: points.to_vec() } @@ -416,7 +350,7 @@ pub(crate) fn curve3(c: ffi::ssi::Nurbs) -> b::Curve { } /// A UV B-spline, as [`curve3`]. -pub(crate) fn curve2(c: ffi::ssi::Nurbs) -> b::Curve2 { +pub(crate) fn curve2(c: Nurbs) -> b::Curve2 { let points = c.points.as_chunks::<2>().0; if c.weights.iter().all(|w| *w == 1.0) { b::Curve2::Spline { knots: c.knots, points: points.to_vec() } @@ -425,76 +359,18 @@ pub(crate) fn curve2(c: ffi::ssi::Nurbs) -> b::Curve2 { } } -/// A UV curve the method may not have returned (`SMLIB_ERR_EMPTY`). -fn uv(r: &ffi::ssi::ResultHandle, i: i32, which: i32) -> Result> { - match ffi::ssi::curve_nurbs(r, i, which, 2) { - Ok(c) => Ok(Some(curve2(c))), - Err(ffi::ERR_EMPTY) => Ok(None), - Err(s) => Err(error(s)), - } -} - /// Intersects `a` and `b` within their domains with `method` at approximation tolerance /// `approx_tol` (<= 0: SMLib's default, 1/1000 of the surfaces' size). An intersector failure is /// SMLib's status; a curve SMLib does not hold as a B-spline is [`Error::Unsupported`]. pub fn intersect(a: &Surface, b: &Surface, method: Method, approx_tol: f64) -> Result { - if !ssi_on_cpp() { - return crate::core::ssi::intersect(a.core()?, b.core()?, method, approx_tol); - } - let r = ffi::ssi::ssi(a.handle()?, b.handle()?, method.code(), approx_tol).map_err(error)?; - let n = ffi::ssi::curve_count(&r).map_err(error)?; - let mut curves = Vec::new(); - for i in 0..n { - let info = ffi::ssi::curve_info(&r, i).map_err(error)?; - curves.push(SsiCurve { - curve_type: CurveType::from_code(info.ty), - deviation: info.deviation, - interval: info.interval, - curve: curve3(ffi::ssi::curve_nurbs(&r, i, ffi::ssi::CURVE_3D, 3).map_err(error)?), - uv_a: uv(&r, i, ffi::ssi::CURVE_UV_A)?, - uv_b: uv(&r, i, ffi::ssi::CURVE_UV_B)?, - }); - } - let n = ffi::ssi::point_count(&r).map_err(error)?; - let mut points = Vec::new(); - for i in 0..n { - let (xyz, uv_a, uv_b) = ffi::ssi::point(&r, i).map_err(error)?; - points.push(SsiPoint { xyz, uv_a, uv_b }); - } - Ok(SsiResult { curves, points }) + crate::core::ssi::intersect(a.core()?, b.core()?, method, approx_tol) } #[cfg(test)] mod tests { use super::*; - /// A plane through a sphere's centre: one great circle. - #[test] - fn plane_through_sphere_is_a_circle() { - #[cfg(feature = "native")] - let _cpp = route::on_cpp(); - let z = [0.0, 0.0, 1.0]; - let (x, y) = ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]); - let plane = Surface::plane([0.0; 3], x, y, [-2.0, 2.0], [-2.0, 2.0]); - let sphere = Surface::sphere([0.0; 3], x, z, 1.0, [0.0, 360.0], [-90.0, 90.0]); - let (plane, sphere) = match (plane, sphere) { - (Err(Error::NotBuilt), _) | (_, Err(Error::NotBuilt)) => return, - (Ok(p), Ok(s)) => (p, s), - (p, s) => panic!("{p:?} {s:?}"), - }; - let r = intersect(&plane, &sphere, Method::Global, 1e-6).expect("intersect"); - assert!(!r.curves.is_empty()); - for c in &r.curves { - let eval = b::eval::CurveEval::new(&c.curve).expect("a curve"); - let [t0, t1] = c.interval; - for k in 0..=100 { - let p = eval.point(t0 + (t1 - t0) * f64::from(k) / 100.0).expect("evaluates"); - assert!((p.x.hypot(p.y) - 1.0).abs() < 1e-5 && p.z.abs() < 1e-5, "{p:?}"); - } - } - } - - /// Refused here, before any kernel: alike on either, and with no C++ kernel. + /// Refused here, before solidcore sees the surface. #[test] fn bad_input_is_an_argument_error() { let (x, y) = ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]); @@ -516,13 +392,10 @@ mod tests { } } - /// On solidcore a surface needs no C++ kernel, and answers by itself. The basic intersector is - /// the one method solidcore has no public entry for, and it says so; nothing falls back. + /// A plane's domain and points are its own parameters; a plane through a sphere's centre meets + /// it in one great circle, by either method. #[test] - fn surfaces_on_solidcore_answer_without_the_cpp_kernel() { - if ssi_on_cpp() { - return; - } + fn a_plane_through_a_spheres_centre_is_a_great_circle() { let (x, y, z) = ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]); let plane = Surface::plane([0.0; 3], x, y, [-2.0, 2.0], [-3.0, 3.0]).unwrap(); assert_eq!(plane.domain().unwrap(), [-2.0, 2.0, -3.0, 3.0]); diff --git a/crates/smlib/src/sweeps.rs b/crates/smlib/src/sweeps.rs index c2c040034b..819f5e2e31 100644 --- a/crates/smlib/src/sweeps.rs +++ b/crates/smlib/src/sweeps.rs @@ -6,7 +6,7 @@ //! With `cap`, the profile's loops close the ends and the result must be a closed manifold solid //! ([`Error::NotSolid`] otherwise); without, it is the swept sheet. -use crate::{Brep, Error, Result, core, error, ffi, init, route}; +use crate::{Brep, Result, core}; /// A 3D curve of a profile, path or section. #[derive(Clone, Debug, PartialEq)] @@ -89,97 +89,23 @@ pub struct Affine { pub t: [f64; 3], } -/// Most stations of one [`Brep::sweep_stations`] (the C API's limit). -pub const MAX_STATIONS: usize = ffi::sweeps::SWEEP_MAX_STATIONS; -/// Most profile curves times stations of one [`Brep::sweep_stations`] (the C API's limit). -pub const MAX_STATION_CURVES: usize = ffi::sweeps::SWEEP_MAX_STATION_CURVES; +/// Most stations of one [`Brep::sweep_stations`]. +pub const MAX_STATIONS: usize = 4096; +/// Most profile curves times stations of one [`Brep::sweep_stations`]. +pub const MAX_STATION_CURVES: usize = 400_000; -/// Most curves one list may hold (the C API's limit). -pub const MAX_CURVES: usize = ffi::sweeps::CURVE_MAX_CURVES; - -/// The curves as the C API's curve list. -#[derive(Default)] -pub(crate) struct List { - kinds: Vec, - reals: Vec, -} - -impl List { - pub(crate) fn of<'a>(curves: impl IntoIterator) -> Result { - let mut l = List::default(); - for c in curves { - l.push(c)?; - } - Ok(l) - } - - fn push(&mut self, c: &Curve) -> Result<()> { - if self.kinds.len() >= 3 * MAX_CURVES { - return Err(Error::Argument); - } - match c { - Curve::Line { start, end } => { - self.kinds.push(ffi::sweeps::CURVE_LINE); - self.reals.extend_from_slice(start); - self.reals.extend_from_slice(end); - } - Curve::Arc { center, axis, reference, radius, start, end } => { - self.kinds.push(ffi::sweeps::CURVE_ARC); - self.reals.extend_from_slice(center); - self.reals.extend_from_slice(axis); - self.reals.extend_from_slice(reference); - self.reals.extend_from_slice(&[*radius, *start, *end]); - } - Curve::Nurbs { degree, points, knots } => { - if *degree == 0 - || *degree > ffi::sweeps::CURVE_MAX_DEGREE - || points.len() > ffi::sweeps::CURVE_MAX_POINTS - || knots.len() != points.len() + degree + 1 - { - return Err(Error::Argument); - } - let d = i64::try_from(*degree).map_err(|_| Error::Argument)?; - let n = i64::try_from(points.len()).map_err(|_| Error::Argument)?; - self.kinds.extend_from_slice(&[ffi::sweeps::CURVE_NURBS, d, n]); - for p in points { - self.reals.extend_from_slice(p); - } - self.reals.extend_from_slice(knots); - } - } - Ok(()) - } - - pub(crate) fn raw(&self) -> ffi::sweeps::List<'_> { - ffi::sweeps::List::new(&self.kinds, &self.reals) - } -} - -#[cfg(test)] -thread_local! { - /// Tests: this thread's sweeps run on the C++ kernel whatever the choice. - pub(crate) static ON_CPP: std::cell::Cell = const { std::cell::Cell::new(false) }; -} - -/// Whether the sweeps run on solidcore ([`route::backend`]). -fn on_core() -> bool { - #[cfg(test)] - if ON_CPP.with(std::cell::Cell::get) { - return false; - } - route::backend() == route::Backend::Solidcore -} +/// Most curves one list may hold. +pub const MAX_CURVES: usize = 16_384; +/// Highest degree of a [`Curve::Nurbs`]. +pub(crate) const CURVE_MAX_DEGREE: usize = 25; +/// Most control points of a [`Curve::Nurbs`]. +pub(crate) const CURVE_MAX_POINTS: usize = 100_000; impl Brep { /// A sheet of planar faces bounded by `loops`: closed loops of curves in one plane, in any /// order, nested as outer loops, their holes and islands in the holes. pub fn planar_faces(loops: &[Curve]) -> Result { - if on_core() { - return core::sweep::planar_faces(loops).map(Brep::made_core); - } - init()?; - let l = List::of(loops)?; - ffi::sweeps::planar_faces(&l.raw()).map(Brep::made).map_err(error) + core::sweep::planar_faces(loops).map(Brep::made_core) } /// The planar `profile` extruded `distance` along the unit `direction` (square to its plane). @@ -190,56 +116,20 @@ impl Brep { /// [`DraftCorner::Curvature`], a curve of a profile with a sharp corner is neither a line nor /// an arc). pub fn extrude(profile: &[Curve], direction: [f64; 3], distance: f64, draft: f64, corner: DraftCorner, cap: bool) -> Result { - if on_core() { - return core::sweep::extrude(profile, direction, distance, draft, corner, cap).map(Brep::made_core); - } - init()?; - let l = List::of(profile)?; - let rule = match corner { - DraftCorner::Curvature => ffi::sweeps::DRAFT_CORNER_CURVATURE, - DraftCorner::Tangent => ffi::sweeps::DRAFT_CORNER_TANGENT, - DraftCorner::Round => ffi::sweeps::DRAFT_CORNER_ROUND, - }; - let mut at = [f64::NAN; 3]; - ffi::sweeps::extrude(&l.raw(), &direction, distance, draft, rule, cap, &mut at).map(Brep::made).map_err(|s| { - let why = match s { - ffi::sweeps::ERR_CORNER_APART => Some(CornerRefusal::Apart), - ffi::sweeps::ERR_WALL_CLOSES => Some(CornerRefusal::WallCloses), - ffi::trim::ERR_NOT_EXACT => Some(CornerRefusal::NotExact), - _ => None, - }; - match why { - Some(why) if at.iter().all(|x| x.is_finite()) => Error::Corner { why, at }, - _ => error(s), - } - }) + core::sweep::extrude(profile, direction, distance, draft, corner, cap).map(Brep::made_core) } /// The planar `profile` turned `angle` radians (0 < angle <= 2 pi) right-handed about the /// axis through `origin` along `direction`, in the profile's plane (touching it at most). pub fn revolve(profile: &[Curve], origin: [f64; 3], direction: [f64; 3], angle: f64, cap: bool) -> Result { - if on_core() { - return core::sweep::revolve(profile, origin, direction, angle, cap).map(Brep::made_core); - } - init()?; - let l = List::of(profile)?; - ffi::sweeps::revolve(&l.raw(), &origin, &direction, angle, cap).map(Brep::made).map_err(error) + core::sweep::revolve(profile, origin, direction, angle, cap).map(Brep::made_core) } /// `profile` swept along `path` (end-to-end curves), carried with the path's start: the /// profile keeps its place relative to it. The walls are B-spline approximations, one per /// stretch of profile between tangent breaks. pub fn sweep(profile: &[Curve], path: &[Curve], frame: SweepFrame, cap: bool) -> Result { - if on_core() { - return core::sweep::sweep_placed(profile, path, frame, cap).map(|(b, _)| Brep::made_core(b)); - } - init()?; - let (l, p) = (List::of(profile)?, List::of(path)?); - let frame = match frame { - SweepFrame::Follow => ffi::sweeps::SWEEP_FOLLOW, - SweepFrame::Translate => ffi::sweeps::SWEEP_TRANSLATE, - }; - ffi::sweeps::sweep(&l.raw(), &p.raw(), frame, cap).map(Brep::made).map_err(error) + core::sweep::sweep_placed(profile, path, frame, cap).map(|(b, _)| Brep::made_core(b)) } /// [`Brep::sweep`], with where the sweep placed the profile: the rigid motions carrying the @@ -248,22 +138,7 @@ impl Brep { /// out from point to point along the path); a sweep that carries on from where this one left /// the profile starts from the last. pub fn sweep_placed(profile: &[Curve], path: &[Curve], frame: SweepFrame, cap: bool) -> Result<(Brep, [Affine; 3])> { - if on_core() { - return core::sweep::sweep_placed(profile, path, frame, cap).map(|(b, m)| (Brep::made_core(b), m)); - } - init()?; - let (l, p) = (List::of(profile)?, List::of(path)?); - let frame = match frame { - SweepFrame::Follow => ffi::sweeps::SWEEP_FOLLOW, - SweepFrame::Translate => ffi::sweeps::SWEEP_TRANSLATE, - }; - let mut placed = [0.0; 36]; - let brep = ffi::sweeps::sweep_placed(&l.raw(), &p.raw(), frame, cap, &mut placed).map(Brep::made).map_err(error)?; - let motion = |i: usize| { - let at = |k: usize| placed.get(12 * i + k).copied().unwrap_or(0.0); - Affine { m: [[at(0), at(1), at(2)], [at(3), at(4), at(5)], [at(6), at(7), at(8)]], t: [at(9), at(10), at(11)] } - }; - Ok((brep, [motion(0), motion(1), motion(2)])) + core::sweep::sweep_placed(profile, path, frame, cap).map(|(b, m)| (Brep::made_core(b), m)) } /// The `curves` projected along `direction` (pointing from them toward the face) onto the @@ -279,13 +154,7 @@ impl Brep { direction: [f64; 3], tolerance: f64, ) -> Result> { - if on_core() { - return core::sweep::project_curves(self.core()?, face_point, pick_tolerance, curves, direction, tolerance); - } - init()?; - let l = List::of(curves)?; - let out = ffi::sweeps::project_curves(self.cpp()?, &face_point, pick_tolerance, &l.raw(), &direction, tolerance).map_err(picked)?; - nurbs_list(&out) + core::sweep::project_curves(self.core(), face_point, pick_tolerance, curves, direction, tolerance) } /// The section of the face nearest `face_point` by the plane through `plane_point` square to @@ -298,72 +167,33 @@ impl Brep { plane_normal: [f64; 3], tolerance: f64, ) -> Result> { - if on_core() { - return core::sweep::face_section(self.core()?, face_point, pick_tolerance, plane_point, plane_normal, tolerance); - } - init()?; - let out = ffi::sweeps::face_section(self.cpp()?, &face_point, pick_tolerance, &plane_point, &plane_normal, tolerance).map_err(picked)?; - nurbs_list(&out) + core::sweep::face_section(self.core(), face_point, pick_tolerance, plane_point, plane_normal, tolerance) } /// A loft (skin) through `sections` (at least two closed loops), `degree` (1 ruled, 2 or 3) /// across them. pub fn loft(sections: &[Vec], degree: u32, cap: bool) -> Result { - if on_core() { - return core::sweep::loft(sections, degree, cap).map(Brep::made_core); - } - init()?; - if sections.len() < 2 || sections.len() > MAX_CURVES || !(1..=3).contains(°ree) { - return Err(Error::Argument); - } - let l = List::of(sections.iter().flatten())?; - let sizes: Vec = sections.iter().map(|s| i64::try_from(s.len()).map_err(|_| Error::Argument)).collect::>()?; - let degree = i32::try_from(degree).map_err(|_| Error::Argument)?; - ffi::sweeps::loft(&l.raw(), &sizes, degree, cap).map(Brep::made).map_err(error) + core::sweep::loft(sections, degree, cap).map(Brep::made_core) } /// The planar `profile` placed by each of `stations` in turn (2…[`MAX_STATIONS`], each /// section exactly the profile's image), its walls skinned through the sections, cubic /// across them: one wall per profile curve. `ends` says how the ends close. pub fn sweep_stations(profile: &[Curve], stations: &[Affine], ends: Ends) -> Result { - if on_core() { - return core::sweep::sweep_stations(profile, stations, ends).map(Brep::made_core); - } - init()?; - if !(2..=MAX_STATIONS).contains(&stations.len()) || profile.len().saturating_mul(stations.len()) > MAX_STATION_CURVES { - return Err(Error::Argument); - } - let l = List::of(profile)?; - let maps: Vec = stations.iter().flat_map(|s| s.m.iter().flatten().chain(&s.t).copied()).collect(); - let ends = match ends { - Ends::Open => ffi::sweeps::ENDS_OPEN, - Ends::Capped => ffi::sweeps::ENDS_CAPPED, - Ends::Joined => ffi::sweeps::ENDS_JOINED, - }; - ffi::sweeps::sweep_stations(&l.raw(), &maps, ends).map(Brep::made).map_err(error) + core::sweep::sweep_stations(profile, stations, ends).map(Brep::made_core) } /// A loft from the planar `section` (one closed loop of lines, in order) to the point `apex` /// off its plane: a pyramid, its base the section and a triangle on each line. pub fn loft_to_point(section: &[Curve], apex: [f64; 3]) -> Result { - if on_core() { - return core::sweep::loft_to_point(section, apex).map(Brep::made_core); - } - init()?; - let l = List::of(section)?; - ffi::sweeps::loft_to_point(&l.raw(), &apex).map(Brep::made).map_err(error) + core::sweep::loft_to_point(section, apex).map(Brep::made_core) } /// `profile` carried by a screw motion about the axis through `origin` along `direction`: /// `turns` right-handed turns about it, rising `pitch` (signed) per turn, capped at both ends, /// its walls within about `tolerance` of the true screw surfaces. pub fn screw_sweep(profile: &[Curve], origin: [f64; 3], direction: [f64; 3], pitch: f64, turns: f64, tolerance: f64) -> Result { - if on_core() { - return core::sweep::screw_sweep(profile, origin, direction, pitch, turns, None, tolerance).map(Brep::made_core); - } - init()?; - let l = List::of(profile)?; - ffi::sweeps::screw_sweep(&l.raw(), &origin, &direction, pitch, turns, tolerance).map(Brep::made).map_err(error) + core::sweep::screw_sweep(profile, origin, direction, pitch, turns, None, tolerance).map(Brep::made_core) } /// [`screw_sweep`](Self::screw_sweep) with the profile moved off the axis as it turns: at turn @@ -381,49 +211,10 @@ impl Brep { law: &[(f64, f64)], tolerance: f64, ) -> Result { - if on_core() { - return core::sweep::screw_sweep(profile, origin, direction, pitch, turns, Some(law), tolerance).map(Brep::made_core); - } - init()?; - let l = List::of(profile)?; - let flat: Vec = law.iter().flat_map(|(t, o)| [*t, *o]).collect(); - ffi::sweeps::screw_sweep_radial(&l.raw(), &origin, &direction, pitch, turns, &flat, tolerance).map(Brep::made).map_err(error) + core::sweep::screw_sweep(profile, origin, direction, pitch, turns, Some(law), tolerance).map(Brep::made_core) } } -/// The error of a call that picks faces by points. -fn picked(s: i32) -> Error { - match s { - ffi::ops::ERR_PICK_NONE => Error::NoMatch { point: 0 }, - ffi::ops::ERR_PICK_AMBIGUOUS => Error::Ambiguous { point: 0 }, - s => error(s), - } -} - -/// The NURBS curves of a curve list the C API made, each with its interval. -fn nurbs_list(out: &ffi::sweeps::OutCurves) -> Result> { - let (mut k, mut r) = (0usize, 0usize); - let mut curves = Vec::new(); - for ivl in out.intervals.as_chunks::<2>().0 { - let (Some(&ffi::sweeps::CURVE_NURBS), Some(°ree), Some(&count)) = (out.kinds.get(k), out.kinds.get(k + 1), out.kinds.get(k + 2)) else { - return Err(Error::Argument); - }; - k += 3; - let degree = usize::try_from(degree).map_err(|_| Error::Argument)?; - let count = usize::try_from(count).map_err(|_| Error::Argument)?; - let pts = out.reals.get(r..r + 4 * count).ok_or(Error::Argument)?; - r += 4 * count; - let knots = out.reals.get(r..r + count + degree + 1).ok_or(Error::Argument)?.to_vec(); - r += count + degree + 1; - let points = pts.as_chunks::<4>().0.to_vec(); - curves.push((Curve::Nurbs { degree, points, knots }, [ivl[0], ivl[1]])); - } - if k != out.kinds.len() || r != out.reals.len() { - return Err(Error::Argument); - } - Ok(curves) -} - #[cfg(test)] #[path = "sweeps_tests.rs"] mod tests; diff --git a/crates/smlib/src/sweeps_tests.rs b/crates/smlib/src/sweeps_tests.rs index e255b70fb0..20b5a7e867 100644 --- a/crates/smlib/src/sweeps_tests.rs +++ b/crates/smlib/src/sweeps_tests.rs @@ -1,10 +1,9 @@ -//! The sweeps against analytic volumes, each result a valid closed solid (built with SMLib only). -#![cfg(feature = "native")] +//! The sweeps against analytic volumes, each result a valid closed solid. use std::f64::consts::{PI, TAU}; use super::*; -use crate::CheckLevel; +use crate::{CheckLevel, Error}; fn solid_volume(b: &Brep, what: &str) -> f64 { let report = b.check(CheckLevel::All).unwrap(); diff --git a/crates/smlib/src/tests.rs b/crates/smlib/src/tests.rs index 41ac8e13e7..29bb96debe 100644 --- a/crates/smlib/src/tests.rs +++ b/crates/smlib/src/tests.rs @@ -1,6 +1,5 @@ use super::*; -#[cfg(feature = "native")] #[test] fn box_end_to_end() { let b = Brep::make_box([1.0, 2.0, 3.0], [10.0, 20.0, 30.0]).unwrap(); @@ -9,20 +8,15 @@ fn box_end_to_end() { let c = b.try_clone().unwrap(); drop(b); assert!((c.volume(1e-7).unwrap() - 6000.0).abs() < 1e-6); - let mut buf = [0u8; 512]; - assert_eq!(ffi::current_call(&mut buf), 0, "no C call in flight between calls"); + assert_eq!(current_call(), None, "no call in flight between calls"); } -#[cfg(feature = "native")] #[test] fn bad_arguments_are_errors() { assert_eq!(Brep::make_box([0.0; 3], [1.0, -1.0, 1.0]).unwrap_err(), Error::Argument); assert_eq!(Brep::make_box([f64::NAN, 0.0, 0.0], [1.0; 3]).unwrap_err(), Error::Argument); - // each kernel refuses a bad box the same way, whichever one is in force - init().unwrap(); for size in [[1.0, -1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, f64::NAN]] { - assert_eq!(core::make_box([0.0; 3], size).err(), Some(Error::Argument), "solidcore: {size:?}"); - assert_eq!(ffi::make_box(&[0.0; 3], &size).map_err(error).err(), Some(Error::Argument), "C++: {size:?}"); + assert_eq!(core::make_box([0.0; 3], size).err(), Some(Error::Argument), "{size:?}"); } let b = Brep::make_box([0.0; 3], [1.0; 3]).unwrap(); assert_eq!(b.volume(f64::INFINITY).unwrap_err(), Error::Argument); @@ -38,13 +32,13 @@ fn solidcore_reads_back_the_files_it_writes() { let cylinder = core::make_cylinder([1.0, 2.0, 3.0], 2.0, 5.0).map(Brep::made_core).unwrap(); for ascii in [false, true] { let path = dir.join(format!("cylinder-{ascii}.smb")); - core::file::write(cylinder.core().unwrap(), &path, ascii).unwrap(); + core::file::write(cylinder.core(), &path, ascii).unwrap(); let back = core::file::read(&path, ascii).map(Brep::made_core).unwrap(); - let v = core::volume(back.core().unwrap(), 1e-7).unwrap(); + let v = core::volume(back.core(), 1e-7).unwrap(); assert!((v - PI * 4.0 * 5.0).abs() < 1e-5, "ascii {ascii}: {v}"); - assert_eq!(core::counts(back.core().unwrap()).unwrap(), core::counts(cylinder.core().unwrap()).unwrap(), "ascii {ascii}"); + assert_eq!(core::counts(back.core()).unwrap(), core::counts(cylinder.core()).unwrap(), "ascii {ascii}"); } - let body = cylinder.core().unwrap(); + let body = cylinder.core(); assert!(core::file::write(body, &dir.join("missing/x.smb"), false).is_err()); assert!(core::file::read(&dir.join("missing.smb"), false).is_err()); let junk = dir.join("junk.smb"); @@ -67,60 +61,6 @@ fn solidcore_refuses_an_ascii_file_of_junk() { assert!(r.is_err()); } -/// A file either kernel wrote reads on the other, with the same measure. -#[cfg(feature = "native")] -#[test] -fn each_kernel_reads_the_files_of_the_other() { - use std::f64::consts::PI; - let dir = std::env::temp_dir().join(format!("solidcraft-smlib-cross-file-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let want = PI * 4.0 * 5.0; - for ascii in [false, true] { - let from_core = dir.join(format!("core-{ascii}.smb")); - let body = core::make_cylinder([0.0; 3], 2.0, 5.0).unwrap(); - core::file::write(&body, &from_core, ascii).unwrap(); - crate::init().unwrap(); - let on_cpp = ffi::read(&c_path(&from_core).unwrap(), ascii).map(Brep::made).unwrap(); - let v = ffi::volume(on_cpp.cpp().unwrap(), 1e-7).unwrap(); - assert!((v - want).abs() < 1e-5, "C++ on solidcore's file, ascii {ascii}: {v}"); - let from_cpp = dir.join(format!("cpp-{ascii}.smb")); - let cpp = ffi::make_cylinder(&[0.0; 3], 2.0, 5.0).map(Brep::made).unwrap(); - crate::check(ffi::write(cpp.cpp().unwrap(), &c_path(&from_cpp).unwrap(), ascii)).unwrap(); - let on_core = core::file::read(&from_cpp, ascii).unwrap(); - let v = core::volume(&on_core, 1e-7).unwrap(); - assert!((v - want).abs() < 1e-5, "solidcore on C++'s file, ascii {ascii}: {v}"); - } - let _ = std::fs::remove_dir_all(&dir); -} - -#[cfg(feature = "native")] -#[test] -fn native_files_round_trip_and_failures_dump_their_inputs() { - let _dumps = DUMP_DIR_TESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner); - let _cpp = route::on_cpp(); - let dir = std::env::temp_dir().join(format!("solidcraft-smlib-test-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let b = Brep::make_box([0.0; 3], [2.0, 3.0, 4.0]).unwrap(); - let path = dir.join("box.smb"); - b.write(&path, false).unwrap(); - let back = Brep::read(&path, false).unwrap(); - assert!((back.volume(1e-7).unwrap() - 24.0).abs() < 1e-9); - - let dumps = dir.join("dumps"); - std::fs::create_dir_all(&dumps).unwrap(); - set_dump_dir(Some(&dumps)).unwrap(); - assert!(b.write(&dir.join("missing/x.smb"), false).is_err()); - set_dump_dir(None).unwrap(); - let names: Vec = - std::fs::read_dir(&dumps).unwrap().filter_map(|e| e.ok()).map(|e| e.file_name().to_string_lossy().into_owned()).collect(); - assert!(names.iter().any(|n| n.ends_with("-smlib_brep_write.txt")), "{names:?}"); - let smb = names.iter().find(|n| n.ends_with("-smlib_brep_write-brep.smb")).unwrap(); - let replayed = Brep::read(&dumps.join(smb), false).unwrap(); - assert!((replayed.volume(1e-7).unwrap() - 24.0).abs() < 1e-9); - let _ = std::fs::remove_dir_all(&dir); -} - -#[cfg(feature = "native")] #[test] fn calls_from_many_threads_are_serialised() { let handles: Vec<_> = (0..8) @@ -135,30 +75,11 @@ fn calls_from_many_threads_are_serialised() { assert!(handles.into_iter().all(|h| h.join().unwrap())); } -#[cfg(not(feature = "native"))] -#[test] -fn without_native_every_call_says_so() { - assert_eq!(init(), Err(Error::NotBuilt)); - // the C API's own box, whichever kernel is in force - assert_eq!(ffi::make_box(&[0.0; 3], &[1.0; 3]).map_err(error).err(), Some(Error::NotBuilt)); - // solidcore builds a box without SMLib; asking the C++ kernel about it says there is none - let b = core::make_box([0.0; 3], [1.0; 3]).map(Brep::made_core).unwrap(); - assert_eq!(b.cpp().err(), Some(Error::NotBuilt)); - // and says how to get it - assert!(!built()); - assert!(Error::NotBuilt.to_string().contains("--features smlib"), "{}", Error::NotBuilt); -} - /// The dump folder is process-wide: the tests that set it take turns. static DUMP_DIR_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(()); -/// Which public entry points work with no C++ kernel: all of them but [`CPP_ONLY`], which are the -/// C++ kernel's own arrays and start-up, reached only across kernels and by tests. A new entry point -/// fails [`every_entry_point_is_in_the_guard`] until it is called in the test below or listed. -const CPP_ONLY: [&str; 8] = ["init", "new", "get_i64", "get_f64", "set_i64", "set_f64", "import", "export"]; - /// Entry points that ask no kernel, so have nothing to say here. -const NO_KERNEL: [&str; 11] = [ +const NO_KERNEL: [&str; 10] = [ "inertia_tensor", "is_valid", "failed_tests", @@ -169,8 +90,6 @@ const NO_KERNEL: [&str; 11] = [ "to_native_report", "to_native_ordered", "blends", - // The last refusal's fit, read after it: on solidcore, none yet. - "blend_fit", ]; const ANSWERING: [&str; 62] = [ @@ -258,36 +177,28 @@ fn every_entry_point_is_in_the_guard() { for line in file.lines() { let Some(rest) = line.trim_start().strip_prefix("pub fn ") else { continue }; let name = rest.split(['(', '<']).next().unwrap_or(rest); - assert!(ANSWERING.contains(&name) || CPP_ONLY.contains(&name) || NO_KERNEL.contains(&name), "`{name}` is in neither list"); + assert!(ANSWERING.contains(&name) || NO_KERNEL.contains(&name), "`{name}` is in neither list"); } } } -/// With no C++ kernel, each entry point answers with something other than "not built": its result -/// or its own refusal, on a body of the right kind or the wrong one. Built with the C++ kernel, the -/// same calls with nothing chosen never touch a C++ body: nothing on the default path reaches it. +/// Each entry point answers, with its result or its own refusal, on a body of the right kind or the +/// wrong one: none panics. #[test] -fn without_the_cpp_kernel_every_entry_point_answers() { +fn every_entry_point_answers() { use crate::sweeps::{Affine, Curve, DraftCorner, Ends, SweepFrame}; use crate::trim::{Continuation, Holes}; use crate::{Blend, BooleanOp, ChamferSecond, CheckLevel}; - if route::backend() != route::Backend::Solidcore { - return; - } - fn says(name: &str, r: Result) -> Option { - match r { - Err(Error::NotBuilt) => panic!("{name} needs the C++ kernel"), - r => r.ok(), - } + fn says(_name: &str, r: Result) -> Option { + r.ok() } let dir = std::env::temp_dir().join(format!("solidcraft-smlib-guard-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); { let _dumps = DUMP_DIR_TESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner); - says("set_dump_dir", set_dump_dir(None)); + set_dump_dir(None); } let _ = current_call(); - route::take_reached(); let a = says("make_box", Brep::make_box([0.0; 3], [10.0; 3])).unwrap(); let t = says("make_box", Brep::make_box([5.0; 3], [10.0; 3])).unwrap(); let p = |x: f64, y: f64, z: f64| [x, y, z]; @@ -363,8 +274,6 @@ fn without_the_cpp_kernel_every_entry_point_answers() { says("loft_to_point", Brep::loft_to_point(&lines, p(5.0, 5.0, 12.0))); says("screw_sweep", Brep::screw_sweep(&lines, p(30.0, 0.0, 0.0), p(0.0, 0.0, 1.0), 12.0, 1.0, 0.01)); says("screw_sweep_radial", Brep::screw_sweep_radial(&lines, p(30.0, 0.0, 0.0), p(0.0, 0.0, 1.0), 12.0, 1.0, &[(0.0, 0.0), (1.0, 0.0)], 0.01)); - let (cpp, core) = route::take_reached(); - assert!(cpp == 0 && core > 0, "the default path touched a C++ body {cpp} times ({core} solidcore calls)"); let _ = std::fs::remove_dir_all(&dir); } @@ -400,31 +309,6 @@ fn a_failing_solidcore_call_leaves_a_dump_and_names_itself() { let _ = std::fs::remove_dir_all(&dir); } -#[test] -fn call_lines_give_the_function_and_the_c_api_time() { - let (name, took) = call_time("smlib_boolean(op=1, a=#3 (12 faces), b=#4) -> 0 (12.345 ms)").unwrap(); - assert_eq!((name, took), ("smlib_boolean", std::time::Duration::from_micros(12_345))); - assert_eq!(call_time("smlib_make_box(origin=(0,0,0)) -> -1002 (0.001 ms)").map(|c| c.0), Some("smlib_make_box")); - for bad in ["SMLib warning at x.cpp:12: tolerance (in smlib_boolean(...))", "smlib_x() -> 0 (nan ms)", "smlib_x() -> 0 (-1 ms)", "", "smlib_x"] { - assert_eq!(call_time(bad), None, "{bad}"); - } -} - -#[cfg(feature = "native")] -#[test] -fn every_call_is_a_profiler_span_timed_by_the_c_api() { - let _cpp = route::on_cpp(); - solidcraft_profile::set_enabled(true); - let mark = solidcraft_profile::mark(); - // the C API's own box, whichever kernel is in force - crate::init().unwrap(); - let b = ffi::make_box(&[0.0; 3], &[1.0; 3]).map(Brep::made).unwrap(); - let _ = b.volume(1e-7).unwrap(); - let r = mark.report(); - let names: Vec<&str> = r.rows.iter().filter(|r| r.category == "smlib").map(|r| r.name).collect(); - assert!(names.contains(&"smlib_make_box") && names.contains(&"smlib_brep_volume"), "{r:?}"); -} - /// SMLib may end an edge at a parameter of its own (a knot its vertex lies at, where its drop onto /// the patch ends): within the edge's tolerance as SMLib held it, on its curve, the pcurve is /// mapped onto our interval exactly; farther is SMLib trimming the edge, refused with how far. An @@ -456,7 +340,6 @@ fn a_pcurve_smlib_ends_within_the_edges_tolerance_is_mapped_onto_our_interval() /// when it makes its trim, unless the edge's tolerance covers its gap. The gap is measured /// (SMLib's own, over the patch) and covered, so the face's pcurves come back on the edge's /// own interval, ending at the patch's corner. -#[cfg(feature = "native")] #[test] fn an_edge_a_few_um_past_its_patch_keeps_its_interval() { use solidcraft_geom::brep::pcurve::{TrimEdge, TrimFace, TrimUse, Trims}; diff --git a/crates/smlib/src/trim.rs b/crates/smlib/src/trim.rs index a528e22324..1195f6b07d 100644 --- a/crates/smlib/src/trim.rs +++ b/crates/smlib/src/trim.rs @@ -1,7 +1,7 @@ //! Untrim and extend of a sheet's faces. Each returns a new body and leaves its input unchanged; //! faces and holes are chosen by points, as [`crate::ops`] chooses them. -use crate::{Brep, Result, core, error, ffi, init, route}; +use crate::{Brep, Result, core}; /// How [`Brep::extend_sheet`] continues a surface past its side. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -33,14 +33,7 @@ impl Brep { Holes::All => None, Holes::At(at) => Some(at), }; - if route::backend() == route::Backend::Solidcore { - return core::trim::untrim_face(self.core()?, point, tolerance, holes, outer, in_place).map(Brep::made_core); - } - init()?; - ffi::trim::untrim_face(self.cpp()?, &point, tolerance, holes, outer, in_place).map(Brep::made).map_err(|s| match s { - ffi::ops::ERR_PICK_NONE => crate::Error::NoMatch { point: 0 }, - s => error(s), - }) + core::trim::untrim_face(self.core(), point, tolerance, holes, outer, in_place).map(Brep::made_core) } /// The one-face sheet `self`, whose face is its surface's whole parameter box, continued @@ -48,29 +41,12 @@ impl Brep { /// `how` says: SMLib's extended surface over the grown box, the face and its sides carrying /// the tags of those they continue. A sheet of another shape is [`crate::Error::Unsupported`]. pub fn extend_sheet(&self, point: [f64; 3], tolerance: f64, distance: f64, how: Continuation) -> Result { - if route::backend() == route::Backend::Solidcore { - return core::trim::extend_sheet(self.core()?, point, tolerance, distance, how).map(Brep::made_core); - } - init()?; - let mode = match how { - Continuation::Natural => ffi::trim::EXTEND_NATURAL, - Continuation::Tangent => ffi::trim::EXTEND_TANGENT, - Continuation::Curvature => ffi::trim::EXTEND_CURVATURE, - }; - ffi::trim::extend_sheet(self.cpp()?, &point, tolerance, distance, mode).map(Brep::made).map_err(|s| match s { - ffi::ops::ERR_PICK_NONE => crate::Error::NoMatch { point: 0 }, - s => error(s), - }) + core::trim::extend_sheet(self.core(), point, tolerance, distance, how).map(Brep::made_core) } /// A point inside each face, in SMLib's face order: on the face's surface, inside its trims /// (`SmFace::CalculateAnInternalPoint`). pub fn face_points(&self) -> Result> { - if route::backend() == route::Backend::Solidcore { - return core::trim::face_points(self.core()?); - } - init()?; - let [faces, _, _] = self.counts()?; - ffi::trim::face_points(self.cpp()?, faces).map_err(error) + core::trim::face_points(self.core()) } } diff --git a/crates/smlib/tests/ssi_corpus.rs b/crates/smlib/tests/ssi_corpus.rs index c2b33b287a..bdc866e788 100644 --- a/crates/smlib/tests/ssi_corpus.rs +++ b/crates/smlib/tests/ssi_corpus.rs @@ -11,9 +11,9 @@ //! cargo test -p solidcraft-smlib --test ssi_corpus -- --ignored --nocapture //! ``` //! -//! With the `native` feature, [`smlib_global_intersect_at_1e_6`] runs every case through SMLib's +//! [`solidcore_global_intersect_at_1e_6`] runs every case through solidcore's //! GlobalSurfaceIntersect (via `solidcraft_smlib::ssi`, as the corpus runner calls it) and checks -//! exactly which cases pass. +//! that every case SMLib passed passes. mod ssi_score; @@ -363,7 +363,7 @@ fn parity_with_python_scorer() { assert!(problems.is_empty(), "{}", problems.join("\n")); } -// ---- SMLib itself (feature `native`) --------------------------------------------------------- +// ---- The kernel -------------------------------------------------------------------------------- /// The cases SMLib's GlobalSurfaceIntersect passes at approximation tolerance 1e-6, as score.py /// grades the corpus runner's output. A fix that makes another case pass adds it here. @@ -564,7 +564,6 @@ fn runner_output( CurveType::Coincident => "COINCIDENT", CurveType::NearTangent => "NEAR_TANGENT", CurveType::RegionBoundary => "REGION_BOUNDARY", - CurveType::Other(_) => "?", }; let xyz = match CurveEval::new(&c.curve) { Ok(e) => params(c.interval).filter_map(|t| e.point(t)).map(|p| [p.x, p.y, p.z]).collect(), @@ -577,58 +576,24 @@ fn runner_output( Outcome::Ran(EngineResult { status: ssi_score::SM_SUCCESS, status_name: None, time_ms: ms, curves, points: Vec::new() }) } -/// One kernel at a time: the choice of kernel is process-wide. -static KERNEL_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(()); - -/// Every case through `backend`'s GlobalSurfaceIntersect at approximation tolerance 1e-6, scored as -/// score.py scores the corpus runner's `gsi 1e-6` run. Prints each case that does not pass. -/// `None` when `SOLIDCRAFT_KERNEL` chose the other kernel: it wins over `set_choice`, and scoring -/// the wrong kernel against this one's list says nothing. -fn global_intersect_scores(backend: solidcraft_smlib::route::Backend) -> Option> { - use solidcraft_smlib::route::{Backend, choice, set_choice}; +/// Every case through GlobalSurfaceIntersect at approximation tolerance 1e-6, scored as score.py +/// scores the corpus runner's `gsi 1e-6` run. Prints each case that does not pass. +fn global_intersect_scores() -> Vec { use solidcraft_smlib::ssi::{Method, intersect}; let t0 = std::time::Instant::now(); let cases = all_cases(); - let outcomes: Vec = { - let _kernel = KERNEL_TESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner); - set_choice(backend); - if choice() != backend { - set_choice(Backend::Solidcore); - eprintln!("skipped: SOLIDCRAFT_KERNEL chose the other kernel"); - return None; - } - // The kernels serialise their calls, so the cases run one after another; only the scoring is parallel. - let outcomes = cases - .iter() - .map(|case| { - let a = surface(&case.surfaces[0]).unwrap_or_else(|e| panic!("{} surface 1: {e}", case.id)); - let b = surface(&case.surfaces[1]).unwrap_or_else(|e| panic!("{} surface 2: {e}", case.id)); - let t = std::time::Instant::now(); - let r = intersect(&a, &b, Method::Global, 1e-6); - runner_output(r, &a, &b, t.elapsed().as_secs_f64() * 1e3) - }) - .collect(); - set_choice(Backend::Solidcore); - outcomes - }; - let ran = t0.elapsed(); - // Status names for the flags, from the SMLib sources the build used, as score.py has them. - let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); - let src = std::env::var_os("SOLIDCRAFT_SMLIB_DIR").map(std::path::PathBuf::from).unwrap_or_else(|| { - let rev = std::fs::read_to_string(manifest.join("SMLIB_REV")).unwrap_or_default(); - manifest.join("../../target").join(format!("smlib-{}", rev.trim().get(..12).unwrap_or_default())) - }); - let names = std::fs::read_to_string(src.join("source/SMLib/inc/SmMessages.h")).map(|h| ssi_score::status_names(&h)).unwrap_or_default(); - let outcomes: Vec = outcomes - .into_iter() - .map(|o| match o { - Outcome::Ran(mut r) if r.status_name.is_none() => { - r.status_name = names.get(&r.status).cloned(); - Outcome::Ran(r) - } - o => o, + // The kernel serialises its calls, so the cases run one after another; only the scoring is parallel. + let outcomes: Vec = cases + .iter() + .map(|case| { + let a = surface(&case.surfaces[0]).unwrap_or_else(|e| panic!("{} surface 1: {e}", case.id)); + let b = surface(&case.surfaces[1]).unwrap_or_else(|e| panic!("{} surface 2: {e}", case.id)); + let t = std::time::Instant::now(); + let r = intersect(&a, &b, Method::Global, 1e-6); + runner_output(r, &a, &b, t.elapsed().as_secs_f64() * 1e3) }) .collect(); + let ran = t0.elapsed(); let pairs: Vec<(&Case, &Outcome)> = cases.iter().zip(&outcomes).collect(); let scores = par_map(&pairs, |(case, out)| score_case(case, out, Tolerances::at(1e-6))); @@ -637,7 +602,7 @@ fn global_intersect_scores(backend: solidcraft_smlib::route::Backend) -> Option< by_verdict.entry(s.verdict).or_default().push(&s.id); } println!( - "{backend:?} GlobalSurfaceIntersect, approx tol 1e-6 ({} cases, kernel {:.1} s, total {:.1} s)", + "GlobalSurfaceIntersect, approx tol 1e-6 ({} cases, kernel {:.1} s, total {:.1} s)", scores.len(), ran.as_secs_f64(), t0.elapsed().as_secs_f64() @@ -648,32 +613,20 @@ fn global_intersect_scores(backend: solidcraft_smlib::route::Backend) -> Option< for s in scores.iter().filter(|s| s.verdict != Verdict::Pass) { println!(" {:<40} {:<15} {}", s.id, s.verdict.as_str(), s.flags.join("; ")); } - Some(scores) + scores } -/// Exactly [`GSI_1E6_PASS`] pass on the C++ kernel. -#[cfg(feature = "native")] -#[test] -fn smlib_global_intersect_at_1e_6() { - let Some(scores) = global_intersect_scores(solidcraft_smlib::route::Backend::Smlib) else { return }; - let passed: std::collections::BTreeSet<&str> = scores.iter().filter(|s| s.verdict == Verdict::Pass).map(|s| s.id.as_str()).collect(); - let listed: std::collections::BTreeSet<&str> = GSI_1E6_PASS.iter().copied().collect(); - assert_eq!(listed.len(), GSI_1E6_PASS.len(), "GSI_1E6_PASS lists a case twice"); - let newly: Vec<_> = passed.difference(&listed).collect(); - let lost: Vec<_> = listed.difference(&passed).collect(); - assert!(newly.is_empty() && lost.is_empty(), "now pass (add to GSI_1E6_PASS): {newly:?}; no longer pass: {lost:?}"); - assert_eq!(passed.len(), 111); -} -/// solidcore passes every case the C++ kernel passes ([`GSI_1E6_PASS`]). The self-crossing +/// solidcore passes every case SMLib passed ([`GSI_1E6_PASS`]). The self-crossing /// curves it missed (Viviani's figure eight, the crossing loops) are split at their crossings /// since solidcore's `ssi_split_curves_at_singular_points` (tangency-labs/solidcore#2110). #[test] fn solidcore_global_intersect_at_1e_6() { - let Some(scores) = global_intersect_scores(solidcraft_smlib::route::Backend::Solidcore) else { return }; + let scores = global_intersect_scores(); let passed: std::collections::BTreeSet<&str> = scores.iter().filter(|s| s.verdict == Verdict::Pass).map(|s| s.id.as_str()).collect(); let listed: std::collections::BTreeSet<&str> = GSI_1E6_PASS.iter().copied().collect(); + assert_eq!(listed.len(), GSI_1E6_PASS.len(), "GSI_1E6_PASS lists a case twice"); let lost: Vec<&str> = listed.difference(&passed).copied().collect(); let extra: Vec<_> = passed.difference(&listed).collect(); - assert!(lost.is_empty(), "solidcore misses what the C++ kernel passes: {lost:?}; extra passes: {extra:?}"); + assert!(lost.is_empty(), "solidcore misses what SMLib passed: {lost:?}; extra passes: {extra:?}"); } diff --git a/tests/KNOWN_FAILING_TESTS b/tests/KNOWN_FAILING_TESTS index a197970b2d..3819272720 100644 --- a/tests/KNOWN_FAILING_TESTS +++ b/tests/KNOWN_FAILING_TESTS @@ -31,12 +31,6 @@ solidcraft-kernel::blend_wrong_way_tests::a_chamfer_that_would_grow_the_part_on_ solidcraft-kernel::corner_g2::tests::a_long_setback_keeps_the_g2_patch_inside_the_box at set backs 11 to 15 the G2 corner patch bulges past the box, as on C++ solidcraft-kernel::corner_g2::tests::curvature_rounds_meet_at_a_g2_corner the corner between curvature rounds jumps 2.07 in curvature over 538 stations on solidcore; the test wants it curvature continuous, as SMLib f916a8d's setback corner (smlib#104), which solidcore ports in solidcore#2310 builds it solidcraft-kernel::corner_g2::tests::the_box_corner_g2_patch_is_a_closed_solid_curvature_continuous_to_its_neighbours the fitted G2 corner patch bulges 1.7e-5 past the box, as on C++ -solidcraft-kernel::fillet_setback_tests::a_setback_not_past_the_radius_is_refused_naming_the_least solidcore builds setback 5.5 (15 faces) where SMLib f916a8d's setback corner (smlib#104), which solidcore ports in solidcore#2310 refuses it naming the least that builds; solidcraft_smlib::blend_fit names no size on solidcore until then +solidcraft-kernel::fillet_setback_tests::a_setback_not_past_the_radius_is_refused_naming_the_least solidcore builds setback 5.5 (15 faces) where SMLib f916a8d's setback corner (smlib#104), which solidcore ports in solidcore#2310 refuses it naming the least that builds; nothing names the least setback on solidcore until then solidcraft-kernel::tests::fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole the R25 rim and a corner arc round together since solidcore#2223, but the two R2 corner arcs picked together fail 1001 in trim_at_creases (cliff_overflow with the crease trims off), though each builds alone (solidcore#2314) -solidcraft-smlib::core::blend_tests::fillet_all_edges::solidcore_matches_the_cpp_kernel the C++ comparison: solidcore's 8 box corners are analytic spheres, C++ 5e8c9c1's rational splines (SMLib 5e8c9c1's blend (Ball corners as rational patches, the round as its definition, smlib#79), which solidcore ports in solidcore#2273) -solidcraft-smlib::core::blend_tests::fillet_variable_linear::solidcore_matches_the_cpp_kernel the C++ comparison: solidcore 5972.141093 against C++ 5e8c9c1's 5972.142573 (SMLib 5e8c9c1's blend (Ball corners as rational patches, the round as its definition, smlib#79), which solidcore ports in solidcore#2273) -solidcraft-smlib::core::blend_tests::rounds_on_the_871_part the C++ comparison: solidcore 3874.348250 against C++ 5e8c9c1's 3874.348458 (SMLib 5e8c9c1's blend (Ball corners as rational patches, the round as its definition, smlib#79), which solidcore ports in solidcore#2273) -solidcraft-smlib::core::blend_tests::setback_corner::solidcore_matches_the_cpp_kernel the C++ comparison: solidcore 5943.191647 against C++ f916a8d's 5948.231174 (SMLib f916a8d's setback corner (smlib#104), which solidcore ports in solidcore#2310) -solidcraft-smlib::core::convert_tests::cpp_round_trip::a_shelled_rounded_box the C++ round trip: the shelled rounded box C++ 5e8c9c1 builds differs from solidcore's in its arrays (real array 154[0]: -10 against 0), its corners built by SMLib 5e8c9c1's blend (Ball corners as rational patches, the round as its definition, smlib#79), which solidcore ports in solidcore#2273 -solidcraft-smlib::core::offset_tests::mitred_rounds_follow_a_moved_top the C++ comparison: solidcore's offset edges keep their tolerances (offset_edges_keep_source_tolerance, solidcore#2111), C++ at 7ac037e's do not, so the moved tops' volumes are 2e-9 to 2.3e-7 apart; both are 1.2e-8 to 3.1e-7 off the closed form at accuracy 1e-8, solidcore with the fix 2.1e-7 at most solidcraft-smlib::ops::tests::a_variable_round_with_a_corner_in_its_law_is_its_balls_union solidcore refuses a variable fillet whose law has a corner (solidcore#430)