smlib crate: the C++ SMLib kernel and its FFI removed

crates/smlib now wraps solidcore alone; the crate keeps its name (the
rename is a later move).
- Gone: the C++ FFI (src/ffi.rs, src/ffi/), build.rs and its `cc`
  build dependency, SMLIB_REV, SMLIB_PREBUILT, the `native` feature, the
  backend routing (src/route.rs: Backend, the choice, SOLIDCRAFT_KERNEL),
  init(), the C API's log and profiler lines, the flat-array handle
  (flat::Flat, Brep::export) and the history records' decoder. Brep is
  solidcore's body; Error loses NotBuilt and Version; set_dump_dir can no
  longer fail. blend_fit/Fit go: only the C++ kernel ever named a fit.
- The `smlib` feature is gone from the kernel, the engine and the apps
  (cli, web, desktop); the kernel's smlib-gated tests and the board fixture
  example run on solidcore, the side-by-side boolean example (a comparison
  with the C++ kernel) is deleted.
- Tests that compared with the C++ kernel are deleted (the parity and
  closed-form halves run on C++, cpp_round_trip, arrays_match_the_cpp_kernel,
  validate_refuses_what_the_cpp_kernel_refuses, the cross-kernel file test,
  the SSI corpus run on C++). Their solidcore-only assertions stay in the
  same files as checks against solidcore: the blend and boolean history
  changes, the offset drafts (one Unsupported, all building), the L sheet's
  thickened volume, the 871 rounds and the moved mitred top. The tests that
  were built only with the C++ kernel and already ran on solidcore (ops,
  sweeps, check, measure, import, export) are ungated.
