Wave 43/45: smlib: rustfmt after the C++ kernel's removal (and 1 more)
2 commits, squashed; the originals stay reachable from branch jyviko/merge-main-develop-solidcore (#11360). -19db2d7c0jyviko: Docs and comments: solidcore is the kernel -f1ee045dejyviko: smlib: rustfmt after the C++ kernel's removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
This commit is contained in:
1 parent
53211ab6ff
commit
c5ccb9201d
143 files changed
+730
-8689
No files matched your search
@@ -470,13 +470,6 @@ jobs:
|
||||
scope=(--workspace)
|
||||
if [ "$FULL" != true ]; then scope=(); for p in $PACKAGES; do scope+=(-p "$p"); done; fi
|
||||
cargo clippy "${scope[@]}" --all-targets --profile ci --locked -- -D warnings
|
||||
# The C++ SMLib kernel is opt-in (`--features smlib`, off by default): lint the code and tests
|
||||
# behind it too, on the crates that have any. Needs the prebuilt library.
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- name: Clippy with the C++ SMLib kernel
|
||||
run: cargo clippy -p solidcraft-smlib -p solidcraft-kernel -p solidcraft-cli --features solidcraft-cli/smlib --all-targets --profile ci --locked -- -D warnings
|
||||
|
||||
build-tests:
|
||||
name: Build tests
|
||||
@@ -635,69 +628,6 @@ jobs:
|
||||
path: ${{ env.CARGO_TARGET_DIR || 'target' }}/nextest/ci/junit.xml
|
||||
retention-days: 7
|
||||
|
||||
cpp-tests:
|
||||
name: Native Linux tests (C++ SMLib)
|
||||
# The tests that compare solidcore with the C++ SMLib kernel (parity tests, the C++ round trips,
|
||||
# the C++ SSI corpus score, the model cache across kernels) are built only with the opt-in
|
||||
# `smlib` feature, which every other job leaves off. They live in solidcraft-smlib,
|
||||
# solidcraft-kernel and the CLI's model_cache; the other tests there run again here on
|
||||
# solidcore, the default kernel either way, graded against the same list.
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && needs.changes.result == 'success' && needs.changes.outputs.native == 'true' }}
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 20G
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Size parallel compiles to the job's memory
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-cpp-tests
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ github.ref == 'refs/heads/main' && 'on-change' || 'false' }}
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-cpp-tests
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
version: v0.16.0
|
||||
- uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 # v2.87.28
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: Run the tests with the C++ kernel built
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
# The pull request filter of Native Linux tests (default() in .config/nextest.toml);
|
||||
# elsewhere everything but the random boolean property tests, as there.
|
||||
base='default()'
|
||||
if [ "$EVENT_NAME" != pull_request ]; then
|
||||
base='not (package(solidcraft-kernel) & binary(boolean_props))'
|
||||
fi
|
||||
junit=${CARGO_TARGET_DIR:-target}/nextest/ci/junit.xml
|
||||
rm -f "$junit"
|
||||
status=0
|
||||
cargo nextest run -p solidcraft-smlib -p solidcraft-kernel -p solidcraft-cli --features solidcraft-cli/smlib \
|
||||
--cargo-profile ci --locked --profile ci --no-tests=pass --ignore-default-filter -E "($base) & (not package(=solidcraft-cli) | binary(=model_cache))" || status=$?
|
||||
if [ "$status" -ne 0 ] && [ "$status" -ne 100 ]; then exit "$status"; fi
|
||||
cargo xtask known-failing --junit "$junit"
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
|
||||
gpu-path-tests:
|
||||
name: GPU path tests (${{ matrix.shard }}/4)
|
||||
# The GPU path tracer's tests (about 108) compile their pipelines on lavapipe, one test at a
|
||||
@@ -1055,7 +985,7 @@ jobs:
|
||||
# commit's real run instead: the merge queue reads `ci` from the newest run of a commit, so a
|
||||
# run without a passing `ci` would block a pull request that passed (#1850, #3182, #3252).
|
||||
name: ci
|
||||
needs: [changes, checks, quick-compile, clippy, build-tests, tests, cpp-tests, gpu-path-tests, doc-tests, oracle, windows-fxc, windows-startup, dependency-advisories, wasm]
|
||||
needs: [changes, checks, quick-compile, clippy, build-tests, tests, gpu-path-tests, doc-tests, oracle, windows-fxc, windows-startup, dependency-advisories, wasm]
|
||||
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
permissions:
|
||||
@@ -1103,7 +1033,6 @@ jobs:
|
||||
BUILD: ${{ needs.build-tests.result }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
CPP_TESTS: ${{ needs.cpp-tests.result }}
|
||||
GPU_RUN: ${{ needs.changes.outputs.gpu }}
|
||||
GPU: ${{ needs.gpu-path-tests.result }}
|
||||
DOCS: ${{ needs.doc-tests.result }}
|
||||
@@ -1128,7 +1057,6 @@ jobs:
|
||||
test "$BUILD" = success
|
||||
fi
|
||||
test "$TESTS" = success
|
||||
test "$CPP_TESTS" = success
|
||||
fi
|
||||
# The same output decides whether the GPU path tests run (their job's `if`).
|
||||
if [ "$GPU_RUN" = true ]; then
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# The fillet rig as a regression gate: a pull request that touches fillet code (the paths below)
|
||||
# runs every case in tests/kernel-cases/fillets/ on solidcore, through one release build of
|
||||
# runs every case in tests/kernel-cases/fillets/ through one release build of
|
||||
# solidcraft-cli, and fails on a regression (a case that fails and
|
||||
# tests/kernel-cases/fillets/KNOWN_FAILING.solidcore does not list) or on a listed case that now passes
|
||||
# tests/kernel-cases/fillets/KNOWN_FAILING does not list) or on a listed case that now passes
|
||||
# (it is fixed: remove its line, so the list only shrinks). See tests/kernel-cases/README.md.
|
||||
#
|
||||
# GitHub filters a workflow by whole files, so the fillet parts of crates/doc/src/eval*.rs and
|
||||
@@ -25,7 +25,6 @@ on:
|
||||
# The rig itself: the grading, the runner and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "scripts/fetch-smlib.sh"
|
||||
- ".github/workflows/fillet-rig.yml"
|
||||
push:
|
||||
branches: [main, develop]
|
||||
@@ -42,7 +41,6 @@ on:
|
||||
# The rig itself: the grading, the runner and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "scripts/fetch-smlib.sh"
|
||||
- ".github/workflows/fillet-rig.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -90,13 +88,13 @@ jobs:
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
version: v0.16.0
|
||||
- name: Build the CLI (release, solidcore alone)
|
||||
- name: Build the CLI (release)
|
||||
run: cargo build --release --locked -p solidcraft-cli
|
||||
- name: Run the fillet cases against the known-failing list
|
||||
run: >-
|
||||
cargo run -q --locked -p xtask -- kernel-cases --area fillets
|
||||
--cli target/release/solidcraft-cli --out "$RUNNER_TEMP/fillet-rig"
|
||||
--known-failing tests/kernel-cases/fillets/KNOWN_FAILING.solidcore
|
||||
--known-failing tests/kernel-cases/fillets/KNOWN_FAILING
|
||||
- name: Keep the results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# changed files to the rig areas they can affect (scripts/ci/kernel_rig_plan.py): an area runs
|
||||
# when a file its tests/kernel-cases/<area>/PATHS lists changes, or one of its cases; every area
|
||||
# runs when the shared core in tests/kernel-cases/PATHS-ALL changes; the ladder runs whenever any
|
||||
# area does. One job then builds solidcraft-cli once (release, SMLib prebuilt), checks that every
|
||||
# area does. One job then builds solidcraft-cli once (release), checks that every
|
||||
# registry command has a case or an exemption (tests/kernel-cases/EXEMPT), and hands the CLI to a
|
||||
# matrix of shards as an artifact, one per selected area plus the ladder, in parallel, so the
|
||||
# wall time is the build plus the slowest shard. See tests/kernel-cases/README.md.
|
||||
#
|
||||
# - An area, on solidcore, fails on a regression (a case that fails and its
|
||||
# tests/kernel-cases/<area>/KNOWN_FAILING.solidcore does not list) or on a listed case that passes (it is fixed: remove its line in the same pull
|
||||
# - An area fails on a regression (a case that fails and its tests/kernel-cases/<area>/KNOWN_FAILING
|
||||
# does not list) or on a listed case that passes (it is fixed: remove its line in the same pull
|
||||
# request, so the list only shrinks).
|
||||
# - The ladder fails on any case whose status or counts differ from
|
||||
# tests/kernel-cases/ladder/golden.json; a right change is re-recorded (`--record "why"`).
|
||||
@@ -38,7 +38,6 @@ on:
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_ladder.rs"
|
||||
- "scripts/fetch-smlib.sh"
|
||||
- "scripts/ci/kernel_rig_plan.py"
|
||||
- "scripts/ci/test_kernel_rig_plan.py"
|
||||
- ".github/workflows/kernel-rig.yml"
|
||||
@@ -58,7 +57,6 @@ on:
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_ladder.rs"
|
||||
- "scripts/fetch-smlib.sh"
|
||||
- "scripts/ci/kernel_rig_plan.py"
|
||||
- "scripts/ci/test_kernel_rig_plan.py"
|
||||
- ".github/workflows/kernel-rig.yml"
|
||||
@@ -141,16 +139,13 @@ jobs:
|
||||
with:
|
||||
key: kernel-rig
|
||||
save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
version: v0.16.0
|
||||
- name: Build the CLI (release, SMLib) and the rig's runner
|
||||
- name: Build the CLI (release) and the rig's runner
|
||||
run: |
|
||||
cargo build --release --locked -p solidcraft-cli --features smlib
|
||||
cargo build --release --locked -p solidcraft-cli
|
||||
cargo build --locked -p xtask
|
||||
# A tar keeps the executable bits, which an artifact drops. xtask finds the repository from
|
||||
# the path it was built in (CARGO_MANIFEST_DIR), which is the same checkout path in every
|
||||
@@ -199,11 +194,9 @@ jobs:
|
||||
SHARD: ${{ matrix.shard }}
|
||||
run: |
|
||||
if [ "$SHARD" = ladder ]; then
|
||||
# The golden is the C++ kernel's: solidcore differs on one case (shell under mitred
|
||||
# rounds), so this shard stays on it until a solidcore golden is justified.
|
||||
set -- --ladder --gate --kernel smlib
|
||||
set -- --ladder --gate
|
||||
else
|
||||
set -- --area "$SHARD" --known-failing "tests/kernel-cases/$SHARD/KNOWN_FAILING.solidcore"
|
||||
set -- --area "$SHARD" --known-failing "tests/kernel-cases/$SHARD/KNOWN_FAILING"
|
||||
fi
|
||||
target/debug/xtask kernel-cases "$@" --cli target/release/solidcraft-cli --out "$RUNNER_TEMP/kernel-rig"
|
||||
- name: Keep the results
|
||||
|
||||
+1
-1
@@ -815,7 +815,7 @@ crates/engine/src/tests/gltf_import.rs); no third-party models are committed.
|
||||
|
||||
| Crate | Used for | Licence |
|
||||
|---|---|---|
|
||||
| solidcore (https://github.com/tangency-labs/solidcore, pinned by rev in Cargo.toml) | the Rust port of NVIDIA's SMLib: the second backend behind crates/smlib, which routes an operation to it once it matches the C++ kernel on the kernel-case rig (crates/smlib/src/route.rs) | Apache-2.0 (its NOTICE credits NVIDIA's USD BRep) |
|
||||
| solidcore (https://github.com/tangency-labs/solidcore, pinned by rev in Cargo.toml) | the Rust port of NVIDIA's SMLib: the B-rep kernel behind crates/smlib | Apache-2.0 (its NOTICE credits NVIDIA's USD BRep) |
|
||||
|
||||
Linked as a crate; no code from it is copied into SolidCraft. Its licence text and NOTICE go
|
||||
into THIRD-PARTY-LICENSES.txt when `main` regenerates it.
|
||||
|
||||
Generated
-2
@@ -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]]
|
||||
|
||||
+2
-2
@@ -31,8 +31,8 @@ solidcraft-space-mouse = { path = "crates/space-mouse" }
|
||||
solidcraft-mcp = { path = "crates/mcp" }
|
||||
solidcraft-derive = { path = "crates/derive" }
|
||||
solidcraft-3dx-client = { path = "crates/threedx-client" }
|
||||
# The Rust port of NVIDIA's SMLib (Apache-2.0), the kernel crates/smlib routes operations to once
|
||||
# they reach parity (crates/smlib/src/route.rs). Pinned; move the rev on its own commit. For a
|
||||
# The Rust port of NVIDIA's SMLib (Apache-2.0), the B-rep kernel behind crates/smlib. Pinned;
|
||||
# move the rev on its own commit. For a
|
||||
# local checkout, add `[patch."https://github.com/tangency-labs/solidcore"] solidcore = { path =
|
||||
# "../solidcore/crates/solidcore" }` to a `.cargo/config.toml` outside the repository.
|
||||
solidcore = { git = "https://github.com/tangency-labs/solidcore", rev = "2dbd88b6e9b858144609690eb2860e0beba27e54" }
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -9,9 +9,9 @@ use serde_json::{Value, json};
|
||||
use crate::oracle::{ABS_FRAC, Checks, REL_TOL, totals, v3};
|
||||
use crate::{read_json, recipe};
|
||||
|
||||
/// The kernel the cases run on: SMLib, the only one.
|
||||
/// The kernel the cases run on: solidcore, the only one.
|
||||
fn kernel() -> String {
|
||||
"smlib".into()
|
||||
"solidcore".into()
|
||||
}
|
||||
|
||||
/// How many cases a chain of `base` recipes may go through (a ladder case builds on the one
|
||||
@@ -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();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! `--profile` and `--profile-out FILE` for `run`, `eval` and `bench-open` (and
|
||||
//! `SOLIDCRAFT_PROFILE=1` for any command): record where the time goes, SMLib's calls to the
|
||||
//! `SOLIDCRAFT_PROFILE=1` for any command): record where the time goes, the kernel's calls to the
|
||||
//! commands, print the table to stderr when the command ends (stdout stays the command's JSON)
|
||||
//! and write the spans as a Chrome trace for chrome://tracing or Perfetto.
|
||||
|
||||
|
||||
@@ -386,44 +386,3 @@ fn a_design_saved_before_names_were_pinned_opens_pinned_and_unchanged() {
|
||||
}
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
/// A saved model is its kernel's: the two differ in tolerances and in what they refuse, so a design
|
||||
/// opened on the other kernel is evaluated on it, in both directions; on the kernel that saved it the
|
||||
/// model is taken as it is. A process each, since the kernel is chosen for the process.
|
||||
/// It reaches the C++ kernel on purpose: SOLIDCRAFT_KERNEL=smlib is the option under test.
|
||||
#[cfg(feature = "smlib")]
|
||||
#[test]
|
||||
fn a_model_saved_on_one_kernel_is_evaluated_again_on_the_other() {
|
||||
let cli = || {
|
||||
let mut c = std::process::Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"));
|
||||
c.env_remove("SOLIDCRAFT_NO_MODEL_CACHE");
|
||||
c
|
||||
};
|
||||
// Under the build's own temporary folder: the files are the command line's, not this process's.
|
||||
let temp = |name: &str| Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("model-cache-{}-{name}.solidcraft", std::process::id()));
|
||||
let script = temp("kernels-script").with_extension("json");
|
||||
let recipe = json!({"commands": [
|
||||
{"command": "solid.box", "params": {"length": 20, "width": 20, "height": 10, "body_name": "Plate"}},
|
||||
{"command": "solid.cylinder", "params": {"diameter": 8, "height": 10, "base": [10, 10, 10], "operation": "join", "targets": ["Plate"]}},
|
||||
]});
|
||||
std::fs::write(&script, recipe.to_string()).unwrap();
|
||||
// Whether opening found a model in the file it may take, whether it took it, and how many
|
||||
// features it evaluated.
|
||||
let open = |design: &Path, kernel: &str| {
|
||||
let out = cli().env("SOLIDCRAFT_KERNEL", kernel).arg("bench-open").arg(design).output().unwrap();
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
let lines: Vec<Value> = String::from_utf8_lossy(&out.stdout).lines().filter_map(|l| serde_json::from_str(l).ok()).collect();
|
||||
let step = |name: &str| lines.iter().find(|l| l["step"] == name).cloned().unwrap();
|
||||
let (parse, evaluated) = (step("parse"), step("evaluate"));
|
||||
(parse["saved_model"].as_bool().unwrap(), evaluated["adopted"].as_bool().unwrap(), evaluated["recomputed"].as_u64().unwrap())
|
||||
};
|
||||
for (saved, other) in [("solidcore", "smlib"), ("smlib", "solidcore")] {
|
||||
let design = temp(&format!("kernels-{saved}"));
|
||||
let out = cli().env("SOLIDCRAFT_KERNEL", saved).arg("run").arg(&script).arg("--save").arg(&design).output().unwrap();
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
assert_eq!(open(&design, saved), (true, true, 0), "saved on {saved}, opened on it");
|
||||
assert_eq!(open(&design, other), (false, false, 2), "saved on {saved}, opened on {other}");
|
||||
std::fs::remove_file(&design).unwrap();
|
||||
}
|
||||
std::fs::remove_file(&script).unwrap();
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"cmd.diagnostics_kernel": "Geometry Kernel",
|
||||
"prefs.geometry_kernel": "Geometry kernel",
|
||||
"prefs.geometry_kernel_environment": "The SOLIDCRAFT_KERNEL environment variable is set; it wins over this setting.",
|
||||
"prefs.geometry_kernel_note": "solidcore, the default, answers every operation. Choose SMLib for the C++ kernel. A solidcore failure is reported, never retried on SMLib.",
|
||||
"prefs.geometry_kernel_unavailable": "SMLib is not available in this build. Operations run on solidcore."
|
||||
},
|
||||
"ar": {
|
||||
"cmd.diagnostics_kernel": "نواة الهندسة",
|
||||
"prefs.geometry_kernel": "نواة الهندسة",
|
||||
"prefs.geometry_kernel_environment": "متغير البيئة SOLIDCRAFT_KERNEL مضبوط، وله الأولوية على هذا الإعداد.",
|
||||
"prefs.geometry_kernel_note": "يجيب solidcore، وهو الافتراضي، عن كل العمليات. اختر SMLib لاستخدام نواة C++. ويُبلَّغ عن إخفاق solidcore ولا تُعاد المحاولة أبدًا على SMLib.",
|
||||
"prefs.geometry_kernel_unavailable": "SMLib غير متاح في هذا الإصدار. تعمل العمليات على solidcore."
|
||||
},
|
||||
"de": {
|
||||
"cmd.diagnostics_kernel": "Geometriekernel",
|
||||
"prefs.geometry_kernel": "Geometriekernel",
|
||||
"prefs.geometry_kernel_environment": "Die Umgebungsvariable SOLIDCRAFT_KERNEL ist gesetzt; sie hat Vorrang vor dieser Einstellung.",
|
||||
"prefs.geometry_kernel_note": "solidcore, die Standardeinstellung, führt alle Vorgänge aus. Wählen Sie SMLib für den C++-Kernel. Ein Fehler in solidcore wird gemeldet, nie mit SMLib wiederholt.",
|
||||
"prefs.geometry_kernel_unavailable": "SMLib ist in diesem Build nicht verfügbar. Vorgänge laufen auf solidcore."
|
||||
},
|
||||
"fr": {
|
||||
"cmd.diagnostics_kernel": "Noyau géométrique",
|
||||
"prefs.geometry_kernel": "Noyau géométrique",
|
||||
"prefs.geometry_kernel_environment": "La variable d’environnement SOLIDCRAFT_KERNEL est définie ; elle l’emporte sur ce réglage.",
|
||||
"prefs.geometry_kernel_note": "solidcore, le réglage par défaut, traite toutes les opérations. Choisissez SMLib pour le noyau C++. Un échec de solidcore est signalé, jamais réessayé avec SMLib.",
|
||||
"prefs.geometry_kernel_unavailable": "SMLib n’est pas disponible dans cette version. Les opérations s’exécutent sur solidcore."
|
||||
},
|
||||
"he": {
|
||||
"cmd.diagnostics_kernel": "ליבת גאומטריה",
|
||||
"prefs.geometry_kernel": "ליבת גאומטריה",
|
||||
"prefs.geometry_kernel_environment": "משתנה הסביבה SOLIDCRAFT_KERNEL מוגדר, והוא גובר על הגדרה זו.",
|
||||
"prefs.geometry_kernel_note": "solidcore, ברירת המחדל, מבצע את כל הפעולות. בחרו ב-SMLib עבור ליבת ה-C++. כשל של solidcore מדווח, ולעולם אינו מנוסה שוב ב-SMLib.",
|
||||
"prefs.geometry_kernel_unavailable": "SMLib אינו זמין בגרסה זו. הפעולות מתבצעות ב-solidcore."
|
||||
},
|
||||
"ja": {
|
||||
"cmd.diagnostics_kernel": "ジオメトリ カーネル",
|
||||
"prefs.geometry_kernel": "ジオメトリ カーネル",
|
||||
"prefs.geometry_kernel_environment": "環境変数 SOLIDCRAFT_KERNEL が設定されているため、この設定より優先されます。",
|
||||
"prefs.geometry_kernel_note": "既定の solidcore がすべての処理を行います。C++ カーネルを使うには SMLib を選択してください。solidcore の失敗は報告され、SMLib で再試行されることはありません。",
|
||||
"prefs.geometry_kernel_unavailable": "このビルドでは SMLib を利用できません。処理は solidcore で実行されます。"
|
||||
},
|
||||
"vi": {
|
||||
"cmd.diagnostics_kernel": "Nhân hình học",
|
||||
"prefs.geometry_kernel": "Nhân hình học",
|
||||
"prefs.geometry_kernel_environment": "Biến môi trường SOLIDCRAFT_KERNEL đã được đặt; biến này được ưu tiên hơn thiết lập này.",
|
||||
"prefs.geometry_kernel_note": "solidcore, mặc định, xử lý mọi thao tác. Chọn SMLib để dùng nhân C++. Lỗi của solidcore được báo cáo, không bao giờ thử lại trên SMLib.",
|
||||
"prefs.geometry_kernel_unavailable": "Bản dựng này không có SMLib. Các thao tác chạy trên solidcore."
|
||||
},
|
||||
"zh": {
|
||||
"cmd.diagnostics_kernel": "几何内核",
|
||||
"prefs.geometry_kernel": "几何内核",
|
||||
"prefs.geometry_kernel_environment": "已设置环境变量 SOLIDCRAFT_KERNEL,它优先于此设置。",
|
||||
"prefs.geometry_kernel_note": "默认的 solidcore 处理所有操作。如需使用 C++ 内核,请选择 SMLib。solidcore 失败时会报告,绝不会改用 SMLib 重试。",
|
||||
"prefs.geometry_kernel_unavailable": "此版本不提供 SMLib。操作在 solidcore 上运行。"
|
||||
}
|
||||
}
|
||||
@@ -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 = []
|
||||
|
||||
@@ -60,17 +60,6 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
inside it; total counts its outermost run only. Recording also starts with SOLIDCRAFT_PROFILE=1, the CLI's \
|
||||
--profile and the app's preference; off, it costs nothing measurable. Read-only",
|
||||
),
|
||||
CommandSpec::new("diagnostics.kernel", "Geometry Kernel", crate::checked!("diagnostics.kernel", kernel))
|
||||
.icon("bug_report")
|
||||
.noundo()
|
||||
.contract(&KERNEL_CONTRACT)
|
||||
.output(output_schema_for::<crate::schema::diagnostics::KernelReply>)
|
||||
.params(
|
||||
"choice?: solidcore | smlib (sets which kernel answers every operation: solidcore, the default = the Rust port; \
|
||||
smlib = the C++ SMLib kernel, refused as not available on a build without it, such as the web app; bodies already \
|
||||
computed keep their results). SOLIDCRAFT_KERNEL, when set, wins over it. Returns {choice, environment}: choice is \
|
||||
the kernel in force, environment whether SOLIDCRAFT_KERNEL chose it",
|
||||
),
|
||||
CommandSpec::new("diagnostics.validation", "Validation", crate::checked!("diagnostics.validation", validation))
|
||||
.icon("bug_report")
|
||||
.noundo()
|
||||
@@ -96,34 +85,6 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
),
|
||||
];
|
||||
|
||||
fn kernel(_: &mut Session, p: &Value) -> Result<Value> {
|
||||
use solidcraft_kernel::kernel_route as route;
|
||||
if let Some(v) = p.get("choice").filter(|v| !v.is_null()) {
|
||||
route::set_choice(kernel_asked(v, solidcraft_kernel::smlib_built())?);
|
||||
}
|
||||
use crate::schema::diagnostics::{KernelChoice, KernelReply};
|
||||
let choice = match route::choice() {
|
||||
route::Backend::Solidcore => KernelChoice::Solidcore,
|
||||
route::Backend::Smlib => KernelChoice::Smlib,
|
||||
};
|
||||
serde_json::to_value(KernelReply { choice, environment: route::overridden() }).map_err(|e| bad("diagnostics.kernel", e.to_string()))
|
||||
}
|
||||
|
||||
/// The kernel `choice` asks for, on a build that has (`built`) the C++ kernel or doesn't. A choice of
|
||||
/// the wrong type is refused like an unknown one, not read as no choice; `smlib` on a build without
|
||||
/// it is refused too, never answered by the other kernel.
|
||||
pub(crate) fn kernel_asked(choice: &Value, built: bool) -> Result<solidcraft_kernel::kernel_route::Backend> {
|
||||
use solidcraft_kernel::kernel_route::Backend;
|
||||
let asked = choice.as_str().and_then(Backend::from_id).ok_or_else(|| bad("diagnostics.kernel", "`choice` is solidcore or smlib"))?;
|
||||
if asked == Backend::Smlib && !built {
|
||||
return Err(EngineError::Disabled(
|
||||
"diagnostics.kernel".into(),
|
||||
"this build doesn't include the C++ SMLib kernel; build with --features smlib".into(),
|
||||
));
|
||||
}
|
||||
Ok(asked)
|
||||
}
|
||||
|
||||
fn dir(cmd: &str) -> Result<std::path::PathBuf> {
|
||||
crash::default_dir().ok_or_else(|| bad(cmd, "no crash folder on this system"))
|
||||
}
|
||||
@@ -182,7 +143,6 @@ fn crash_test(_: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
const PROFILE_CONTRACT: crate::schema::TypedContract<crate::schema::diagnostics::ProfileParams> = crate::schema::TypedContract::new();
|
||||
|
||||
const KERNEL_CONTRACT: crate::schema::TypedContract<crate::schema::diagnostics::KernelParams> = crate::schema::TypedContract::new();
|
||||
const VALIDATION_CONTRACT: crate::schema::TypedContract<crate::schema::diagnostics::ValidationParams> = crate::schema::TypedContract::new();
|
||||
|
||||
fn validation(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -197,8 +197,7 @@ pub struct ProfileParams {
|
||||
/// One category's share of the recorded self time.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct ProfileCategory {
|
||||
/// `smlib` (SMLib's own time, measured by its C API), `kernel`, `feature`, `doc`, `command`,
|
||||
/// `import`, `io`.
|
||||
/// `solidcore` (the kernel's own calls), `kernel`, `feature`, `doc`, `command`, `import`, `io`.
|
||||
pub category: String,
|
||||
pub self_ms: f64,
|
||||
/// Of all recorded self time, 0–1.
|
||||
@@ -330,44 +329,6 @@ impl From<solidcraft_kernel::Validation> for ValidationMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read, or set, which kernel answers every operation.
|
||||
#[derive(Debug, Default, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct KernelParams {
|
||||
/// The kernel to use from now on. Default: leave it as it is. `smlib` is refused on a build
|
||||
/// without the C++ kernel; `SOLIDCRAFT_KERNEL`, when set, wins over it.
|
||||
#[serde(default, deserialize_with = "kernel_choice")]
|
||||
pub choice: Option<KernelChoice>,
|
||||
}
|
||||
|
||||
/// A geometry kernel.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum KernelChoice {
|
||||
/// solidcore, the Rust port of SMLib (the default).
|
||||
Solidcore,
|
||||
/// The C++ SMLib kernel.
|
||||
Smlib,
|
||||
}
|
||||
|
||||
fn kernel_choice<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<KernelChoice>, D::Error> {
|
||||
match Value::deserialize(d)? {
|
||||
Value::Null => Ok(None),
|
||||
Value::String(s) if s == "solidcore" => Ok(Some(KernelChoice::Solidcore)),
|
||||
Value::String(s) if s == "smlib" => Ok(Some(KernelChoice::Smlib)),
|
||||
_ => Err(serde::de::Error::custom(super::check::whole("`choice` is solidcore or smlib"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The reply of `diagnostics.kernel`.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct KernelReply {
|
||||
/// The kernel in force now.
|
||||
pub choice: KernelChoice,
|
||||
/// Whether `SOLIDCRAFT_KERNEL` chose it.
|
||||
pub environment: bool,
|
||||
}
|
||||
|
||||
/// Read, or set, when SMLib's full check runs on the features computed from now on.
|
||||
#[derive(Debug, Default, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
|
||||
@@ -2,7 +2,7 @@ use serde_json::{Value, json};
|
||||
|
||||
use super::{
|
||||
CrashReport, CrashReportList, CrashReportParams, CrashReportsClearParams, CrashReportsCleared, CrashReportsParams, CrashTestMode,
|
||||
CrashTestParams, KernelParams, ProfileParams, ValidationParams,
|
||||
CrashTestParams, ProfileParams, ValidationParams,
|
||||
};
|
||||
use crate::Session;
|
||||
use crate::schema::tests::{assert_keys, assert_strict_keeps_todays_errors, conforms};
|
||||
@@ -16,7 +16,6 @@ fn schemas_and_the_key_check_agree() {
|
||||
assert_keys(input_schema_for::<CrashTestParams>(), &["mode"]);
|
||||
assert_keys(input_schema_for::<ProfileParams>(), &["enabled", "reset", "top", "trace"]);
|
||||
assert_keys(input_schema_for::<ValidationParams>(), &["mode"]);
|
||||
assert_keys(input_schema_for::<KernelParams>(), &["choice"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -30,8 +29,6 @@ fn strict_keeps_todays_errors() {
|
||||
("diagnostics.profile", json!({"trace": 5})),
|
||||
("diagnostics.validation", json!({"mode": "each step"})),
|
||||
("diagnostics.validation", json!({"mode": true})),
|
||||
("diagnostics.kernel", json!({"choice": "cpp"})),
|
||||
("diagnostics.kernel", json!({"choice": 1})),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -154,94 +151,6 @@ fn diagnostics_profile_reports_commands_features_and_the_kernel() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Tests that set the process-wide kernel choice run one at a time, and put `solidcore` back
|
||||
/// however they end (a failed assertion included).
|
||||
struct KernelChoice(#[allow(dead_code)] std::sync::MutexGuard<'static, ()>);
|
||||
|
||||
static KERNEL_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
impl KernelChoice {
|
||||
fn take() -> Self {
|
||||
KernelChoice(KERNEL_TESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for KernelChoice {
|
||||
fn drop(&mut self) {
|
||||
solidcraft_kernel::kernel_route::set_choice(solidcraft_kernel::kernel_route::Backend::Solidcore);
|
||||
}
|
||||
}
|
||||
|
||||
/// diagnostics.kernel says the kernel in force and whether the environment chose it, and its
|
||||
/// `choice` sets it (unless SOLIDCRAFT_KERNEL overrides it).
|
||||
///
|
||||
/// The choice is process-wide and the other tests in this binary build bodies meanwhile, so only
|
||||
/// `solidcore` is set through the command; what `smlib` asks for is read from `kernel_asked`.
|
||||
#[test]
|
||||
fn diagnostics_kernel_reports_and_sets_the_choice() {
|
||||
use solidcraft_kernel::kernel_route as route;
|
||||
let _choice = KernelChoice::take();
|
||||
let mut s = Session::default();
|
||||
let r = s.execute("diagnostics.kernel", &json!({})).unwrap();
|
||||
let environment = route::overridden();
|
||||
assert_eq!(r, json!({ "choice": route::choice().id(), "environment": environment }));
|
||||
let r = s.execute("diagnostics.kernel", &json!({ "choice": "solidcore" })).unwrap();
|
||||
assert!(environment || r["choice"] == json!("solidcore"), "{r}");
|
||||
assert_eq!(crate::cmd::diagnostics::kernel_asked(&json!("solidcore"), false).unwrap(), route::Backend::Solidcore);
|
||||
assert_eq!(crate::cmd::diagnostics::kernel_asked(&json!("solidcore"), true).unwrap(), route::Backend::Solidcore);
|
||||
assert_eq!(crate::cmd::diagnostics::kernel_asked(&json!("smlib"), true).unwrap(), route::Backend::Smlib);
|
||||
}
|
||||
|
||||
/// `smlib` on a build without the C++ kernel is refused as not available, whichever kernel is in
|
||||
/// force, and leaves the choice alone: never an answer from the other kernel. Built with it, the
|
||||
/// command would move the other tests' bodies, so `kernel_asked` alone is asked.
|
||||
#[test]
|
||||
fn diagnostics_kernel_refuses_smlib_on_a_build_without_it() {
|
||||
let _choice = KernelChoice::take();
|
||||
let refused = crate::cmd::diagnostics::kernel_asked(&json!("smlib"), false).unwrap_err();
|
||||
assert!(matches!(&refused, crate::EngineError::Disabled(cmd, _) if cmd == "diagnostics.kernel"), "{refused}");
|
||||
if !solidcraft_kernel::smlib_built() {
|
||||
let mut s = Session::default();
|
||||
let before = s.execute("diagnostics.kernel", &json!({})).unwrap();
|
||||
assert!(matches!(s.execute("diagnostics.kernel", &json!({"choice": "smlib"})), Err(crate::EngineError::Disabled(..))));
|
||||
assert_eq!(s.execute("diagnostics.kernel", &json!({})).unwrap(), before);
|
||||
}
|
||||
}
|
||||
|
||||
/// An unknown or mistyped choice is an error that changes nothing; no hostile value panics.
|
||||
#[test]
|
||||
fn diagnostics_kernel_refuses_unknown_choices() {
|
||||
let _choice = KernelChoice::take();
|
||||
let mut s = Session::default();
|
||||
let before = s.execute("diagnostics.kernel", &json!({})).unwrap()["choice"].clone();
|
||||
let huge = "solidcore".repeat(100_000);
|
||||
for bad in [
|
||||
json!("truck"),
|
||||
json!("SMLIB"),
|
||||
json!(""),
|
||||
json!(huge),
|
||||
json!("routed"),
|
||||
json!("routed\0"),
|
||||
json!(5),
|
||||
json!(true),
|
||||
json!(["smlib"]),
|
||||
json!({"choice": "smlib"}),
|
||||
] {
|
||||
match s.execute("diagnostics.kernel", &json!({ "choice": bad })) {
|
||||
Err(crate::EngineError::BadParams { .. }) => {}
|
||||
other => panic!("`{}` was not refused: {:?}", bad.as_str().map_or(0, str::len), other.map_err(|e| e.to_string())),
|
||||
}
|
||||
}
|
||||
for v in crate::tests::hostile_values() {
|
||||
for p in [json!({ "choice": v.clone() }), v] {
|
||||
if let Err(crate::EngineError::Internal(_, msg)) = s.execute("diagnostics.kernel", &p) {
|
||||
panic!("diagnostics.kernel {p}: {msg}");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(s.execute("diagnostics.kernel", &json!({})).unwrap()["choice"], before);
|
||||
}
|
||||
|
||||
/// Features are checked once each by default; the command reads and sets the mode, and the mode
|
||||
/// is the user's: it stays when another design is started and when everything is computed again.
|
||||
#[test]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -1548,13 +1548,11 @@ mod tests {
|
||||
manifest.as_object_mut().unwrap().remove("evaluation");
|
||||
entries.insert("cache/model.json".into(), serde_json::to_vec(&manifest).unwrap());
|
||||
assert!(read_design_geometry(&package(&entries)).is_none());
|
||||
// The kernel that computed it is recorded, and another kernel's model is evaluated again:
|
||||
// the same source gives different bodies on the two (a cache without one is a miss too).
|
||||
use solidcraft_kernel::kernel_route::{Backend, choice};
|
||||
assert_eq!(written(&original)["kernel"], json!(choice().id()));
|
||||
let other = if choice() == Backend::Solidcore { Backend::Smlib } else { Backend::Solidcore };
|
||||
let bytes = with(&[("kernel", other.id())]);
|
||||
assert!(read_design_geometry(&bytes).is_none(), "{}'s cache", other.id());
|
||||
// The kernel that computed it is recorded, and a model the C++ kernel computed (an earlier
|
||||
// build's) is evaluated again (a cache without one is a miss too).
|
||||
assert_eq!(written(&original)["kernel"], json!(native_cache::KERNEL));
|
||||
let bytes = with(&[("kernel", "smlib")]);
|
||||
assert!(read_design_geometry(&bytes).is_none(), "the C++ kernel's cache");
|
||||
assert_eq!(crate::read_design(&bytes).unwrap().to_json(), doc.to_json());
|
||||
let mut entries = original.clone();
|
||||
let mut manifest = written(&entries);
|
||||
|
||||
@@ -29,11 +29,10 @@ fn evaluation() -> String {
|
||||
format!("{}/{}-{}", env!("SOLIDCRAFT_EVALUATION_SOURCE"), std::env::consts::ARCH, std::env::consts::OS)
|
||||
}
|
||||
|
||||
/// The geometry kernel that answers in this process: another kernel's cache is not its result (the
|
||||
/// two differ in tolerances and in what they refuse), so a design opened under it evaluates again.
|
||||
fn kernel() -> &'static str {
|
||||
solidcraft_kernel::kernel_route::choice().id()
|
||||
}
|
||||
/// The geometry kernel that computed a cache. A cache another kernel wrote (the C++ SMLib, which
|
||||
/// earlier builds could run on) is not this build's result: the kernels differ in tolerances and in
|
||||
/// what they refuse, so a design opened from it evaluates again.
|
||||
pub(super) const KERNEL: &str = "solidcore";
|
||||
|
||||
/// A 4 x 4 matrix as its 16 numbers, in storage order.
|
||||
fn matrix(t: &[[f64; 4]; 4]) -> Vec<f64> {
|
||||
@@ -302,7 +301,7 @@ impl Encoded {
|
||||
document: digest(document),
|
||||
evaluation: evaluation(),
|
||||
format: FORMAT,
|
||||
kernel: kernel(),
|
||||
kernel: KERNEL,
|
||||
model: records,
|
||||
payload: digest(&data),
|
||||
producer: PRODUCER,
|
||||
@@ -352,7 +351,7 @@ fn current(manifest: &Value, document: &[u8]) -> bool {
|
||||
is("format", FORMAT)
|
||||
&& is("producer", PRODUCER)
|
||||
&& is("evaluation", &evaluation())
|
||||
&& is("kernel", kernel())
|
||||
&& is("kernel", KERNEL)
|
||||
&& is("document", &digest(document))
|
||||
&& source_trusted(env!("SOLIDCRAFT_EVALUATION_SOURCE"), cfg!(any(test, feature = "trust-unknown-source")))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -192,9 +192,9 @@ mod tests {
|
||||
let k = row("kernel", "boolean").unwrap_or_else(|| panic!("{rep:?}"));
|
||||
assert!(k.count >= 1 && row("kernel", "box_solid").is_some(), "{rep:?}");
|
||||
if r.is_ok() {
|
||||
// The geometry kernel's calls, on whichever answers, are children of the boolean: their
|
||||
// time is not its self time.
|
||||
let calls: std::time::Duration = rep.rows.iter().filter(|r| matches!(r.category, "smlib" | "solidcore")).map(|r| r.total).sum();
|
||||
// The geometry kernel's calls are children of the boolean: their time is not its self
|
||||
// time.
|
||||
let calls: std::time::Duration = rep.rows.iter().filter(|r| r.category == "solidcore").map(|r| r.total).sum();
|
||||
assert!(calls > std::time::Duration::ZERO && k.self_time < k.total, "{rep:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -373,7 +373,6 @@ fn closed_display_mesh(name: &str, b: &Body, tol: f64) -> Mesh {
|
||||
/// A sphere from SMLib cut through its centre: the half sphere is bounded by its rim and its
|
||||
/// seam from the pole up to the rim, used both ways. The seam's two uses share their nodes, so
|
||||
/// the mesh has no crack along it and nothing degenerate at the pole.
|
||||
#[cfg(feature = "smlib")]
|
||||
#[test]
|
||||
fn a_half_sphere_from_smlib_is_closed_along_its_seam() {
|
||||
use solidcraft_smlib::{BooleanOp, Brep as S};
|
||||
@@ -406,7 +405,6 @@ fn display_plans_stay_within_the_chord_tolerance() {
|
||||
}
|
||||
|
||||
/// SMLib's torus is one face bounded by its two seams: a grid over the whole parameter box.
|
||||
#[cfg(feature = "smlib")]
|
||||
#[test]
|
||||
fn an_smlib_torus_stays_within_the_chord_tolerance() {
|
||||
let Ok(ring) = solidcraft_smlib::Brep::make_torus([0.0; 3], 22.0, 4.0) else { panic!("torus") };
|
||||
@@ -424,7 +422,6 @@ fn an_smlib_torus_stays_within_the_chord_tolerance() {
|
||||
|
||||
/// A groove of trapezoid `section` (radius, axial offset) in the plane through the z axis along
|
||||
/// y, carried `turns` turns of `pitch` about z from `base`, as an SMLib solid.
|
||||
#[cfg(feature = "smlib")]
|
||||
fn smlib_groove(section: [(f64, f64); 4], base: f64, pitch: f64, turns: f64) -> solidcraft_smlib::Brep {
|
||||
use solidcraft_geom::{Loop2, Vec2};
|
||||
smlib_screw(Loop2::polygon(§ion.map(|(r, z)| Vec2::new(r, z))), base, pitch, turns)
|
||||
@@ -432,7 +429,6 @@ fn smlib_groove(section: [(f64, f64); 4], base: f64, pitch: f64, turns: f64) ->
|
||||
|
||||
/// The `section` (radius, axial offset) in the plane through the z axis along y, carried
|
||||
/// `turns` turns of `pitch` about z from `base`, as an SMLib solid.
|
||||
#[cfg(feature = "smlib")]
|
||||
fn smlib_screw(section: solidcraft_geom::Loop2, base: f64, pitch: f64, turns: f64) -> solidcraft_smlib::Brep {
|
||||
use solidcraft_geom::{Plane, Region2};
|
||||
let origin = Vec3::new(0.0, 0.0, base);
|
||||
@@ -444,7 +440,6 @@ fn smlib_screw(section: solidcraft_geom::Loop2, base: f64, pitch: f64, turns: f6
|
||||
ok(crate::native::smlib_body(&groove))
|
||||
}
|
||||
|
||||
#[cfg(feature = "smlib")]
|
||||
fn smlib_cut(a: &solidcraft_smlib::Brep, b: &solidcraft_smlib::Brep) -> solidcraft_smlib::Brep {
|
||||
match a.boolean(b, solidcraft_smlib::BooleanOp::Subtract) {
|
||||
Ok(Some(r)) => r,
|
||||
@@ -455,7 +450,6 @@ fn smlib_cut(a: &solidcraft_smlib::Brep, b: &solidcraft_smlib::Brep) -> solidcra
|
||||
/// The ISO basic profile's groove for pitch `p` and major diameter `d`: internal, from 0.1 p
|
||||
/// inside the hole wall of radius `wall` out to the major radius; external, from the root out
|
||||
/// past the shaft's surface (radius `wall`).
|
||||
#[cfg(feature = "smlib")]
|
||||
fn iso_groove(p: f64, d: f64, wall: f64, internal: bool) -> [(f64, f64); 4] {
|
||||
let h = p * 3f64.sqrt() / 2.0;
|
||||
let slope = (PI / 6.0).tan();
|
||||
@@ -478,7 +472,6 @@ fn iso_groove(p: f64, d: f64, wall: f64, internal: bool) -> [(f64, f64); 4] {
|
||||
/// parabolic segment) that flat triangles bridge rather than follow: a face no wider than its
|
||||
/// chords sag (the thread's crest band where it runs out through the block's end, 0.06 mm sag
|
||||
/// at 0.1) takes up to that much more or less area however it is triangulated inside.
|
||||
#[cfg(feature = "smlib")]
|
||||
fn assert_faces_lie_flat(name: &str, plan: &solidcraft_geom::tess::TessPlan, tol: f64) {
|
||||
for fp in &plan.faces {
|
||||
let Some(s) = fp.surface.and_then(|i| plan.surfaces.get(i as usize)) else { continue };
|
||||
@@ -530,7 +523,6 @@ fn assert_faces_lie_flat(name: &str, plan: &solidcraft_geom::tess::TessPlan, tol
|
||||
/// #1995: an M8x1 thread through a 7 mm hole in a 20 mm block, both ends open. Truck meshed the
|
||||
/// groove's winding walls with triangles reaching past both ends of the block; the body's mesh
|
||||
/// now comes from the display plan, inside the block and with the analytic volume.
|
||||
#[cfg(feature = "smlib")]
|
||||
#[test]
|
||||
fn a_modelled_thread_in_a_bore_meshes_inside_its_block() {
|
||||
use solidcraft_smlib::Brep as S;
|
||||
@@ -562,7 +554,6 @@ fn a_modelled_thread_in_a_bore_meshes_inside_its_block() {
|
||||
/// a triangle's side, with no four corners to blend. The butterfly of a round face made it a
|
||||
/// grid of ~200 000 points whose triangles zigzagged along the strip (six times the band's area,
|
||||
/// and off the surface by more than the tolerance); it is its boundary nodes alone now.
|
||||
#[cfg(feature = "smlib")]
|
||||
#[test]
|
||||
fn a_thread_root_band_running_out_through_an_end_face_lies_on_its_surface() {
|
||||
let Ok(shaft) = solidcraft_smlib::Brep::make_cylinder([0.0; 3], 5.0, 30.0) else { panic!("shaft") };
|
||||
@@ -587,7 +578,6 @@ fn a_thread_root_band_running_out_through_an_end_face_lies_on_its_surface() {
|
||||
/// halves go to the display plan for their mesh; at the measuring tolerance a half's wall wants
|
||||
/// more cells than a grid may have, and its cut grid gave up for an under-refined Delaunay (the
|
||||
/// halves measured 1.5% short of the whole). The cut grid now thins its lines to fit.
|
||||
#[cfg(feature = "smlib")]
|
||||
#[test]
|
||||
fn the_halves_of_a_split_smlib_coil_measure_as_the_whole() {
|
||||
use solidcraft_geom::{Loop2, Vec2};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "solidcraft-profile"
|
||||
description = "SolidCraft profiler: nested, per-thread timed spans of SMLib calls, kernel operations, feature evaluation, commands and imports, as a table and a Chrome trace"
|
||||
description = "SolidCraft profiler: nested, per-thread timed spans of kernel calls, kernel operations, feature evaluation, commands and imports, as a table and a Chrome trace"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -36,7 +36,7 @@ pub const OTHER: &str = "other";
|
||||
/// One span name's aggregate.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Row {
|
||||
/// What the span is: `smlib`, `kernel`, `feature`, `command`, `import`, …
|
||||
/// What the span is: `solidcore`, `kernel`, `feature`, `command`, `import`, …
|
||||
pub category: &'static str,
|
||||
pub name: &'static str,
|
||||
/// Times the span ran.
|
||||
|
||||
@@ -41,8 +41,8 @@ fn nested_spans_have_self_and_total_time() {
|
||||
let _inner = span("test", "inner");
|
||||
spin(4);
|
||||
}
|
||||
// Timed by someone else (SMLib's C API): a child of `outer`.
|
||||
record("smlib", "call", Duration::from_millis(2));
|
||||
// Timed by someone else: a child of `outer`.
|
||||
record("external", "call", Duration::from_millis(2));
|
||||
}
|
||||
set_enabled(false);
|
||||
let r = report();
|
||||
|
||||
+3
-27
@@ -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"
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
a18bbe523b9c7819f1a9da8c40a54c15f9936125
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()));
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,83 +281,6 @@ 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)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
@@ -368,19 +289,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 +345,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 +373,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 +510,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 +532,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,65 +666,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
|
||||
/// cusp), chamfered d at the tip. No ball lies d back along both faces there; the chamfer is the
|
||||
@@ -928,23 +746,9 @@ fn edges_871() -> Vec<[f64; 3]> {
|
||||
|
||||
/// #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 +760,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 +767,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 +818,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);
|
||||
}
|
||||
}
|
||||
@@ -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,66 +186,6 @@ 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)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
@@ -256,13 +194,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,306 +321,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
|
||||
/// equal circles a little apart cross; they do not coincide over a tiny edge. The stack's volume
|
||||
@@ -726,3 +357,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:?}");
|
||||
}
|
||||
}
|
||||
@@ -2492,12 +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
|
||||
/// trims or analytic conversion made; it heals unless `SOURCE` says SMLib wrote the arrays.
|
||||
|
||||
@@ -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,47 +90,6 @@ 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)?) => {
|
||||
mod $m {
|
||||
@@ -149,18 +107,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 +164,13 @@ 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 +185,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,323 +282,3 @@ fn shells_listed_inner_first_import_outer_first() {
|
||||
let v = volume(&back, 1e-9).unwrap();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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,103 +108,6 @@ 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)?) => {
|
||||
mod $m {
|
||||
@@ -240,50 +142,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 +153,17 @@ 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 +172,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 +212,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 +256,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 +284,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 +291,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 +306,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,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,46 +199,6 @@ 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)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
@@ -249,19 +207,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,25 +343,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]
|
||||
fn the_body_stays_as_it_was() {
|
||||
@@ -428,30 +354,3 @@ fn the_body_stays_as_it_was() {
|
||||
let _ = merge_faces(&block, &[], TOL).unwrap();
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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,79 +335,6 @@ fn solidcore_case(name: &'static str) -> Got {
|
||||
})
|
||||
}
|
||||
|
||||
/// The case on the C++ kernel, called directly (whatever the choice).
|
||||
#[cfg(feature = "native")]
|
||||
fn cpp_case(name: &str) -> Got {
|
||||
use crate::ops::pick_error;
|
||||
use crate::sweeps::DraftCorner;
|
||||
use crate::{Brep, error, ffi};
|
||||
crate::init().unwrap();
|
||||
let line = |a: [f64; 3], b: [f64; 3]| crate::sweeps::Curve::Line { start: a, end: b };
|
||||
let make = |s: Shape| -> Brep {
|
||||
match s {
|
||||
Shape::Box(o, z) => ffi::make_box(&o, &z).map(Brep::made).unwrap(),
|
||||
Shape::Cylinder(o, r, h) => ffi::make_cylinder(&o, r, h).map(Brep::made).unwrap(),
|
||||
Shape::Sphere(o, r) => ffi::make_sphere(&o, r).map(Brep::made).unwrap(),
|
||||
Shape::Frustum(o, a, b, h) => ffi::make_cone(&o, a, b, h).map(Brep::made).unwrap(),
|
||||
Shape::LineSheet(a, b, dir, dist) => Brep::extrude(&[line(a, b)], dir, dist, 0.0, DraftCorner::default(), false).unwrap(),
|
||||
Shape::ArcSheet(o, r, degrees, h) => {
|
||||
let arc = crate::sweeps::Curve::Arc {
|
||||
center: o,
|
||||
axis: [0.0, 0.0, 1.0],
|
||||
reference: [1.0, 0.0, 0.0],
|
||||
radius: r,
|
||||
start: 0.0,
|
||||
end: degrees.to_radians(),
|
||||
};
|
||||
Brep::extrude(&[arc], [0.0, 0.0, 1.0], h, 0.0, DraftCorner::default(), false).unwrap()
|
||||
}
|
||||
Shape::Notched => {
|
||||
let block = ffi::make_box(&[0.0; 3], &[60.0, 40.0, 10.0]).unwrap();
|
||||
let pin = ffi::make_cylinder(&[22.5, 7.0, -5.0], 3.0, 20.0).unwrap();
|
||||
let notch = ffi::make_box(&[25.0, -5.0, -5.0], &[10.0, 10.0, 20.0]).unwrap();
|
||||
let a = ffi::ops::boolean(&block, &pin, ffi::ops::BOOLEAN_SUBTRACT).unwrap();
|
||||
Brep::made(ffi::ops::boolean(&a, ¬ch, ffi::ops::BOOLEAN_SUBTRACT).unwrap())
|
||||
}
|
||||
}
|
||||
};
|
||||
let c = case(name);
|
||||
let tol = tolerance(name);
|
||||
let r = match &c.op {
|
||||
Op::Shell(s, points, t) => ffi::ops::pick_op(ffi::ops::Pick::Shell, make(*s).cpp().unwrap(), points, tol, *t).map_err(pick_error),
|
||||
Op::OffsetFaces(s, points, d) => ffi::ops::pick_op(ffi::ops::Pick::OffsetFaces, make(*s).cpp().unwrap(), points, tol, *d).map_err(pick_error),
|
||||
Op::Thicken(s, t, sym) => ffi::ops::thicken(make(*s).cpp().unwrap(), *t, *sym).map_err(error),
|
||||
Op::OffsetSheet(s, d) => ffi::sheets::offset_sheet(make(*s).cpp().unwrap(), *d).map_err(error),
|
||||
Op::Draft(s, points, o, n, pull, angle) => ffi::ops::draft(make(*s).cpp().unwrap(), points, tol, o, n, pull, *angle).map_err(pick_error),
|
||||
};
|
||||
let sheet = is_sheet(&c.expect);
|
||||
let got = r.map(|h| {
|
||||
let size = if sheet { ffi::measure::area(&h, 1e-8) } else { ffi::volume(&h, 1e-8) }.unwrap();
|
||||
let n = ffi::counts(&h).unwrap().map(|x| usize::try_from(x).unwrap());
|
||||
let bx = ffi::measure::bounds(&h).unwrap();
|
||||
let b = Brep::made(h);
|
||||
let (rec, _) = native(&b.cpp_arrays().unwrap()).unwrap();
|
||||
rec.validate().unwrap();
|
||||
Measured { size, bounds: bx, counts: n, kinds: kinds(&rec) }
|
||||
});
|
||||
judge(name, "C++", &c.expect, got)
|
||||
}
|
||||
|
||||
/// solidcore and C++ agree: volume (area) within 1e-9, the same counts and faces by kind, or the
|
||||
/// same error.
|
||||
#[cfg(feature = "native")]
|
||||
fn parity(name: &'static str) {
|
||||
let cpp = cpp_case(name);
|
||||
let ours = solidcore_case(name);
|
||||
match (ours, cpp) {
|
||||
(Ok(a), Ok(b)) => {
|
||||
assert!(rel(a.size, b.size) < 1e-9, "{name}: solidcore {} vs C++ {}", a.size, b.size);
|
||||
assert_eq!(a.counts, b.counts, "{name}: face, edge, vertex counts (solidcore vs C++)");
|
||||
assert_eq!(a.kinds, b.kinds, "{name}: faces by surface kind (solidcore vs C++)");
|
||||
}
|
||||
(a, b) => assert_eq!(a.err(), b.err(), "{name}: errors (solidcore vs C++)"),
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! cases {
|
||||
($($m:ident $(: $ign:meta)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
@@ -418,19 +343,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,208 +524,6 @@ 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`):
|
||||
/// a top moved onto the other's plane closes the step wall between them to nothing, and the tops
|
||||
@@ -838,24 +548,9 @@ fn offset_faces_close_a_step_wall() {
|
||||
/// 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 +583,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);
|
||||
}
|
||||
@@ -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,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,76 +127,6 @@ 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)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
@@ -207,19 +135,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 +223,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 +230,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,26 +255,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
|
||||
/// [0, 40]) cut by four plane strips standing on the square [12, 28]^2: the strips meet the
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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(°ree) || count < degree + 1 || count > ffi::sweeps::CURVE_MAX_POINTS {
|
||||
if !(1..=CURVE_MAX_DEGREE).contains(°ree) || count < degree + 1 || count > CURVE_MAX_POINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut ps = Vec::with_capacity(count);
|
||||
@@ -213,7 +214,7 @@ fn parse(k: &Kernel, curves: &[Profile]) -> Out<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,12 +1594,7 @@ 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(°ree)
|
||||
|| count < degree + 1
|
||||
|| count > ffi::sweeps::CURVE_MAX_POINTS
|
||||
|| knots.len() != count + degree + 1
|
||||
|| !clamped
|
||||
{
|
||||
if !(1..=CURVE_MAX_DEGREE).contains(°ree) || count < degree + 1 || count > CURVE_MAX_POINTS || knots.len() != count + degree + 1 || !clamped {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
let points = can.control_points.iter().enumerate().map(|(i, p)| [p.x, p.y, p.z, can.weights.get(i).copied().unwrap_or(1.0)]).collect();
|
||||
|
||||
@@ -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,52 +355,6 @@ 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)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
@@ -411,26 +363,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 +398,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 +421,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,21 +469,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.
|
||||
#[test]
|
||||
@@ -669,62 +570,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]
|
||||
fn hostile_arguments_are_refused() {
|
||||
|
||||
@@ -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,44 +174,6 @@ 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)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
@@ -222,19 +182,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));
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
@@ -291,38 +238,3 @@ fn the_body_stays_as_it_was() {
|
||||
assert_eq!(bounds(x).unwrap(), ([1.0, 2.0, 3.0], [11.0, 22.0, 33.0]));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,39 +132,6 @@ 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)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
@@ -174,19 +140,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,26 +183,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 {
|
||||
use solidcore::topology::brep::p3::new_brep;
|
||||
@@ -339,42 +272,6 @@ 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)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
@@ -383,19 +280,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));
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
@@ -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(¢er, radius).map(Brep::made).map_err(error)
|
||||
crate::core::make_sphere(center, radius).map(Brep::made_core)
|
||||
}
|
||||
|
||||
/// A cone or frustum about +Z: base centre, base radius (> 0), top radius (≥ 0, not the base
|
||||
/// radius) and height (> 0), in mm.
|
||||
pub fn make_cone(origin: [f64; 3], base_radius: f64, top_radius: f64, height: f64) -> Result<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(¢er, major_radius, minor_radius).map(Brep::made).map_err(error)
|
||||
crate::core::make_torus(center, major_radius, minor_radius).map(Brep::made_core)
|
||||
}
|
||||
|
||||
/// The flat arrays of this body that the mapping reads, from the kernel in force
|
||||
/// ([`crate::route::backend`]).
|
||||
/// The flat arrays of this body that the mapping reads.
|
||||
pub fn arrays(&self) -> Result<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.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#[cfg(feature = "native")]
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
mod native_tests {
|
||||
use super::*;
|
||||
use crate::{CheckLevel, MEASURE_ACCURACY};
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
@@ -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();
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
-229
@@ -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,15 +278,7 @@ 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)
|
||||
}
|
||||
|
||||
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)
|
||||
core::blend::blend(self.core(), kind, edges, sizes, tolerance, corners, setbacks, open).map(Brep::made_core)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
}
|
||||
@@ -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,6 @@ 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
@@ -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
@@ -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(°ree) {
|
||||
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(°ree), Some(&count)) = (out.kinds.get(k), out.kinds.get(k + 1), out.kinds.get(k + 2)) else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
k += 3;
|
||||
let degree = usize::try_from(degree).map_err(|_| Error::Argument)?;
|
||||
let count = usize::try_from(count).map_err(|_| Error::Argument)?;
|
||||
let pts = out.reals.get(r..r + 4 * count).ok_or(Error::Argument)?;
|
||||
r += 4 * count;
|
||||
let knots = out.reals.get(r..r + count + degree + 1).ok_or(Error::Argument)?.to_vec();
|
||||
r += count + degree + 1;
|
||||
let points = pts.as_chunks::<4>().0.to_vec();
|
||||
curves.push((Curve::Nurbs { degree, points, knots }, [ivl[0], ivl[1]]));
|
||||
}
|
||||
if k != out.kinds.len() || r != out.reals.len() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(curves)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sweeps_tests.rs"]
|
||||
mod tests;
|
||||
@@ -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
@@ -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};
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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,19 @@ 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:?}");
|
||||
}
|
||||
+7
-103
@@ -37,8 +37,6 @@ use crate::ui_kit;
|
||||
use crate::viewport::NavMode;
|
||||
|
||||
pub const UNITS: &[&str] = &["mm", "cm", "m", "in", "ft"];
|
||||
/// The Geometry kernel preference's choices, as `diagnostics.kernel` spells them.
|
||||
pub const KERNELS: [&str; 2] = ["solidcore", "smlib"];
|
||||
pub const OPENSCAD_NAV: &str = "openscad";
|
||||
pub const TOUCHPAD_NAV: &str = "touchpad";
|
||||
/// A mouse button that can drag the camera.
|
||||
@@ -1161,11 +1159,6 @@ preference_settings! {
|
||||
/// Up/Down and the wheel over a focused value field step its plain number (`value_step`,
|
||||
/// #2320). Off by default: the keys move the text cursor and the wheel scrolls, as before.
|
||||
step_values: bool = false, PAGE_DESIGN => check_id("ui.step_values");
|
||||
/// Which geometry kernel answers every operation (one of [`KERNELS`]): `solidcore`, the
|
||||
/// default, or `smlib`, the C++ kernel. A saved `routed`, the old default, reads as
|
||||
/// `solidcore` ([`migrate`]). Applied through
|
||||
/// `diagnostics.kernel`; `SOLIDCRAFT_KERNEL`, when set, wins.
|
||||
geometry_kernel: String = "solidcore".into(), PAGE_DESIGN;
|
||||
/// When SMLib's full check runs while features are computed (`diagnostics.validation`):
|
||||
/// `each_feature` (the default) checks the bodies each feature keeps once; `each_step`, as
|
||||
/// before, checks after every sweep, boolean and blend; `off` (experimental) never does.
|
||||
@@ -1633,14 +1626,6 @@ pub struct PrefsWindow {
|
||||
/// The Profile operations preference the profiler last followed: applied when it changes,
|
||||
/// so `diagnostics.profile` (an agent) and SOLIDCRAFT_PROFILE can switch it in between.
|
||||
applied_profile: Option<bool>,
|
||||
/// The Geometry kernel preference `diagnostics.kernel` last applied: applied when it changes
|
||||
/// (and at startup), so an agent's `diagnostics.kernel` in between stands.
|
||||
applied_kernel: Option<String>,
|
||||
/// The choice `SOLIDCRAFT_KERNEL` forces when it overrides the preference (the command's
|
||||
/// `environment` and `choice`).
|
||||
pub(crate) kernel_environment: Option<String>,
|
||||
/// The saved kernel is one this build doesn't have (`smlib` without the C++ kernel): the page says so.
|
||||
pub(crate) kernel_unavailable: bool,
|
||||
/// The appearance last asked of the native window ([`sync_window_theme`]).
|
||||
window_theme: Option<egui::SystemTheme>,
|
||||
/// The perspective angle the view was last drawn with ([`follow_view_fov`]); 0 before the first.
|
||||
@@ -2313,26 +2298,6 @@ pub fn apply(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
app.prefs_window.applied_profile = Some(profile);
|
||||
}
|
||||
// The geometry kernel, through its command (an unknown saved value is the default). `smlib` on
|
||||
// a build without it is refused: the preference page and the status line say so.
|
||||
let kernel = KERNELS.iter().copied().find(|k| *k == app.preferences.geometry_kernel).unwrap_or(KERNELS[0]);
|
||||
if app.prefs_window.applied_kernel.as_deref() != Some(kernel) {
|
||||
match app.session.execute("diagnostics.kernel", &json!({ "choice": kernel })) {
|
||||
Ok(v) => {
|
||||
let environment = v.get("environment").and_then(serde_json::Value::as_bool).unwrap_or(false);
|
||||
app.prefs_window.kernel_environment =
|
||||
environment.then(|| v.get("choice").and_then(serde_json::Value::as_str).unwrap_or_default().to_string());
|
||||
app.prefs_window.kernel_unavailable = false;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("geometry kernel preference `{kernel}`: {e}");
|
||||
app.prefs_window.kernel_unavailable = true;
|
||||
let message = app.tr("prefs.geometry_kernel_unavailable").to_string();
|
||||
app.set_status(message, true);
|
||||
}
|
||||
}
|
||||
app.prefs_window.applied_kernel = Some(kernel.into());
|
||||
}
|
||||
apply_file_limits(&app.preferences);
|
||||
// The viewport's display quality (an unknown saved value draws at Normal).
|
||||
solidcraft_engine::doc::display_quality::set_current(
|
||||
@@ -4057,10 +4022,6 @@ pub fn migrate(mut p: Prefs) -> Prefs {
|
||||
p.orbit_around = OrbitAround::Cursor;
|
||||
p.orbit_cursor = false;
|
||||
}
|
||||
// `routed`, the old default, ran every operation on solidcore.
|
||||
if p.geometry_kernel == "routed" {
|
||||
p.geometry_kernel = KERNELS[0].into();
|
||||
}
|
||||
if !crate::space_mouse::drives_each_once(p.space_mouse_axis_source) {
|
||||
log::warn!("preferences: the 3D mouse axis assignment {:?} doesn't drive each axis once; using the default", p.space_mouse_axis_source);
|
||||
p.space_mouse_axis_source = crate::space_mouse::SAME_AXES;
|
||||
@@ -4201,10 +4162,6 @@ fn prepare_preferences(
|
||||
{
|
||||
return Err("`validation` must be each_step, each_feature or off".into());
|
||||
}
|
||||
// Before `migrate`, which reads the old saved `routed` as the default: asked for now, it is refused.
|
||||
if p.get("geometry_kernel").is_some_and(|v| !v.as_str().is_some_and(|k| KERNELS.contains(&k))) {
|
||||
return Err(format!("`geometry_kernel` must be one of {}", KERNELS.join(", ")));
|
||||
}
|
||||
if let Some(value) = p.get("navigation_bar") {
|
||||
crate::navigation_bar::validate(value)?;
|
||||
}
|
||||
@@ -4228,9 +4185,6 @@ fn prepare_preferences(
|
||||
if !UNITS.contains(&new.default_units.as_str()) {
|
||||
return Err(format!("units must be one of {}", UNITS.join(", ")));
|
||||
}
|
||||
if !KERNELS.contains(&new.geometry_kernel.as_str()) {
|
||||
return Err(format!("`geometry_kernel` must be one of {}", KERNELS.join(", ")));
|
||||
}
|
||||
if !WINDOW_SYSTEMS.contains(&new.window_system.as_str()) {
|
||||
return Err(format!("`window_system` must be one of {}", WINDOW_SYSTEMS.join(", ")));
|
||||
}
|
||||
@@ -4408,68 +4362,18 @@ mod tests {
|
||||
assert!(!solidcraft_engine::profile::enabled());
|
||||
}
|
||||
|
||||
/// The Geometry kernel preference reaches the kernel through diagnostics.kernel, at startup
|
||||
/// and when it changes, and only a choice the command knows is accepted. `smlib` keeps the C++
|
||||
/// kernel reachable, the behaviour from before solidcore was the default. (The kernel choice is
|
||||
/// process-wide and other tests start harnesses meanwhile, so `smlib` is only set on a build
|
||||
/// without it, where it is refused and nothing moves.)
|
||||
#[test]
|
||||
fn the_geometry_kernel_preference_applies_through_its_command() {
|
||||
use serde_json::json;
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.frames(1);
|
||||
assert_eq!(h.app.prefs_window.applied_kernel.as_deref(), Some("solidcore"), "applied at startup");
|
||||
let r = h.app.session.execute("diagnostics.kernel", &json!({})).unwrap();
|
||||
let environment = (r["environment"] == json!(true)).then(|| r["choice"].as_str().unwrap_or_default().to_string());
|
||||
assert_eq!(h.app.prefs_window.kernel_environment, environment);
|
||||
assert!(h.call("ui.prefs", json!({"geometry_kernel": "truck"}))["error"].as_str().is_some_and(|e| e.contains("geometry_kernel")));
|
||||
// `routed` is gone: a saved one reads as solidcore (below), asked for now it is refused.
|
||||
assert!(h.call("ui.prefs", json!({"geometry_kernel": "routed"}))["error"].as_str().is_some_and(|e| e.contains("geometry_kernel")));
|
||||
assert_eq!(h.app.preferences.geometry_kernel, "solidcore");
|
||||
assert_eq!(super::KERNELS, ["solidcore", "smlib"]);
|
||||
h.call("ui.prefs", json!({"geometry_kernel": "solidcore"}));
|
||||
h.frames(2);
|
||||
assert_eq!(h.app.prefs_window.applied_kernel.as_deref(), Some("solidcore"));
|
||||
assert!(!h.app.prefs_window.kernel_unavailable);
|
||||
// Saved with the other preferences.
|
||||
let saved = serde_json::to_value(&h.app.preferences).unwrap();
|
||||
assert_eq!(saved["geometry_kernel"], json!("solidcore"));
|
||||
// On a build without the C++ kernel, asking for it says so: not a switch to the other one.
|
||||
if !solidcraft_engine::kernel::smlib_built() {
|
||||
h.call("ui.prefs", json!({"geometry_kernel": "smlib"}));
|
||||
h.frames(2);
|
||||
assert!(h.app.prefs_window.kernel_unavailable);
|
||||
assert_eq!(h.app.preferences.geometry_kernel, "smlib");
|
||||
assert_ne!(h.app.session.execute("diagnostics.kernel", &json!({})).unwrap()["choice"], json!("smlib"));
|
||||
h.call("ui.prefs", json!({"geometry_kernel": "solidcore"}));
|
||||
h.frames(2);
|
||||
assert!(!h.app.prefs_window.kernel_unavailable);
|
||||
}
|
||||
}
|
||||
|
||||
/// A saved `routed`, the old default, loads as `solidcore`, without an error; any other value
|
||||
/// this version doesn't know stays as it was saved and runs on the default, and nothing panics.
|
||||
#[test]
|
||||
fn a_saved_routed_kernel_loads_as_solidcore() {
|
||||
use serde_json::json;
|
||||
let read = |saved: serde_json::Value| super::migrate(serde_json::from_value::<super::Prefs>(saved).unwrap()).geometry_kernel;
|
||||
assert_eq!(read(json!({"geometry_kernel": "routed"})), "solidcore");
|
||||
assert_eq!(read(json!({})), "solidcore");
|
||||
assert_eq!(read(json!({"geometry_kernel": "smlib"})), "smlib");
|
||||
assert_eq!(read(json!({"geometry_kernel": "truck"})), "truck");
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.preferences.geometry_kernel = "truck".into();
|
||||
h.frames(2);
|
||||
assert_eq!(h.app.prefs_window.applied_kernel.as_deref(), Some("solidcore"));
|
||||
}
|
||||
|
||||
/// Preferences saved with a setting this version no longer has still load, every other
|
||||
/// setting kept: the removed key is ignored like any unknown one.
|
||||
#[test]
|
||||
fn saved_preferences_with_a_removed_setting_still_load() {
|
||||
use serde_json::json;
|
||||
for removed in ["metric_tap_drill_major_less_pitch", "surface_extrude_symmetric_per_side"] {
|
||||
let saved = json!({"length_decimals": 4, removed: true});
|
||||
for (removed, value) in [
|
||||
("geometry_kernel", json!("smlib")),
|
||||
("geometry_kernel", json!("solidcore")),
|
||||
("metric_tap_drill_major_less_pitch", json!(true)),
|
||||
("surface_extrude_symmetric_per_side", json!(true)),
|
||||
] {
|
||||
let saved = json!({"length_decimals": 4, removed: value});
|
||||
let p = serde_json::from_value::<super::Prefs>(saved.clone()).unwrap();
|
||||
assert_eq!(p.length_decimals, 4, "{removed}");
|
||||
assert_eq!(super::lenient(&saved).length_decimals, 4, "{removed}");
|
||||
|
||||
@@ -136,28 +136,4 @@ fn origin_plane_names(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
setting::hide_origin(app, ui);
|
||||
ui_kit::text(ui, app.tr("ui.hide_origin_note"), ui_kit::Text::Hint("hide_origin"));
|
||||
});
|
||||
// Which geometry kernel answers; `prefs::apply` applies it through diagnostics.kernel.
|
||||
ui_kit::settings_section(ui, "geometry_kernel", app.tr("prefs.geometry_kernel"), |ui| {
|
||||
// Under SOLIDCRAFT_KERNEL the control shows the environment's choice, disabled.
|
||||
let environment = app.prefs_window.kernel_environment.clone();
|
||||
ui.add_enabled_ui(environment.is_none(), |ui| {
|
||||
let options = ["solidcore", "SMLib"];
|
||||
let shown = environment.as_deref().unwrap_or(&app.preferences.geometry_kernel);
|
||||
let mut selected = KERNELS.iter().position(|k| *k == shown).unwrap_or(0);
|
||||
ui_kit::segmented(ui, &mut selected, &options, ui_kit::Segments { intrinsic: true, ..Default::default() });
|
||||
if let Some(kernel) = KERNELS.get(selected)
|
||||
&& environment.is_none()
|
||||
&& app.preferences.geometry_kernel != *kernel
|
||||
{
|
||||
app.preferences.geometry_kernel = (*kernel).into();
|
||||
}
|
||||
});
|
||||
ui_kit::text(ui, app.tr("prefs.geometry_kernel_note"), ui_kit::Text::Hint("geometry_kernel"));
|
||||
if environment.is_some() {
|
||||
ui_kit::text(ui, app.tr("prefs.geometry_kernel_environment"), ui_kit::Text::Hint("geometry_kernel_environment"));
|
||||
}
|
||||
if app.prefs_window.kernel_unavailable {
|
||||
ui_kit::text(ui, app.tr("prefs.geometry_kernel_unavailable"), ui_kit::Text::Hint("geometry_kernel_unavailable"));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -13,5 +13,5 @@ allow-wildcard-paths = true
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
# solidcore, pinned by rev in Cargo.toml (crates/smlib's second backend).
|
||||
# solidcore, pinned by rev in Cargo.toml (the kernel behind crates/smlib).
|
||||
allow-git = ["https://github.com/tangency-labs/solidcore"]
|
||||
@@ -334,7 +334,7 @@ To find the slow steps of a script, `--timings` prints how long each command too
|
||||
the measurement: `{"timings": [{"index", "command", "ms"}], "total_ms"}`, with `index` counted
|
||||
from 0. `--profile` adds each command's `profile: {"boolean_ms", "tessellate_ms", "other_ms"}`,
|
||||
its wall time in the kernel's booleans, in tessellation and in everything else, and prints the
|
||||
profiler's table on stderr: where the time went, SMLib's calls to the commands
|
||||
profiler's table on stderr: where the time went, the kernel's calls to the commands
|
||||
(`docs/perf.md`, "Where the time goes"); `--profile-out trace.json` also writes the spans as a
|
||||
Chrome trace. `--profile` works on a design or STEP file too. With
|
||||
`--results` as well, the results go in the same object as `results`; `--results` alone still
|
||||
|
||||
@@ -164,7 +164,7 @@ same triangles, colour and paint; the picture shown while opening draws its tria
|
||||
`cache/model.json` records `solidcraft-model-cache/8`, the producer revision
|
||||
`solidcraft-truck-adapter/4`, the application version, the build (source commit,
|
||||
CPU architecture and OS), the evaluation source (below, with the CPU architecture
|
||||
and OS), the geometry kernel that computed it (`solidcore` or `smlib`), the SHA-256 of the exact `document.json` (whose blob references pin every
|
||||
and OS), the geometry kernel that computed it (`solidcore`; earlier builds could write `smlib`, a cache this build evaluates again), the SHA-256 of the exact `document.json` (whose blob references pin every
|
||||
STEP, mesh and image payload) and of `cache/bodies.bin`, the slices, each kernel
|
||||
body's slice and display share, and the model's records. A cache is read back only
|
||||
when the format, the producer, the evaluation source, the platform, the kernel and both
|
||||
|
||||
+1
-2
@@ -9,8 +9,7 @@ What it records, as nested spans on each thread:
|
||||
|
||||
| Category | Spans |
|
||||
|---|---|
|
||||
| `solidcore` | every call into solidcore, the default kernel |
|
||||
| `smlib` | every SMLib C API call, timed by the C API itself (`steady_clock`), when the C++ kernel is asked for |
|
||||
| `solidcore` | every call into solidcore, the kernel |
|
||||
| `kernel` | booleans, blends, sweeps, lofts, shell/draft/offset, healing, sewing, conversions, meshing, measuring |
|
||||
| `import` | the stages of a STEP or IGES import: parse, index, each solid, placement, mesh check, convert batches |
|
||||
| `feature` | each feature computed, by its type; `doc evaluate` around them |
|
||||
|
||||
@@ -118,7 +118,6 @@ class Real(unittest.TestCase):
|
||||
for area, globs in {"PATHS-ALL": shared, **areas}.items():
|
||||
if area != "PATHS-ALL":
|
||||
self.assertTrue(os.path.isfile(os.path.join(ROOT, plan.CASES, area, "KNOWN_FAILING")), area)
|
||||
self.assertTrue(os.path.isfile(os.path.join(ROOT, plan.CASES, area, "KNOWN_FAILING.solidcore")), area)
|
||||
for g in globs:
|
||||
with self.subTest(area=area, glob=g):
|
||||
rx = plan.glob_regex(g)
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fetches SMLib for solidcraft-smlib (feature `native`), at the commit in crates/smlib/SMLIB_REV.
|
||||
#
|
||||
# scripts/fetch-smlib.sh [TARGET...] the prebuilt C API library for each Rust target (default:
|
||||
# the host's), checked against crates/smlib/SMLIB_PREBUILT,
|
||||
# into target/smlib-<rev12>-prebuilt/<target>; the sources
|
||||
# for a target with no prebuilt library
|
||||
# scripts/fetch-smlib.sh --source the sources (source/SMLib, source/SM_API, capi), into
|
||||
# target/smlib-<rev12> or SOLIDCRAFT_SMLIB_DIR, to compile
|
||||
# scripts/fetch-smlib.sh --pin writes SMLIB_PREBUILT from the release of SMLIB_REV
|
||||
#
|
||||
# The libraries are the release prebuilt-<rev> of tangency-labs/smlib (its prebuilt.yml).
|
||||
# OpenCASCADE is not part of the fork.
|
||||
set -euo pipefail
|
||||
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
rev="$(tr -d '[:space:]' < "$root/crates/smlib/SMLIB_REV")"
|
||||
pins="$root/crates/smlib/SMLIB_PREBUILT"
|
||||
release="https://github.com/tangency-labs/smlib/releases/download/prebuilt-$rev"
|
||||
|
||||
sha256() {
|
||||
if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi
|
||||
}
|
||||
|
||||
fetch_source() {
|
||||
local dest="${SOLIDCRAFT_SMLIB_DIR:-$root/target/smlib-${rev:0:12}}"
|
||||
if [ -f "$dest/capi/include/smlib.h" ] && [ "$(git -C "$dest" rev-parse HEAD 2>/dev/null)" = "$rev" ]; then
|
||||
echo "SMLib sources $rev already at $dest"
|
||||
return
|
||||
fi
|
||||
rm -rf "$dest"
|
||||
mkdir -p "$dest"
|
||||
git -C "$dest" init -q
|
||||
git -C "$dest" remote add origin https://github.com/tangency-labs/smlib.git
|
||||
git -C "$dest" sparse-checkout set --no-cone /source/SMLib/ /source/SM_API/ /capi/ /LICENSE
|
||||
git -C "$dest" fetch -q --depth 1 --filter=blob:none origin "$rev"
|
||||
git -C "$dest" checkout -q FETCH_HEAD
|
||||
echo "SMLib sources $rev at $dest"
|
||||
}
|
||||
|
||||
fetch_prebuilt() {
|
||||
local target="$1" want dest tmp
|
||||
want="$(awk -v t="$target" '$1 == t { print $2 }' "$pins")"
|
||||
if [ -z "$want" ]; then
|
||||
echo "no prebuilt SMLib for $target: fetching the sources to compile"
|
||||
fetch_source
|
||||
return
|
||||
fi
|
||||
dest="$root/target/smlib-${rev:0:12}-prebuilt/$target"
|
||||
if [ "$(cat "$dest/.sha256" 2>/dev/null)" = "$want" ]; then
|
||||
echo "SMLib $rev for $target already at $dest"
|
||||
return
|
||||
fi
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' RETURN
|
||||
curl -fsSL --retry 3 -o "$tmp/smlib.tar.gz" "$release/smlib-$target.tar.gz"
|
||||
if [ "$(sha256 "$tmp/smlib.tar.gz")" != "$want" ]; then
|
||||
echo "smlib-$target.tar.gz does not match its SHA-256 in crates/smlib/SMLIB_PREBUILT" >&2
|
||||
exit 1
|
||||
fi
|
||||
tar -xzf "$tmp/smlib.tar.gz" -C "$tmp"
|
||||
if [ "$(tr -d '[:space:]' < "$tmp/smlib-$target/REV")" != "$rev" ]; then
|
||||
echo "smlib-$target.tar.gz was built from another commit than SMLIB_REV" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$dest"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
mv "$tmp/smlib-$target" "$dest"
|
||||
echo "$want" > "$dest/.sha256"
|
||||
echo "SMLib $rev for $target at $dest"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--source)
|
||||
fetch_source
|
||||
;;
|
||||
--pin)
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
curl -fsSL --retry 3 -o "$tmp/SHA256SUMS" "$release/SHA256SUMS"
|
||||
{
|
||||
echo "# Rust target, SHA-256 of smlib-<target>.tar.gz in the release prebuilt-<SMLIB_REV> of"
|
||||
echo "# tangency-labs/smlib. Written by scripts/fetch-smlib.sh --pin; never edited by hand."
|
||||
sed -E 's/^([0-9a-f]{64}) +smlib-(.*)\.tar\.gz$/\2 \1/' "$tmp/SHA256SUMS" | sort
|
||||
} > "$pins"
|
||||
echo "pinned $(grep -vc '^#' "$pins") prebuilt targets of $rev"
|
||||
;;
|
||||
*)
|
||||
if [ $# -eq 0 ]; then
|
||||
set -- "$(rustc -vV | sed -n 's/^host: //p')"
|
||||
fi
|
||||
for target in "$@"; do
|
||||
fetch_prebuilt "$target"
|
||||
done
|
||||
;;
|
||||
esac
|
||||
@@ -1,10 +1,12 @@
|
||||
# The cargo tests that fail today on the default kernel, solidcore, one a line: `<test> <why it fails
|
||||
# The cargo tests that fail today on the kernel, solidcore, one a line: `<test> <why it fails
|
||||
# today>`, with the issue that tracks it when there is one. Sorted. `<test>` is nextest's
|
||||
# `<binary id>::<test name>`, as the junit report names it. CI's test jobs (`cargo xtask known-failing`)
|
||||
# fail on a test that fails and is not listed here, and on a listed test that passes: a fix removes its
|
||||
# line in the same pull request. The tests stay un-ignored, so every run shows whether they still fail.
|
||||
# `KNOWN_FAILING_TESTS.smlib` is the same list for the C++ kernel (SOLIDCRAFT_KERNEL=smlib, graded with
|
||||
# --list). Refresh as that list's header says.
|
||||
# Refresh: download the junit artifacts of develop's latest CI run, one directory each
|
||||
# (`gh run download <run> -R tangency-labs/solidcraft -n <artifact> -D <dir>/<artifact>` for junit
|
||||
# and junit-gpu-1..4), run `cargo xtask known-failing --junit <dir>`, then drop each FIXED line and
|
||||
# add each REGRESSION line with its cause, from the test's failure message.
|
||||
solidcraft-cli::action_matrix::action_matrix_sample 3 baseline actions fail, the same 3 as on C++: 2 shells (Bad Loop Orient on issue-616-lid-ribs; status 1001 on filleted-shelled-box), 1 fillet (SmSurfOfRevolution Test #7 on oblique_cut_of_cylinder)
|
||||
solidcraft-cli::model_cache::a_later_round_after_opening_blends_the_saved_rounds_again the later r3 round at the r2 mitre builds solidcore's 10-face corner (7928.743041), the test wants SMLib f916a8d's setback corner (smlib#104), which solidcore ports in solidcore#2310: 15 faces, 7928.142563938
|
||||
solidcraft-cli::naming_corpus::corpus_designs_name_alike_every_build_and_fail_where_a_reference_is_gone oracle/83-boundary-loop-thicken does not load: step 8, thicken status 1001, as on C++
|
||||
@@ -31,12 +33,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)
|
||||
@@ -1,43 +0,0 @@
|
||||
# The cargo tests that fail today on the C++ kernel (SOLIDCRAFT_KERNEL=smlib), one a line: `<test> <why it fails today>`, with the issue that
|
||||
# tracks it when there is one. Sorted. `<test>` is nextest's `<binary id>::<test name>`, as the
|
||||
# junit report names it. CI's test jobs (`cargo xtask known-failing`) fail on a test that fails
|
||||
# and is not listed here, and on a listed test that passes: a fix removes its line in the same
|
||||
# pull request. The tests stay un-ignored, so every run shows whether they still fail.
|
||||
# Refresh (after an SMLib pin bump): download the junit artifacts of develop's latest CI run, one
|
||||
# directory each (`gh run download <run> -R tangency-labs/solidcraft -n <artifact> -D <dir>/<artifact>`
|
||||
# for junit and junit-gpu-1..4), run `cargo xtask known-failing --junit <dir>`, then drop each
|
||||
# FIXED line and add each REGRESSION line with its cause, from the test's failure message.
|
||||
# Baseline: develop 38fd4a4f6 (CI run 41157648345, x86_64 Linux), after the SMLib eaa998a pin; SMLib lines as of f916a8d (smlib 5862ee223).
|
||||
solidcraft-cli::action_matrix::action_matrix_sample 2 baseline actions fail on SMLib: 2 shells (Bad Loop Orient on issue-616-lid-ribs; status 1001 on filleted-shelled-box, shelled again open at an inner wall)
|
||||
solidcraft-cli::naming_corpus::corpus_designs_name_alike_every_build_and_fail_where_a_reference_is_gone oracle/83-boundary-loop-thicken does not load: step 8, SMLib thicken status 1001
|
||||
solidcraft-doc::eval::chain_tests::a_propagated_chain_that_cannot_build_says_how_to_blend_the_edge_alone the propagated chain now builds a round (9 faces), so there is no refusal to read, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-doc::eval::cost_tests::a_cut_leaves_bodies_it_cannot_reach_as_they_were main's mesh-based version (#10303) not taken; the exact replacement is to come
|
||||
solidcraft-doc::eval::cost_tests::a_tap_marking_searches_only_the_bodies_near_it main's mesh-based version (#10303) not taken; the exact replacement is to come
|
||||
solidcraft-doc::eval::cost_tests::a_tap_marking_still_searches_the_bodies_within_its_reach main's mesh-based version (#10303) not taken; the exact replacement is to come
|
||||
solidcraft-engine::cmd::file::tests::each_format_has_an_export_command_of_its_own SMLib meshes a box's planar faces into 124 triangles, not 12 (the STL is 6284 bytes, the test counts 84 + 50 x 12)
|
||||
solidcraft-engine::cmd::sketch_project::tests::intersection_curve_of_picked_faces_spans_the_whole_surface main's mesh-based version (#9107) not taken; the exact replacement is to come
|
||||
solidcraft-engine::extent_tests::cut_to_the_far_side_of_a_curved_wall main's mesh-based version (#8634) not taken; the exact replacement is to come
|
||||
solidcraft-engine::hole_face_tests::a_hole_wider_than_the_part_says_it_removes_all_of_it a 40 mm hole at (10, 10) touches the plate's edge y = 30 at one point; SMLib parts the plate there into 2 bodies, the test wants truck's 1
|
||||
solidcraft-engine::measure_centre_tests::a_fillet_round_a_boss_reports_its_radius SMLib's round at the foot of a boss is not an analytic torus of minor radius 3, so no face of the body is (the test finds none to measure)
|
||||
solidcraft-engine::tests::fillet_fit_taper_closes_to_a_point SMLib fillet status 1001 on the taper fit (radius 16 closing to 0)
|
||||
solidcraft-engine::tests::mirror_placed::dragging_a_mirrored_part_meshes_its_copies_once on SMLib a copy of a copy drawn from its first copy's triangles (#10101) is triangulated differently from a fresh evaluation; the surface is the same. Dealt with in solidcore, where the mesher moves (docs/layers.md)
|
||||
solidcraft-engine::tests::robustness::fillet_into_an_earlier_round R2 into the earlier round: volume 35923.387 against 35919.524, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-engine::tests::shell_tangent_chain_that_opens_every_face_explains_how_to_keep_walls SMLib shell status 1034 on a box with all 12 edges rounded (Ball corners since 5e8c9c1), opened at one face (tangency-labs/smlib#102)
|
||||
solidcraft-kernel::blend::corner_tests::a_round_ending_in_an_earlier_round_matches_both_together SMLib fillet status 1001 on the round that ends in the earlier one
|
||||
solidcraft-kernel::blend::corner_tests::corner_blends_hold_at_any_scale at 0.01x a picked edge runs on tangentially into an unblended edge and SMLib's result is not a closed solid (the smooth-vertex End, tangency-labs/smlib#81)
|
||||
solidcraft-kernel::blend_corner::tests::a_round_next_to_a_round_of_another_radius the face count is 9, not the expected count, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::blend_corner::tests::issue2422_vertical_after_separate_bottom_rounds SMLib's check refuses the fillet: edge and vertex tolerance 0.97 against the body's 0.01 (#2422)
|
||||
solidcraft-kernel::blend_smlib::tests::smooth_edges_and_unfinished_tangent_chains_are_refused_saying_why the smooth edge builds (volume 7957.723) where a refusal is expected, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::corner_g2::tests::a_concave_corner_takes_the_g2_patch_too the concave G2 corner's curvature jump clear of the corners is 5.5e-2, above the bound, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
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 2.6e-6 to 1.33e-3 past the box (the explicit corner is in progress)
|
||||
solidcraft-kernel::corner_g2::tests::curvature_rounds_meet_at_a_g2_corner the corner loop's fill misses its neighbours' curvature by 1.6e-3 (tolerance 1e-3), on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::corner_g2::tests::set_back_8_the_g2_corner_lies_between_the_hull_and_the_prisms set back 8: the G2 corner's distance check reads 0.0611, past its bound (volume 47370.642, in range), on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
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, which SMLib's exact tight box now sees (the explicit G2 corner builds it inside)
|
||||
solidcraft-kernel::issue_851_tests::blend_the_arc_top_edge_and_the_top_front_edge removes 59.148 against 60.122, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::issue_851_tests::fillet_the_arc_top_edge_and_the_front_upright_at_the_arc removes 31.497 against 32.310, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::issue_851_tests::fillet_the_top_front_edge_and_the_front_left_upright removes 42.236 against 43.187, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::issue_851_tests::fillet_the_top_front_edge_and_the_front_upright_at_the_arc removes 41.341 against 42.146, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::smooth_edges::tests::a_seam_split_in_two_edges_is_drawn_whole the side seam is one edge ([24]), not split in two, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
solidcraft-kernel::tests::fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole the partial selection runs on tangentially into an unblended edge and SMLib's result is not a closed solid (the smooth-vertex End, tangency-labs/smlib#81)
|
||||
solidcraft-smlib::ops::tests::a_setback_corner_left_open_returns_its_hole the open corner returns holes of 9, 27 and 19 edges, not its one hole; SMLib's open_holes counts a seam edge its own face uses twice as free (fixed for v1.1.0-alpha.1)
|
||||
solidcraft-smlib::ops::tests::fillet_takes_exactly_the_picked_edges one round on the picked edge gives 9 faces, not the expected count, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
|
||||
@@ -2,15 +2,13 @@
|
||||
# (scripts/ci/kernel_rig_plan.py). One git pathspec glob a line, relative to the repository root:
|
||||
# `*` stays within a folder, `**` crosses folders. Each area's own sources are in <area>/PATHS.
|
||||
#
|
||||
# SMLib: the bindings, the schema 4 record bridge and the pinned revision (SMLIB_REV,
|
||||
# SMLIB_PREBUILT), and the script that fetches it.
|
||||
# The kernel: solidcore's wrapper and the schema 4 record bridge.
|
||||
crates/smlib/**
|
||||
scripts/fetch-smlib.sh
|
||||
# The geometry every body is made of: B-rep record, curves, surfaces, meshes.
|
||||
crates/geom/**
|
||||
# The kernel's core, which every case goes through: bodies, extrude/revolve/primitives, booleans
|
||||
# and placements, the planar prisms, the SMLib sweeps and booleans, history and provenance, the
|
||||
# native bridge to SMLib, tessellation, and what the grader measures (measure, validity, counts).
|
||||
# and placements, the planar prisms, the sweeps and booleans, history and provenance, the
|
||||
# native bridge to the kernel, tessellation, and what the grader measures (measure, validity, counts).
|
||||
crates/kernel/Cargo.toml
|
||||
crates/kernel/src/body.rs
|
||||
crates/kernel/src/boolean_record.rs
|
||||
|
||||
Loaded 100 of 143 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user