- tests/KNOWN_FAILING_TESTS drops the six lines of the deleted comparisons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
This commit is contained in:
jyviko
2026-11-02 18:01:31 -05:00
parent fa1b4a9b9d
commit aa87a149f8
73 changed files with 511 additions and 7451 deletions
Generated
-2
View File
@@ -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]]
-4
View File
@@ -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"]
+4 -14
View File
@@ -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<Value> = 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();
-4
View File
@@ -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"]
-4
View File
@@ -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"]
-2
View File
@@ -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 = []
+2 -2
View File
@@ -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 }
+1 -7
View File
@@ -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 }
File diff suppressed because it is too large Load Diff
+1 -10
View File
@@ -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};
+1 -9
View File
@@ -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),
})?;
@@ -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;
+2 -2
View File
@@ -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() {
+1 -3
View File
@@ -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 {
@@ -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;
-1
View File
@@ -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};
+1 -19
View File
@@ -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");
-10
View File
@@ -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(&section.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};
-1
View File
@@ -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;
+4 -9
View File
@@ -54,14 +54,9 @@ fn smlib_validity(body: &Body) -> Option<SmlibCheck> {
})
}
/// 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);
}
+3 -27
View File
@@ -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"
-10
View File
@@ -1,10 +0,0 @@
# Rust target, SHA-256 of smlib-<target>.tar.gz in the release prebuilt-<SMLIB_REV> 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
-1
View File
@@ -1 +0,0 @@
a18bbe523b9c7819f1a9da8c40a54c15f9936125
-179
View File
@@ -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-<rev>-prebuilt/<target>`, when its SHA-256 is the one in `SMLIB_PREBUILT`;
//! - the sources in `target/smlib-<rev>` (`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<Option<String>, 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<PathBuf, String> {
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<Vec<PathBuf>, String> {
let mut v: Vec<PathBuf> = 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)
}
+3 -18
View File
@@ -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<CheckReport> {
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()));
+1 -3
View File
@@ -398,10 +398,8 @@ pub(crate) static KERNEL: Mutex<Option<Kernel>> = Mutex::new(None);
pub(crate) fn run<T: dump::Answer>(call: &'static str, f: impl FnOnce(&mut Kernel) -> std::result::Result<T, solidcore::SolidError>) -> Result<T> {
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))) {
+6 -2
View File
@@ -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 {
+42 -230
View File
@@ -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<i64> =
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<Change> {
let mut c: Vec<Change> = 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<Change> = 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);
}
}
+139 -368
View File
@@ -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<Measured> {
})
}
/// The case on the C++ kernel, called directly (whatever the choice): what it measured.
#[cfg(feature = "native")]
fn cpp_case(name: &str) -> Option<Measured> {
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<Brep> = 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<Brep> = (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<Curve> = (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<Brep> = (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:?}");
}
}
-5
View File
@@ -2492,11 +2492,6 @@ fn extent2d(u0: f64, v0: f64, u1: f64, v1: f64) -> Result<Extent2d> {
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
+2 -464
View File
@@ -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>) -> 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::<Vec<_>>(), got.ints.keys().collect::<Vec<_>>(), "{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::<Vec<_>>(), got.reals.keys().collect::<Vec<_>>(), "{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<Curve> = (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<Curve> = (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<f64>); 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);
}
}
+6 -198
View File
@@ -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, `<reporting type>::Test #<n>`, 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:?}");
}
}
+1 -98
View File
@@ -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());
}
}
+58 -318
View File
@@ -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, &notch, 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<Curve> = (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<Curve> = (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<Curve> = (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<Curve> = (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<Curve> = (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);
}
+2 -2
View File
@@ -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;
+1 -111
View File
@@ -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<crate::ssi::Surface> = tools.iter().map(surface).collect();
let handles: Vec<&ffi::ssi::SurfaceHandle> = made.iter().map(|s| s.handle().unwrap()).collect();
let faces: Vec<i64> = 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
+1 -2
View File
@@ -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`).
+18 -12
View File
@@ -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<T> = std::result::Result<T, Error>;
@@ -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<Curve> {
let count = points.len();
if !(1..=ffi::sweeps::CURVE_MAX_DEGREE).contains(&degree) || count < degree + 1 || count > ffi::sweeps::CURVE_MAX_POINTS {
if !(1..=CURVE_MAX_DEGREE).contains(&degree) || 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<Vec<Curve>> {
}
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<Out<(Profile, [f64; 2])>> {
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(&degree)
if !(1..=CURVE_MAX_DEGREE).contains(&degree)
|| count < degree + 1
|| count > ffi::sweeps::CURVE_MAX_POINTS
|| count > CURVE_MAX_POINTS
|| knots.len() != count + degree + 1
|| !clamped
{
+4 -155
View File
@@ -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<String, usize>| kinds.iter().filter(|(k, _)| !k.starts_with("plane")).map(|(_, n)| *n).sum::<usize>();
@@ -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<dyn Fn() -> 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]
+1 -86
View File
@@ -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:?}");
}
}
}
+2 -113
View File
@@ -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));
}
}
)*};
}
+7 -41
View File
@@ -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<Brep> {
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<Brep> {
if crate::route::backend() == crate::route::Backend::Solidcore {
return crate::core::make_sphere(center, radius).map(Brep::made_core);
}
init()?;
ffi::make_sphere(&center, 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<Brep> {
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<Brep> {
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(&center, 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<Arrays> {
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<Arrays> {
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.
-2
View File
@@ -1,7 +1,5 @@
#[cfg(feature = "native")]
use super::*;
#[cfg(feature = "native")]
mod native_tests {
use super::*;
use crate::{CheckLevel, MEASURE_ACCURACY};
-405
View File
@@ -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<LogFn>, 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<RawBrep>);
// 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<Handle, i32> {
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<fn(i32, String)>) {
let log: Option<LogFn> = 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<Handle, i32> {
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<Handle, i32> {
new_brep(|out| unsafe { smlib_brep_copy(b.raw(), out) })
}
pub fn transform(b: &Handle, matrix: &[[f64; 4]; 4]) -> Result<Handle, i32> {
new_brep(|out| unsafe { smlib_brep_transform(b.raw(), matrix.as_ptr(), out) })
}
pub fn make_box(origin: &[f64; 3], size: &[f64; 3]) -> Result<Handle, i32> {
new_brep(|out| unsafe { smlib_make_box(origin.as_ptr(), size.as_ptr(), out) })
}
pub fn volume(b: &Handle, relative_accuracy: f64) -> Result<f64, i32> {
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<fn(i32, String)>) {}
pub fn set_dump_dir(_: Option<&CStr>) -> i32 {
NOT_BUILT
}
pub fn current_call(_: &mut [u8]) -> usize {
0
}
pub fn read(_: &CStr, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
pub fn write(_: &Handle, _: &CStr, _: bool) -> i32 {
NOT_BUILT
}
pub fn copy(_: &Handle) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
pub fn transform(_: &Handle, _: &[[f64; 4]; 4]) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
pub fn make_box(_: &[f64; 3], _: &[f64; 3]) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
pub fn volume(_: &Handle, _: f64) -> Result<f64, i32> {
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<fn(i32, String)> = 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<Handle, i32> {
new_brep(|out| unsafe { smlib_make_cylinder(origin.as_ptr(), radius, height, out) })
}
pub fn make_sphere(center: &[f64; 3], radius: f64) -> Result<Handle, i32> {
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<Handle, i32> {
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<Handle, i32> {
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<Handle, i32> {
Err(NOT_BUILT)
}
pub fn make_sphere(_: &[f64; 3], _: f64) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
pub fn make_cone(_: &[f64; 3], _: f64, _: f64, _: f64) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
pub fn make_torus(_: &[f64; 3], _: f64, _: f64) -> Result<Handle, i32> {
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<web_time::Instant> = 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()
}
}
-36
View File
@@ -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))
}
-134
View File
@@ -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<RawFlat>);
// Only ever used through the C API, which serialises every call.
unsafe impl Send for FlatHandle {}
unsafe impl Sync for FlatHandle {}
impl Drop for FlatHandle {
fn drop(&mut self) {
#[cfg(feature = "native")]
unsafe {
smlib_flat_free(self.0.as_ptr())
};
}
}
#[cfg(feature = "native")]
mod calls {
use super::*;
use crate::ffi::{ERR_ARG, ERR_EMPTY, ERR_EXCEPTION};
pub fn new() -> Result<FlatHandle, i32> {
std::ptr::NonNull::new(unsafe { smlib_flat_new() }).map(FlatHandle).ok_or(ERR_EXCEPTION)
}
pub fn export(b: &Handle) -> Result<FlatHandle, i32> {
let mut out: *mut RawFlat = std::ptr::null_mut();
match (unsafe { smlib_brep_export(b.raw(), &mut out) }, std::ptr::NonNull::new(out)) {
(OK, Some(p)) => Ok(FlatHandle(p)),
(OK, None) => Err(ERR_EMPTY),
(s, _) => Err(s),
}
}
pub fn import(f: &FlatHandle) -> Result<Handle, i32> {
new_brep(|out| unsafe { smlib_brep_import(f.0.as_ptr(), out) })
}
/// Copies the array out: the C side's buffer lives only as long as the arrays are unchanged.
fn copy<T: Copy>(s: i32, data: *const T, len: i64) -> Result<Vec<T>, i32> {
if s != OK {
return Err(s);
}
let n = usize::try_from(len).map_err(|_| ERR_ARG)?;
if n == 0 || data.is_null() {
return Ok(Vec::new());
}
Ok(unsafe { std::slice::from_raw_parts(data, n) }.to_vec())
}
pub fn get_i64(f: &FlatHandle, id: i32) -> Result<Vec<i64>, i32> {
let (mut data, mut len) = (std::ptr::null(), 0i64);
let s = unsafe { smlib_flat_get_i64(f.0.as_ptr(), id, &mut data, &mut len) };
copy(s, data, len)
}
pub fn get_f64(f: &FlatHandle, id: i32) -> Result<Vec<f64>, i32> {
let (mut data, mut len) = (std::ptr::null(), 0i64);
let s = unsafe { smlib_flat_get_f64(f.0.as_ptr(), id, &mut data, &mut len) };
copy(s, data, len)
}
pub fn set_i64(f: &mut FlatHandle, id: i32, v: &[i64]) -> i32 {
let Ok(n) = i64::try_from(v.len()) else { return ERR_ARG };
unsafe { smlib_flat_set_i64(f.0.as_ptr(), id, v.as_ptr(), n) }
}
pub fn set_f64(f: &mut FlatHandle, id: i32, v: &[f64]) -> i32 {
let Ok(n) = i64::try_from(v.len()) else { return ERR_ARG };
unsafe { smlib_flat_set_f64(f.0.as_ptr(), id, v.as_ptr(), n) }
}
pub fn is_manifold_solid(b: &Handle) -> Result<bool, i32> {
let mut out = 0i32;
match unsafe { smlib_brep_is_manifold_solid(b.raw(), &mut out) } {
OK => Ok(out != 0),
s => Err(s),
}
}
}
/// The same calls when SMLib is not built: each one fails with `NOT_BUILT`.
#[cfg(not(feature = "native"))]
mod calls {
use super::*;
use crate::ffi::NOT_BUILT;
pub fn new() -> Result<FlatHandle, i32> {
Err(NOT_BUILT)
}
pub fn export(_: &Handle) -> Result<FlatHandle, i32> {
Err(NOT_BUILT)
}
pub fn import(_: &FlatHandle) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
pub fn get_i64(_: &FlatHandle, _: i32) -> Result<Vec<i64>, i32> {
Err(NOT_BUILT)
}
pub fn get_f64(_: &FlatHandle, _: i32) -> Result<Vec<f64>, i32> {
Err(NOT_BUILT)
}
pub fn set_i64(_: &mut FlatHandle, _: i32, _: &[i64]) -> i32 {
NOT_BUILT
}
pub fn set_f64(_: &mut FlatHandle, _: i32, _: &[f64]) -> i32 {
NOT_BUILT
}
pub fn is_manifold_solid(_: &Handle) -> Result<bool, i32> {
Err(NOT_BUILT)
}
}
pub use calls::*;
-53
View File
@@ -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<Handle, i32> {
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<Vec<i64>, i32> {
let mut len = 0i64;
let mut buf: Vec<i64> = 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<Handle, i32> {
Err(super::NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn read(_: &Handle) -> Result<Vec<i64>, i32> {
Err(super::NOT_BUILT)
}
-95
View File
@@ -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<f64, i32> {
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<f64, i32> {
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<i32, i32> {
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<i32, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn edge_face_distance(_: &Handle, _: i64, _: i64) -> Result<f64, i32> {
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<f64, i32> {
Err(NOT_BUILT)
}
-563
View File
@@ -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<usize>);
/// 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<usize>, Option<usize>);
/// 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<Handle, i32> {
new_brep(|out| unsafe { smlib_boolean(a.raw(), b.raw(), op, out) })
}
#[cfg(feature = "native")]
pub fn boolean_union_all(breps: &[&Handle]) -> Result<Handle, i32> {
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<Handle, i32> {
new_brep(|out| unsafe { smlib_boolean2d(a.raw(), b.raw(), op, out) })
}
#[cfg(feature = "native")]
pub fn imprint(body: &Handle, tool: &Handle) -> Result<Handle, i32> {
new_brep(|out| unsafe { smlib_imprint(body.raw(), tool.raw(), out) })
}
#[cfg(feature = "native")]
pub fn solid_count(b: &Handle) -> Result<i64, i32> {
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<Handle, PickError> {
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::<f64>();
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<Handle, i32> {
new_brep(|out| unsafe { smlib_thicken(b.raw(), thickness, i32::from(symmetric), out) })
}
#[cfg(not(feature = "native"))]
pub fn thicken(_: &Handle, _: f64, _: bool) -> Result<Handle, i32> {
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<Handle, PickError> {
let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None))?;
let flat = points.as_ptr().cast::<f64>();
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<Handle, PickError> {
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<Handle, PickError> {
let n = i64::try_from(keep.len()).map_err(|_| (ERR_ARG, None))?;
let flat = keep.as_ptr().cast::<f64>();
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<Handle, PickError> {
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<Handle, 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::<f64>();
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<Handle, 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::<f64>();
let flat_corners = corners.as_ptr().cast::<f64>();
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<Handle, SetbackError> {
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<i64>), 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::<f64>();
let flat_corners = corners.as_ptr().cast::<f64>();
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<i64>), 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<Handle, i32> {
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<Handle, PickError> {
let n = i64::try_from(points.len()).map_err(|_| (ERR_ARG, None))?;
let flat = points.as_ptr().cast::<f64>();
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<Handle, PickError> {
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<bool>), 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::<f64>();
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<bool>), PickError> {
Err((NOT_BUILT, None))
}
#[cfg(not(feature = "native"))]
pub fn blend(_: i32, _: &Handle, _: &[[f64; 3]], _: &[f64], _: f64) -> Result<Handle, PickError> {
Err((NOT_BUILT, None))
}
#[cfg(not(feature = "native"))]
pub fn fillet_variable(_: &Handle, _: &[f64; 3], _: f64, _: &[f64; 3], _: i32, _: &[f64], _: &[f64]) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn boolean(_: &Handle, _: &Handle, _: i32) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn boolean_union_all(_: &[&Handle]) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn boolean2d(_: &Handle, _: &Handle, _: i32) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn imprint(_: &Handle, _: &Handle) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn solid_count(_: &Handle) -> Result<i64, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn pick_op(_: Pick, _: &Handle, _: &[[f64; 3]], _: f64, _: f64) -> Result<Handle, PickError> {
Err((NOT_BUILT, None))
}
-40
View File
@@ -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<Handle, i32> {
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<Handle, i32> {
Err(NOT_BUILT)
}
-96
View File
@@ -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<i64>), 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<i64>), 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)
}
-378
View File
@@ -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<RawSurface>);
/// An owned `smlib_ssi_result`, freed once on drop.
#[cfg_attr(not(feature = "native"), allow(dead_code))]
pub struct ResultHandle(std::ptr::NonNull<RawResult>);
// 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<f64>,
pub weights: Vec<f64>,
pub knots: Vec<f64>,
}
#[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<SurfaceHandle, i32> {
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<SurfaceHandle, i32> {
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<SurfaceHandle, i32> {
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<SurfaceHandle, i32> {
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<SurfaceHandle, i32> {
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<SurfaceHandle, i32> {
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<SurfaceHandle, i32> {
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<ResultHandle, i32> {
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<i32, i32> {
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<CurveInfo, i32> {
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<Nurbs, i32> {
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<i32, i32> {
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<SurfaceHandle, i32> {
Err(NOT_BUILT)
}
pub fn cylinder(_: &[f64; 3], _: &[f64; 3], _: &[f64; 3], _: f64, _: &[f64; 2], _: &[f64; 2]) -> Result<SurfaceHandle, i32> {
Err(NOT_BUILT)
}
pub fn cone(_: &[f64; 3], _: &[f64; 3], _: &[f64; 3], _: f64, _: &[f64; 2]) -> Result<SurfaceHandle, i32> {
Err(NOT_BUILT)
}
pub fn sphere(_: &[f64; 3], _: &[f64; 3], _: &[f64; 3], _: f64, _: &[f64; 2], _: &[f64; 2]) -> Result<SurfaceHandle, i32> {
Err(NOT_BUILT)
}
pub fn torus(_: &[f64; 3], _: &[f64; 3], _: &[f64; 3], _: f64, _: f64, _: &[f64; 2], _: &[f64; 2]) -> Result<SurfaceHandle, i32> {
Err(NOT_BUILT)
}
pub fn nurbs(_: [i32; 2], _: i32, _: i32, _: &[f64], _: &[f64], _: &[f64], _: Option<&[f64]>) -> Result<SurfaceHandle, i32> {
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<ResultHandle, i32> {
Err(NOT_BUILT)
}
pub fn curve_count(_: &ResultHandle) -> Result<i32, i32> {
Err(NOT_BUILT)
}
pub fn curve_info(_: &ResultHandle, _: i32) -> Result<CurveInfo, i32> {
Err(NOT_BUILT)
}
pub fn curve_nurbs(_: &ResultHandle, _: i32, _: i32, _: usize) -> Result<Nurbs, i32> {
Err(NOT_BUILT)
}
pub fn point_count(_: &ResultHandle) -> Result<i32, i32> {
Err(NOT_BUILT)
}
pub fn point(_: &ResultHandle, _: i32) -> Result<([f64; 3], [f64; 2], [f64; 2]), i32> {
Err(NOT_BUILT)
}
}
pub use calls::*;
-431
View File
@@ -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<Handle, i32> {
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<Handle, i32> {
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<Handle, i32> {
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<Handle, i32> {
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<Handle, i32> {
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<i64>,
pub reals: Vec<f64>,
pub intervals: Vec<f64>,
}
/// `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<OutCurves, i32> {
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<OutCurves, i32> {
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<OutCurves, i32> {
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<OutCurves, i32> {
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<Handle, i32> {
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<Handle, i32> {
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<Handle, i32> {
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<Handle, i32> {
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<Handle, i32> {
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<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn extrude(_: &List, _: &[f64; 3], _: f64, _: f64, _: i32, _: bool, _: &mut [f64; 3]) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn revolve(_: &List, _: &[f64; 3], _: &[f64; 3], _: f64, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn sweep(_: &List, _: &List, _: i32, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn sweep_placed(_: &List, _: &List, _: i32, _: bool, _: &mut [f64; 36]) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn project_curves(_: &Handle, _: &[f64; 3], _: f64, _: &List, _: &[f64; 3], _: f64) -> Result<OutCurves, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn face_section(_: &Handle, _: &[f64; 3], _: f64, _: &[f64; 3], _: &[f64; 3], _: f64) -> Result<OutCurves, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn loft(_: &List, _: &[i64], _: i32, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn loft_to_point(_: &List, _: &[f64; 3]) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn screw_sweep(_: &List, _: &[f64; 3], _: &[f64; 3], _: f64, _: f64, _: f64) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn screw_sweep_radial(_: &List, _: &[f64; 3], _: &[f64; 3], _: f64, _: f64, _: &[f64], _: f64) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn sweep_stations(_: &List, _: &[f64], _: i32) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
-72
View File
@@ -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<Handle, i32> {
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<Handle, i32> {
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<Handle, i32> {
let (ptr, n) = match holes {
Some(h) => (h.as_ptr().cast::<f64>(), 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<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(feature = "native")]
pub fn face_points(body: &Handle, n_faces: usize) -> Result<Vec<[f64; 3]>, 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::<f64>(), n) };
if s == super::OK { Ok(out) } else { Err(s) }
}
#[cfg(not(feature = "native"))]
pub fn face_points(_: &Handle, _: usize) -> Result<Vec<[f64; 3]>, i32> {
Err(NOT_BUILT)
}
+4 -60
View File
@@ -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<Flat> {
init()?;
ffi::flat::new().map(Flat).map_err(error)
}
/// The arrays of `a` (see [`crate::import::arrays`]).
pub fn from_arrays(a: &crate::import::Arrays) -> Result<Flat> {
let mut f = Flat::new()?;
for (id, v) in &a.ints {
f.set_i64(*id, v)?;
}
for (id, v) in &a.reals {
f.set_f64(*id, v)?;
}
Ok(f)
}
pub fn get_i64(&self, id: i32) -> Result<Vec<i64>> {
ffi::flat::get_i64(&self.0, id).map_err(error)
}
pub fn get_f64(&self, id: i32) -> Result<Vec<f64>> {
ffi::flat::get_f64(&self.0, id).map_err(error)
}
pub fn set_i64(&mut self, id: i32, v: &[i64]) -> Result<()> {
crate::check(ffi::flat::set_i64(&mut self.0, id, v))
}
pub fn set_f64(&mut self, id: i32, v: &[f64]) -> Result<()> {
crate::check(ffi::flat::set_f64(&mut self.0, id, v))
}
/// The SMLib B-rep these arrays describe (checked on the C side before SMLib sees them).
pub fn import(&self) -> Result<Brep> {
init()?;
ffi::flat::import(&self.0).map(Brep::made).map_err(error)
}
}
impl Brep {
/// This body as flat arrays.
pub fn export(&self) -> Result<Flat> {
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<bool> {
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())
}
}
+4 -100
View File
@@ -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<Brep> {
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<Raw> {
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<usize> = (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<RawEdge> = self
.edges
.iter()
.map(|e| {
let mut f: Vec<usize> = 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<Raw> {
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::<Option<Vec<_>>>()?;
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<usize> {
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<Vec<(usize, usize)>> {
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;
+22 -39
View File
@@ -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![(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();
+4 -7
View File
@@ -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<crate::Brep> {
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`].
-2
View File
@@ -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
+27 -222
View File
@@ -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<T> = std::result::Result<T, Error>;
/// 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> {
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<String> {
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<Result<ffi::Handle>>,
core: OnceLock<Result<core::Body>>,
/// 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<usize>,
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<Brep> {
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<Brep> {
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<Brep> {
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<Brep> {
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<f64> {
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())
}
}
+9 -38
View File
@@ -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<PointClass> {
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<MassProperties> {
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<f64> {
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<f64> {
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,
+25 -228
View File
@@ -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<Fit> {
// 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<HoleEdge>,
}
/// The holes `smlib_blend_setback_open` wrote: `corners` of them, each n and n triples.
fn holes(values: &[i64], corners: usize) -> Option<Vec<Hole>> {
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<Option<Brep>> {
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<Brep> {
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::<Result<Vec<&core::Body>>>()?;
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::<Result<Vec<&ffi::Handle>>>()?;
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<Option<Brep>> {
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<Option<Brep>> {
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<usize> {
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<Brep> {
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<Brep> {
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<Brep> {
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<Brep> {
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<Brep> {
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<Brep> {
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<Brep> {
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)]
+3 -10
View File
@@ -1,6 +1,5 @@
//! The operations against analytic volumes and topology counts (built with SMLib only). `SMLIB_OPS_SHOTS=<dir>` also
//! The operations against analytic volumes and topology counts. `SMLIB_OPS_SHOTS=<dir>` 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];
-171
View File
@@ -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> {
[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<bool> = 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<Backend> {
static ENV: OnceLock<Option<Backend>> = 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);
}
}
+3 -11
View File
@@ -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<Brep> {
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)
}
}
+7 -34
View File
@@ -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<Option<(Brep, Vec<usize>)>> {
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<i64> = faces.iter().map(|f| i64::try_from(*f).map_err(|_| Error::Argument)).collect::<Result<_>>()?;
let handles: Vec<&ffi::ssi::SurfaceHandle> = tools.iter().map(|t| t.handle()).collect::<Result<_>>()?;
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<Option<(Brep, Brep)>> {
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<Option<(Brep, Brep)>> {
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))))
}
}
+23 -150
View File
@@ -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<Result<ffi::ssi::SurfaceHandle>>,
/// solidcore's, made when it is first needed.
core: OnceLock<Result<crate::core::split::Tool>>,
spec: Spec,
@@ -39,11 +38,7 @@ impl std::fmt::Debug for Surface {
}
}
fn int(n: usize) -> Result<i32> {
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<ffi::ssi::SurfaceHandle> {
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<f64> = 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<Surface> {
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<SsiPoint>,
}
/// 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<f64>,
pub weights: Vec<f64>,
pub knots: Vec<f64>,
}
/// 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<Option<b::Curve2>> {
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<SsiResult> {
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]);
+23 -232
View File
@@ -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<i64>,
reals: Vec<f64>,
}
impl List {
pub(crate) fn of<'a>(curves: impl IntoIterator<Item = &'a Curve>) -> Result<List> {
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<bool> = 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<Brep> {
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<Brep> {
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<Brep> {
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<Brep> {
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<Vec<(Curve, [f64; 2])>> {
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<Vec<(Curve, [f64; 2])>> {
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<Curve>], degree: u32, cap: bool) -> Result<Brep> {
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(&degree) {
return Err(Error::Argument);
}
let l = List::of(sections.iter().flatten())?;
let sizes: Vec<i64> = sections.iter().map(|s| i64::try_from(s.len()).map_err(|_| Error::Argument)).collect::<Result<_>>()?;
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<Brep> {
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<f64> = 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<Brep> {
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<Brep> {
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<Brep> {
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<f64> = 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<Vec<(Curve, [f64; 2])>> {
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(&degree), 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;
+2 -3
View File
@@ -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();
+14 -131
View File
@@ -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<String> =
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<T>(name: &str, r: Result<T>) -> Option<T> {
match r {
Err(Error::NotBuilt) => panic!("{name} needs the C++ kernel"),
r => r.ok(),
}
fn says<T>(_name: &str, r: Result<T>) -> Option<T> {
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};
+4 -28
View File
@@ -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<Brep> {
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<Vec<[f64; 3]>> {
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())
}
}
+22 -69
View File
@@ -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<Vec<Score>> {
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<Score> {
use solidcraft_smlib::ssi::{Method, intersect};
let t0 = std::time::Instant::now();
let cases = all_cases();
let outcomes: Vec<Outcome> = {
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<Outcome> = 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<Outcome> = 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:?}");
}
+1 -7
View File
@@ -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)