mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a59c14678f | |||
| b4a3a9baae | |||
| cc3e954f45 | |||
| c5ccb9201d | |||
| 53211ab6ff | |||
| 49d9ec2be7 | |||
| 39807359f0 | |||
| 28e983e191 | |||
| f582b1f39e | |||
| a025d2de9b | |||
| a89ea233ee | |||
| a239ff50c9 | |||
| 96a8820f56 | |||
| 2b85a39c9c | |||
| d0fbd1f60a | |||
| 7240165c31 | |||
| 3f0e9b87fa | |||
| 4b6bbe9d72 | |||
| 7032e6c760 | |||
| 3ddd8c7179 | |||
| 6a3d422a83 | |||
| 8fa2488a00 | |||
| 8f7a8f693c | |||
| 91ca23359e | |||
| 41700f928e | |||
| 905c02afc2 | |||
| 0cb1031f2c | |||
| b6078cbe68 | |||
| 39e9916cbc | |||
| c324acdf29 | |||
| b6cd0d495e | |||
| 8c597703f8 | |||
| 7a761d9436 | |||
| 2c5a94cd1d | |||
| 7df316ccce | |||
| 0710897b63 | |||
| 3ff9e634bc | |||
| 1db07a82f8 | |||
| d3dc45681a | |||
| 824615e119 | |||
| 97f5208c2c | |||
| 3d5e874943 | |||
| 0106365e64 | |||
| 091357ea58 | |||
| 2f7f0c679c | |||
| bc4741b05a |
@@ -442,9 +442,6 @@ jobs:
|
||||
with:
|
||||
key: quick-compile
|
||||
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: Cargo check
|
||||
run: cargo check --workspace --all-targets --locked
|
||||
|
||||
@@ -464,9 +461,6 @@ jobs:
|
||||
with:
|
||||
key: native-clippy
|
||||
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: Clippy
|
||||
env:
|
||||
FULL: ${{ needs.changes.outputs.full }}
|
||||
@@ -510,9 +504,6 @@ jobs:
|
||||
with:
|
||||
key: native-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:
|
||||
@@ -584,9 +575,6 @@ jobs:
|
||||
with:
|
||||
key: native-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:
|
||||
@@ -703,9 +691,6 @@ jobs:
|
||||
with:
|
||||
key: native-gpu
|
||||
save-if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && matrix.shard == 1 }}
|
||||
- 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:
|
||||
@@ -862,9 +847,6 @@ jobs:
|
||||
with:
|
||||
key: doc-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:
|
||||
@@ -923,9 +905,6 @@ jobs:
|
||||
with:
|
||||
key: oracle
|
||||
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:
|
||||
@@ -965,9 +944,6 @@ jobs:
|
||||
with:
|
||||
key: windows-fxc
|
||||
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
|
||||
# One test at a time: WARP's shader JIT crashes now and then (a null read in
|
||||
# d3d10warp.dll's JIT optimizer, about 1 run in 60) when several devices compile compute
|
||||
# pipelines at once, which the parallel tests do. Minidumps showed it in WARP itself, not
|
||||
@@ -992,9 +968,6 @@ jobs:
|
||||
with:
|
||||
key: windows-startup
|
||||
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: Backend order and probing on Windows
|
||||
run: cargo test -p solidcraft --profile ci --locked -- startup::tests native_crash::tests native_frame::tests
|
||||
- name: Build the app
|
||||
@@ -1132,9 +1105,6 @@ jobs:
|
||||
with:
|
||||
key: wasm
|
||||
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 wasm32-unknown-unknown
|
||||
- run: cargo xtask wasm
|
||||
|
||||
riscv64-gates:
|
||||
@@ -1157,9 +1127,6 @@ jobs:
|
||||
with:
|
||||
key: riscv64
|
||||
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 riscv64gc-unknown-linux-gnu
|
||||
- name: Install cross-compilation dependencies
|
||||
run: |
|
||||
# Ubuntu's regular archive serves amd64; RISC-V packages live in ports.
|
||||
@@ -1180,8 +1147,6 @@ jobs:
|
||||
- name: Cargo check on RISC-V
|
||||
env:
|
||||
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
|
||||
# SMLib has no prebuilt library for RISC-V: its sources are compiled with this.
|
||||
CXX_riscv64gc_unknown_linux_gnu: riscv64-linux-gnu-g++
|
||||
PKG_CONFIG_SYSROOT_DIR: /
|
||||
PKG_CONFIG_PATH: /usr/lib/riscv64-linux-gnu/pkgconfig:/usr/share/pkgconfig
|
||||
PKG_CONFIG_ALLOW_CROSS: 1
|
||||
|
||||
@@ -53,9 +53,6 @@ jobs:
|
||||
with:
|
||||
key: docker-web
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh wasm32-unknown-unknown
|
||||
- name: Install trunk
|
||||
run: |
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
|
||||
@@ -29,9 +29,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- name: Build the CLI
|
||||
run: cargo build --profile ci -p solidcraft-cli
|
||||
- name: Import and check every entry
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 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 SMLib, 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 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.
|
||||
@@ -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:
|
||||
|
||||
@@ -86,15 +84,12 @@ jobs:
|
||||
with:
|
||||
key: fillet-rig
|
||||
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
|
||||
- name: Build the CLI (release, SMLib)
|
||||
run: cargo build --release --locked -p solidcraft-cli --features smlib
|
||||
- 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
|
||||
|
||||
@@ -48,9 +48,6 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: generated-files
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- name: THIRD-PARTY-LICENSES.txt
|
||||
run: cargo xtask licences
|
||||
- name: docs/features.md
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# 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.
|
||||
@@ -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
|
||||
@@ -209,7 +204,7 @@ jobs:
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: kernel-rig-results-${{ matrix.shard }}
|
||||
path: ${{ runner.temp }}/kernel-rig/results.json
|
||||
path: ${{ runner.temp }}/kernel-rig/**/results.json
|
||||
if-no-files-found: ignore
|
||||
|
||||
# One check for the whole gate, for branch protection and the merge queue.
|
||||
|
||||
@@ -41,9 +41,6 @@ jobs:
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh wasm32-unknown-unknown
|
||||
- name: Install mdbook and trunk
|
||||
run: |
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
|
||||
@@ -126,9 +126,6 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-icons
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- run: cargo build --release --locked -p solidcraft-cli
|
||||
- run: |
|
||||
. packaging/env.sh
|
||||
@@ -168,9 +165,6 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-linux-${{ matrix.arch }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh
|
||||
- name: Build and package (AppImage, deb, rpm, tar.gz)
|
||||
run: packaging/linux/package.sh
|
||||
- name: Check the packages
|
||||
@@ -395,9 +389,6 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-macos-${{ matrix.arch }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh ${{ matrix.target }}
|
||||
- name: Build
|
||||
run: packaging/macos/package.sh --build-only ${{ matrix.target }}
|
||||
# Not named release-*: the release job collects every release-* artifact as an asset.
|
||||
@@ -496,9 +487,6 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-windows-${{ matrix.arch }}
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh ${{ matrix.target }}
|
||||
- name: Install WiX
|
||||
shell: pwsh
|
||||
run: |
|
||||
@@ -630,9 +618,6 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-web
|
||||
- name: Fetch SMLib (crates/smlib/SMLIB_REV)
|
||||
shell: bash
|
||||
run: scripts/fetch-smlib.sh wasm32-unknown-unknown
|
||||
- name: Install trunk
|
||||
run: |
|
||||
curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \
|
||||
|
||||
@@ -811,6 +811,15 @@ in THIRD-PARTY-LICENSES.txt; no images or artwork from that project are copied.
|
||||
The glTF test files are built by the tests themselves (crates/io/src/gltf_in_tests.rs,
|
||||
crates/engine/src/tests/gltf_import.rs); no third-party models are committed.
|
||||
|
||||
## Geometry kernel crates
|
||||
|
||||
| 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 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.
|
||||
|
||||
## FreeCAD and Rhino import library
|
||||
|
||||
FreeCAD `.FCStd` and Rhino `.3dm` files are read with [cadmpeg](https://github.com/cadmpeg/cadmpeg)
|
||||
@@ -826,6 +835,7 @@ copied into SolidCraft.
|
||||
| SolidCraft file | Original contribution | Licence |
|
||||
|---|---|---|
|
||||
| crates/ui-egui/src/viewport.rs (`nearest_quarter_turn`, `axis_view_orbit`) | N3MI-DG's [PR #101](https://github.com/bherbruck/solidcraft/pull/101): [minimal TOP/BOTTOM turns](https://github.com/bherbruck/solidcraft/commit/c28e780f52490784ef5886f094b9d3f6d10c3e3f) and [drag-to-orbit](https://github.com/bherbruck/solidcraft/commit/f30b30be6ecc4e2e6544a851243137c01a354934), adapted to the axis gizmo | MIT OR Apache-2.0 |
|
||||
| crates/smlib/src/core/offset.rs (`draft`: the tilted planes and `SmBrep::LocalOperation`, the tail of the capi's `draft_faces`) | ablasik's `draft` in `crates/smlib/src/ffi/rs_ops2.rs` on [ablasik/solidcraft `app/solidcore-backend-on-develop`](https://github.com/ablasik/solidcraft/blob/9c7e8e31565e1823f0591b58e2967351c32af13f/crates/smlib/src/ffi/rs_ops2.rs), adapted to `core::offset`'s errors and checks | MIT OR Apache-2.0 |
|
||||
|
||||
## Vendored third-party code
|
||||
|
||||
|
||||
Generated
+9
-2
@@ -4051,6 +4051,14 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcore"
|
||||
version = "1.0.1"
|
||||
source = "git+https://github.com/tangency-labs/solidcore?rev=2dbd88b6e9b858144609690eb2860e0beba27e54#2dbd88b6e9b858144609690eb2860e0beba27e54"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.30.1"
|
||||
@@ -4277,13 +4285,12 @@ dependencies = [
|
||||
name = "solidcraft-smlib"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"flate2",
|
||||
"log",
|
||||
"serde_json",
|
||||
"solidcore",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-profile",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -31,6 +31,11 @@ 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 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" }
|
||||
# test tooling: a dev-dependency only (xtask/src/layers.rs)
|
||||
solidcraft-testkit = { path = "crates/testkit" }
|
||||
|
||||
|
||||
@@ -20,12 +20,9 @@ name = "solvecraft-cli"
|
||||
path = "src/alias.rs"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu", "smlib", "profile"]
|
||||
default = ["raytrace-cpu", "profile"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
|
||||
# Link SMLib into the kernel (solidcraft-engine/smlib; needs scripts/fetch-smlib.sh). SMLib is the
|
||||
# only kernel, so without it the command line models nothing.
|
||||
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,28 +692,21 @@ fn check_case(dir: &str, dumps: Option<&std::path::Path>, validation: solidcraft
|
||||
return out;
|
||||
}
|
||||
};
|
||||
if !solidcraft_engine::kernel::smlib_built() {
|
||||
out["status"] = json!("unsupported");
|
||||
out["error"] = json!("this build has no SMLib: 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);
|
||||
@@ -985,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.
|
||||
|
||||
|
||||
@@ -14,12 +14,8 @@ name = "solidcraft-web"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["smlib", "profile"]
|
||||
default = ["profile"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
# Link SMLib into the kernel (solidcraft-engine/smlib; needs scripts/fetch-smlib.sh
|
||||
# wasm32-unknown-unknown, or the sources and wasi-sdk in WASI_SDK_PATH, crates/smlib/build.rs). SMLib
|
||||
# is the only kernel, so without it the web app models nothing.
|
||||
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"]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Links the browser build with an 8 MiB stack: solidcore's kernel algorithms (subdivision,
|
||||
//! intersection, filleting) recurse legitimately, and wasm has no guard page to catch an
|
||||
//! overflow (solidcore's README recommends 8 MiB).
|
||||
|
||||
fn main() {
|
||||
if std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32") {
|
||||
println!("cargo:rustc-link-arg=-zstack-size=8388608");
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,9 @@ publish = false
|
||||
default-run = "solidcraft"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu", "bundled-render-library", "smlib", "profile"]
|
||||
default = ["raytrace-cpu", "bundled-render-library", "profile"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
|
||||
# Link SMLib into the kernel (solidcraft-engine/smlib; needs scripts/fetch-smlib.sh). SMLib is the
|
||||
# only kernel, so without it the app models nothing.
|
||||
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"]
|
||||
|
||||
@@ -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"]
|
||||
# Link SMLib into the kernel (solidcraft-kernel/smlib; 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 = []
|
||||
|
||||
@@ -55,7 +55,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
trace?: path (write the recorded spans as a Chrome trace-event file, for chrome://tracing or Perfetto). Returns what \
|
||||
was recorded before enabled and reset apply: {compiled, enabled, wall_ms, spans, dropped, by_category: [{category, \
|
||||
self_ms, share}], rows: [{category, name, count, total_ms, self_ms, max_ms}], rows_total, trace?}, the rows the most \
|
||||
self time first. Categories: smlib (SMLib's C API calls, timed by the C API), kernel, feature (one per feature \
|
||||
self time first. Categories: solidcore (the kernel's calls), kernel, feature (one per feature \
|
||||
computed, by its type), doc, command, import (STEP/IGES stages), io. Self time is a span's time less the spans \
|
||||
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",
|
||||
|
||||
@@ -1124,8 +1124,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
/// While the profiler records, a STEP import's reply carries its own profile: the import's
|
||||
/// stages and SMLib's calls, as diagnostics.profile reports them.
|
||||
#[cfg(feature = "smlib")]
|
||||
/// stages and the kernel's calls, as diagnostics.profile reports them.
|
||||
#[test]
|
||||
fn a_step_import_reports_its_profile_while_the_profiler_records() {
|
||||
let _serial = crate::schema::diagnostics::PROFILE_TESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
@@ -1144,7 +1143,7 @@ mod tests {
|
||||
let p = &out["profile"];
|
||||
let rows = p["rows"].as_array().unwrap();
|
||||
let has = |cat: &str| rows.iter().any(|r| r["category"] == cat);
|
||||
assert!(has("import") && has("smlib"), "{p}");
|
||||
assert!(has("import") && has("solidcore"), "{p}");
|
||||
assert!(p["by_category"].as_array().is_some_and(|c| !c.is_empty()) && p["wall_ms"].as_f64().is_some_and(|w| w > 0.0), "{p}");
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ mod component_clone;
|
||||
mod component_clone_assembly;
|
||||
mod construct;
|
||||
mod delete;
|
||||
mod diagnostics;
|
||||
pub(crate) mod diagnostics;
|
||||
mod edit;
|
||||
mod face;
|
||||
mod features;
|
||||
|
||||
@@ -20,7 +20,7 @@ mod tests {
|
||||
#[test]
|
||||
fn the_licences_are_built_in() {
|
||||
assert!(super::LICENSE_MIT.contains("MIT License") && super::LICENSE_APACHE.contains("Apache License"));
|
||||
assert!(super::THIRD_PARTY.contains("truck") && super::THIRD_PARTY.contains("egui"));
|
||||
assert!(super::THIRD_PARTY.contains("wgpu") && super::THIRD_PARTY.contains("egui"));
|
||||
assert!(super::all().contains("NOTICE"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -109,12 +109,25 @@ fn clamped(p: usize, n: usize, k: &[f64]) -> bool {
|
||||
p >= 1 && k.len() == n + p + 1 && ends(1..=p) && ends(n..=n + p - 1)
|
||||
}
|
||||
|
||||
/// `x` moved by whole periods `b − a` into `[a, b]` when it lies outside.
|
||||
/// How far past an end of the interval, relative to its size, a parameter is still that end:
|
||||
/// rounding, not a period away.
|
||||
const WRAP_SLACK: f64 = 1e-12;
|
||||
|
||||
/// `x` moved by whole periods `b − a` into `[a, b]` when it lies outside. A parameter a rounding
|
||||
/// past an end (a pcurve ending on the seam one ulp beyond it) is that end, not moved a period
|
||||
/// to the other one: a plan's node there would put its triangles across the whole period.
|
||||
pub(crate) fn wrap_into(x: f64, (a, b): (f64, f64)) -> f64 {
|
||||
let period = b - a;
|
||||
if (a..=b).contains(&x) || !(period > 0.0) {
|
||||
return x;
|
||||
}
|
||||
let slack = WRAP_SLACK * period.max(a.abs()).max(b.abs());
|
||||
if x > b && x - b <= slack {
|
||||
return b;
|
||||
}
|
||||
if x < a && a - x <= slack {
|
||||
return a;
|
||||
}
|
||||
let w = x - ((x - a) / period).floor() * period;
|
||||
w.clamp(a, b)
|
||||
}
|
||||
@@ -410,6 +423,9 @@ mod tests {
|
||||
}
|
||||
assert_eq!(s.wrap([1.0, 0.5]), [1.0, 0.5]);
|
||||
assert_eq!(s.wrap([1.25, 2.0]), [0.25, 2.0]);
|
||||
// A rounding past an end is that end, not the other one a period away.
|
||||
assert_eq!(s.wrap([1.0 + f64::EPSILON, 0.5]), [1.0, 0.5]);
|
||||
assert_eq!(s.wrap([-1e-17, 0.5]), [0.0, 0.5]);
|
||||
// Projection finds a point near the seam from either side.
|
||||
let p = s.derivs(0.02, 0.5).map(|d| d.0).unwrap_or(Vec3::ZERO);
|
||||
let dom = domain(&s);
|
||||
|
||||
@@ -26,9 +26,9 @@ 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: SMLib
|
||||
# (scripts/fetch-smlib.sh), as in the apps.
|
||||
solidcraft-engine = { workspace = true, features = ["bundled-render-library", "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 }
|
||||
# The HLSL the D3D12 backend compiles (tests check it suits FXC).
|
||||
|
||||
+13
-2
@@ -1215,7 +1215,7 @@ mod tests {
|
||||
let tree: Value = serde_json::from_slice(manifest).unwrap();
|
||||
assert_eq!(serde_json::to_vec(&tree).unwrap(), *manifest);
|
||||
assert_eq!(tree["model"], model.records);
|
||||
assert_eq!(tree.as_object().unwrap().len(), 11, "{:?}", tree.as_object().unwrap().keys().collect::<Vec<_>>());
|
||||
assert_eq!(tree.as_object().unwrap().len(), 12, "{:?}", tree.as_object().unwrap().keys().collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
/// #9170: a mesh body (an imported STL) is kept in the model cache with the rest of the
|
||||
@@ -1474,7 +1474,7 @@ mod tests {
|
||||
let doc = document();
|
||||
let bytes = write_design_container_with_cache(&doc, DesignCompression::Fast, None, Some(&native_geometry())).unwrap();
|
||||
let original = parts(&bytes);
|
||||
for key in ["format", "producer", "evaluation", "document", "payload"] {
|
||||
for key in ["format", "producer", "evaluation", "kernel", "document", "payload"] {
|
||||
let mut entries = original.clone();
|
||||
let mut manifest: Value = serde_json::from_slice(&entries["cache/model.json"]).unwrap();
|
||||
manifest[key] = json!("unsupported-or-stale");
|
||||
@@ -1548,6 +1548,17 @@ 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 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);
|
||||
manifest.as_object_mut().unwrap().remove("kernel");
|
||||
entries.insert("cache/model.json".into(), serde_json::to_vec(&manifest).unwrap());
|
||||
assert!(read_design_geometry(&package(&entries)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The model cache: a design's evaluated timeline (every feature's result and the state after
|
||||
//! it, [`SavedModel`]), saved with it so opening need not evaluate it again. Bodies use
|
||||
//! SolidCraft's exact native B-rep encoding (a mesh body, its own triangles); the key ties the
|
||||
//! cache to the exact document, and to the source and platform that computed it
|
||||
//! cache to the exact document, and to the source, platform and geometry kernel that computed it
|
||||
//! (docs/design-container.md).
|
||||
use super::*;
|
||||
use solidcraft_doc::{ModelState, SavedModel};
|
||||
@@ -29,6 +29,11 @@ fn evaluation() -> String {
|
||||
format!("{}/{}-{}", env!("SOLIDCRAFT_EVALUATION_SOURCE"), std::env::consts::ARCH, std::env::consts::OS)
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
t.iter().flatten().copied().collect()
|
||||
@@ -296,6 +301,7 @@ impl Encoded {
|
||||
document: digest(document),
|
||||
evaluation: evaluation(),
|
||||
format: FORMAT,
|
||||
kernel: KERNEL,
|
||||
model: records,
|
||||
payload: digest(&data),
|
||||
producer: PRODUCER,
|
||||
@@ -320,6 +326,7 @@ struct Manifest<'a> {
|
||||
document: String,
|
||||
evaluation: String,
|
||||
format: &'a str,
|
||||
kernel: &'a str,
|
||||
model: &'a Value,
|
||||
payload: String,
|
||||
producer: &'a str,
|
||||
@@ -336,14 +343,15 @@ pub fn read_design_geometry(bytes: &[u8]) -> Option<std::sync::Arc<ModelState>>
|
||||
DesignContainerReader::new(Cursor::new(bytes)).ok()?.geometry()?.final_state().ok()
|
||||
}
|
||||
|
||||
/// Whether `manifest` is a model cache of this build's evaluation source for `document`. The
|
||||
/// application version and commit that wrote it are not asked: another version of the same
|
||||
/// Whether `manifest` is a model cache of this build's evaluation source and kernel for `document`.
|
||||
/// The application version and commit that wrote it are not asked: another version of the same
|
||||
/// evaluation source computed the same model.
|
||||
fn current(manifest: &Value, document: &[u8]) -> bool {
|
||||
let is = |key: &str, value: &str| manifest.get(key).and_then(Value::as_str) == Some(value);
|
||||
is("format", FORMAT)
|
||||
&& is("producer", PRODUCER)
|
||||
&& is("evaluation", &evaluation())
|
||||
&& 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]
|
||||
# Link NVIDIA's SMLib (tangency-labs/smlib) into the kernel; needs a C++ toolchain and
|
||||
# scripts/fetch-smlib.sh (ADR 0002). Without it every geometric operation answers that SMLib
|
||||
# was 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,8 @@ 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),
|
||||
})?;
|
||||
// Checked here unless the evaluation's validation mode leaves it to the feature's end
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -236,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;
|
||||
|
||||
@@ -108,7 +108,6 @@ struct Regions {
|
||||
touching: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "smlib")]
|
||||
mod sheets {
|
||||
use solidcraft_geom::Loop2;
|
||||
use solidcraft_geom::Vec3;
|
||||
@@ -289,30 +288,6 @@ mod sheets {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "smlib"))]
|
||||
mod sheets {
|
||||
use super::*;
|
||||
|
||||
fn refused<T>() -> Result<T> {
|
||||
Err(KernelError::Failed("not supported yet: planar region booleans run on SMLib (build with the feature `smlib`)".into()))
|
||||
}
|
||||
|
||||
/// A sheet stands for nothing without SMLib.
|
||||
pub(super) enum Sheet {}
|
||||
|
||||
pub(super) fn sheet(_: &[Region2], _: f64) -> Result<Sheet> {
|
||||
refused()
|
||||
}
|
||||
|
||||
pub(super) fn combine(a: &Sheet, _: &Sheet, _: BoolOp) -> Result<Option<Sheet>> {
|
||||
match *a {}
|
||||
}
|
||||
|
||||
pub(super) fn regions(sheet: &Sheet, _: f64) -> Result<Regions> {
|
||||
match *sheet {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::panic)]
|
||||
mod planar_tests {
|
||||
|
||||
@@ -302,8 +302,7 @@ pub(crate) fn segment(run: &Run, plane: &Plane, tol: f64, work: &mut usize) -> O
|
||||
/// `run` in `plane` exactly as segments: as [`segment`], and a curve of several Bézier spans
|
||||
/// (a chain of a font's curves, a spline through points) as one segment per span, each the
|
||||
/// line, conic or Bézier it is, for a planar region's outline. `None` when the edge doesn't lie
|
||||
/// in a plane parallel to `plane` or isn't read exactly. Only planar sheets read on SMLib use it.
|
||||
#[cfg(feature = "smlib")]
|
||||
/// in a plane parallel to `plane` or isn't read exactly.
|
||||
pub(crate) fn projected(run: &Run, plane: &Plane, tol: f64, work: &mut usize) -> Option<Vec<Seg2>> {
|
||||
read(run, plane, tol, work, true)
|
||||
}
|
||||
|
||||
@@ -192,9 +192,10 @@ 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() {
|
||||
// With SMLib built, its calls are children of the boolean: their time is not its self time.
|
||||
let smlib: std::time::Duration = rep.rows.iter().filter(|r| r.category == "smlib").map(|r| r.total).sum();
|
||||
assert!(smlib > std::time::Duration::ZERO && k.self_time < k.total, "{rep:?}");
|
||||
// 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,17 +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 build has no
|
||||
/// SMLib), 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.
|
||||
#[cfg(not(feature = "smlib"))]
|
||||
#[test]
|
||||
fn a_solid_smlib_refuses_with_its_faces_is_refused_not_meshed() {
|
||||
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();
|
||||
@@ -2013,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");
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use super::*;
|
||||
|
||||
fn wedge() -> Body {
|
||||
/// A wedge 40 x 20, 15 high, its top front edge along x from 0 to 40.
|
||||
fn wedge_body() -> Body {
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Vec2};
|
||||
let outer = Loop2::polygon(&[Vec2::new(0., 0.), Vec2::new(40., 0.), Vec2::new(0., 20.)]);
|
||||
let body = crate::extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0., 15.).unwrap().pop().unwrap();
|
||||
crate::extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0., 15.).unwrap().pop().unwrap()
|
||||
}
|
||||
|
||||
fn wedge() -> Body {
|
||||
let body = wedge_body();
|
||||
let at = Vec3::new(20., 0., 15.);
|
||||
// The room's taper, closing to zero at the wedge's tip, given as a spline fillet: R16 itself
|
||||
// is built over the wedge's cliffs (the blend-overflow decision), so a tapered fillet no
|
||||
@@ -12,6 +17,16 @@ fn wedge() -> Body {
|
||||
fillet_variable_spline(&body, at, t.start, &t.spline).unwrap()
|
||||
}
|
||||
|
||||
/// The wedge's variable round takes off the union of its balls (smlib#78, solidcore#2262):
|
||||
/// `removed_by_balls` along the 40 mm edge, about 754.28, on whichever kernel the test runs.
|
||||
#[test]
|
||||
fn the_wedge_round_keeps_its_volume() {
|
||||
let v = crate::measure(&wedge()).unwrap().volume;
|
||||
let law = taper(&wedge_body(), Vec3::new(20., 0., 15.), 16.).unwrap().spline;
|
||||
let want = 6000.0 - super::tests::removed_by_balls(40.0, &|u| law.eval(u), &|_, _| f64::INFINITY, &[]);
|
||||
assert!(((v - want) / want).abs() < 1e-4, "{v} vs {want}");
|
||||
}
|
||||
|
||||
/// The display mesh of a fillet tapering to zero at a wedge's tip converges to the B-rep: each
|
||||
/// mesh lies within its chord tolerance of the faces, so its volume is within the body's area
|
||||
/// times that tolerance of the exact one (SMLib's, integrated on the surfaces, to 1e-8). Only
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -1724,17 +1724,14 @@ fn tabbed_plate_area(da: f64, dt: f64) -> f64 {
|
||||
0.5 * (big * big * (PI + 2.0 * at) + tab) + (4.0 - dt) * (cy - yc)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole() {
|
||||
/// The tabbed plate's outer outline, reconstructed from its seven public pick coordinates: an R25
|
||||
/// plate, x = ±4 tab sides, R2 corners centred at (±2, 280/9) and a 4 mm straight top.
|
||||
fn tabbed_plate_outline() -> Loop2 {
|
||||
use solidcraft_geom::Seg2;
|
||||
// Reconstruct the outer outline from its seven public pick coordinates:
|
||||
// R25 plate, x=+/-4 tab sides, R2 corners and a 4 mm straight top.
|
||||
// The hole's dimensions come from the public reproduction, not an original design file.
|
||||
let theta = (4.0_f64 / 25.0).acos();
|
||||
let y = (625.0_f64 - 16.0).sqrt();
|
||||
let corner_y = 280.0 / 9.0;
|
||||
let hole_y = 262.0 / 9.0;
|
||||
let outline = Loop2 {
|
||||
Loop2 {
|
||||
segs: vec![
|
||||
Seg2::Arc { center: Vec2::ZERO, radius: 25.0, start: PI - theta, sweep: PI + 2.0 * theta },
|
||||
Seg2::Line { a: Vec2::new(4.0, y), b: Vec2::new(4.0, corner_y) },
|
||||
@@ -1743,7 +1740,43 @@ fn fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole() {
|
||||
Seg2::Arc { center: Vec2::new(-2.0, corner_y), radius: 2.0, start: PI / 2.0, sweep: PI / 2.0 },
|
||||
Seg2::Line { a: Vec2::new(-4.0, corner_y), b: Vec2::new(-4.0, y) },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// solidcore#2223: the tabbed plate's R25 rim and one R2 tab corner arc, which share no vertex,
|
||||
/// rounded together at r 0.5 on the plate 2 high (no hole). Each rounds alone; together they
|
||||
/// failed 1001 in cliff_overflow. They take off the sum: the rim alone's 7.958674274526857
|
||||
/// (solidcore#2176's pinned value) and the corner arc's Pappus volume with flat ends,
|
||||
/// 0.15913583972191622 (the spandrel (1 − π/4) r² about the corner's axis at 2 − its centroid
|
||||
/// over a quarter turn), 8.117810114.
|
||||
#[test]
|
||||
fn fillet_tabbed_plate_rim_and_one_corner_arc_together() {
|
||||
let body = extrude(&Plane::XY, &[Region2 { outer: tabbed_plate_outline(), holes: vec![] }], 0.0, 2.0).unwrap().pop().unwrap();
|
||||
let r = 0.5;
|
||||
let (spandrel, centroid) = ((1.0 - PI / 4.0) * r * r, r * (10.0 - 3.0 * PI) / (3.0 * (4.0 - PI)));
|
||||
let corner = spandrel * (PI / 2.0) * (2.0 - centroid);
|
||||
assert!((corner - 0.159_135_839_721_916_22).abs() < 1e-12, "{corner}");
|
||||
let want = 7.958_674_274_526_857 + corner;
|
||||
let picks = [
|
||||
Vec3::new(-24.91935125502191, -2.0064727829797544, 2.0),
|
||||
Vec3::new(24.919351255429795, -2.006472777913911, 2.0),
|
||||
Vec3::new(3.4142135598750105, 32.525324672801695, 2.0),
|
||||
];
|
||||
let blended = fillet(&body, &picks, r).unwrap_or_else(|e| panic!("rim and corner arc together: {e}"));
|
||||
assert!(blended.validity().is_empty(), "{:?}", blended.validity());
|
||||
assert_eq!(blended.face_count(), 12);
|
||||
let got = measure(&body).unwrap().volume - measure(&blended).unwrap().volume;
|
||||
assert!((got - want).abs() < 1e-7, "removed {got}, expected {want}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole() {
|
||||
// The hole's dimensions come from the public reproduction, not an original design file.
|
||||
let theta = (4.0_f64 / 25.0).acos();
|
||||
let y = (625.0_f64 - 16.0).sqrt();
|
||||
let corner_y = 280.0 / 9.0;
|
||||
let hole_y = 262.0 / 9.0;
|
||||
let outline = tabbed_plate_outline();
|
||||
let body =
|
||||
extrude(&Plane::XY, &[Region2 { outer: outline, holes: vec![Loop2::circle(Vec2::new(0.0, hole_y), 1.5)] }], 0.0, 2.0).unwrap().pop().unwrap();
|
||||
let r = 0.5;
|
||||
|
||||
@@ -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,22 +54,9 @@ fn smlib_validity(body: &Body) -> Option<SmlibCheck> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether this build links SMLib (the `smlib` feature).
|
||||
pub fn smlib_built() -> bool {
|
||||
cfg!(feature = "smlib")
|
||||
}
|
||||
|
||||
/// Where every SMLib call that fails from now on leaves its inputs for replay (the C API's
|
||||
/// `smlib_set_dump_dir`: the call's arguments and B-reps, which `smlib_brep_read` reads back);
|
||||
/// `None` stops it. A build without SMLib makes no calls to dump.
|
||||
pub fn smlib_dump_dir(dir: Option<&std::path::Path>) -> crate::Result<()> {
|
||||
#[cfg(feature = "smlib")]
|
||||
{
|
||||
solidcraft_smlib::set_dump_dir(dir).map_err(|e| crate::KernelError::Failed(format!("SMLib dump folder: {e}")))
|
||||
}
|
||||
#[cfg(not(feature = "smlib"))]
|
||||
{
|
||||
let _ = dir;
|
||||
Ok(())
|
||||
}
|
||||
/// Where every kernel call that fails from now on leaves its inputs for replay
|
||||
/// (`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();
|
||||
@@ -57,7 +57,7 @@ fn nested_spans_have_self_and_total_time() {
|
||||
let cats = r.by_category();
|
||||
assert_eq!(cats.iter().map(|c| c.0).collect::<Vec<_>>().len(), 2);
|
||||
let t = r.table(10);
|
||||
assert!(t.contains("test outer") && t.contains("smlib call") && t.contains("self time by category"), "{t}");
|
||||
assert!(t.contains("test outer") && t.contains("external call") && t.contains("self time by category"), "{t}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+4
-25
@@ -1,40 +1,19 @@
|
||||
[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 SMLib. Off, the crate builds without a C++ toolchain or the SMLib sources, and
|
||||
# every call returns `Error::NotBuilt`, so workspace-wide builds and checks need neither.
|
||||
native = []
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
solidcraft-geom = { workspace = true }
|
||||
solidcraft-profile = { 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"] }
|
||||
solidcore = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
solidcraft-profile = { workspace = true, features = ["enabled"] }
|
||||
|
||||
@@ -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, error, ffi};
|
||||
use crate::{Brep, Result, core};
|
||||
|
||||
/// How many of SMLib's tests to run.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -53,22 +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> {
|
||||
let mut buf = vec![0u8; 1 << 16];
|
||||
let (len, failures) = match ffi::check::check(&self.0, 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.0, 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::*;
|
||||
|
||||
|
||||
@@ -0,0 +1,567 @@
|
||||
//! The solidcore backend: one kernel for the process (SMLib's context is process-global too, and
|
||||
//! some of its state, such as the fillet solver's latched step, lives for the whole process),
|
||||
//! every call under its lock and a panic guard.
|
||||
//!
|
||||
//! Upstream fixes ([`fixes`]) are solidcore's opt-in corrections of SMLib defects that the C++
|
||||
//! fork fixed in its own sources; each one turned on here says which fork commit it matches.
|
||||
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::sync::{Mutex, PoisonError};
|
||||
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::kernel::{Kernel, KernelOptions, UpstreamFixes};
|
||||
use solidcore::mass::integrator::properties::{PropertyAttributes, brep_volume};
|
||||
use solidcore::primitive::api_primitives as primitives;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
pub(crate) mod blend;
|
||||
#[cfg(test)]
|
||||
mod blend_tests;
|
||||
pub(crate) mod boolean;
|
||||
#[cfg(test)]
|
||||
mod boolean_tests;
|
||||
mod convert;
|
||||
#[cfg(test)]
|
||||
mod convert_tests;
|
||||
pub(crate) mod dump;
|
||||
pub(crate) mod file;
|
||||
pub(crate) mod history;
|
||||
mod measure;
|
||||
#[cfg(test)]
|
||||
mod measure_tests;
|
||||
pub(crate) mod merge;
|
||||
#[cfg(test)]
|
||||
mod merge_tests;
|
||||
pub(crate) mod offset;
|
||||
#[cfg(test)]
|
||||
mod offset_tests;
|
||||
pub(crate) mod split;
|
||||
#[cfg(test)]
|
||||
mod split_tests;
|
||||
pub(crate) mod ssi;
|
||||
pub(crate) mod sweep;
|
||||
#[cfg(test)]
|
||||
mod sweep_tests;
|
||||
pub(crate) mod transform;
|
||||
#[cfg(test)]
|
||||
mod transform_tests;
|
||||
pub(crate) mod trim;
|
||||
#[cfg(test)]
|
||||
mod trim_tests;
|
||||
|
||||
pub(crate) use convert::{from_arrays, to_arrays};
|
||||
pub(crate) use measure::{area, bounds, check, classify_point, edge_face_distance, is_manifold_solid, mass_properties, solid_count};
|
||||
|
||||
/// A solidcore body. Its own lock: solidcore's operations take `&mut Brep`, even to measure.
|
||||
pub(crate) struct Body(pub(crate) Mutex<solidcore::topology::Brep>);
|
||||
|
||||
impl Body {
|
||||
pub(crate) fn new(b: solidcore::topology::Brep) -> Body {
|
||||
Body(Mutex::new(b))
|
||||
}
|
||||
}
|
||||
|
||||
/// The fixes this backend runs with: every one of solidcore's opt-in fixes of upstream SMLib
|
||||
/// defects (`UpstreamFixes`, each naming its record and issue), whether or not the C++ fork has it,
|
||||
/// unless it appears broken. A test checks none is left off but those in `HELD_BACK`, so moving the
|
||||
/// solidcore pin to a rev with a new flag fails until it is added here.
|
||||
pub(crate) fn fixes() -> UpstreamFixes {
|
||||
let mut f = UpstreamFixes::default();
|
||||
f.fillet_retry_radius_as_first = true;
|
||||
f.fillet_rail_rim_on_face = true;
|
||||
f.fillet_refuse_rails_leaving_faces = true;
|
||||
f.fillet_refuse_face_consuming_blends = true;
|
||||
f.fillet_refuse_non_manifold_result = true;
|
||||
f.fillet_vertex_error_diagnostics = true;
|
||||
f.fillet_corner_ball_patch = true;
|
||||
f.fillet_chamfer_cone_on_arc = true;
|
||||
f.fillet_torus_center_on_axis = true;
|
||||
f.offset_excess_faces_after_merge = true;
|
||||
f.offset_clip_right_side_out = true;
|
||||
f.revolution_offset_axis_both_sides = true;
|
||||
f.offset_extended_edges_mated = true;
|
||||
f.remove_slit_loops_where_solids_touch = true;
|
||||
f.keep_operand_uv_trims = true;
|
||||
f.proto_vertex_miss_is_false = true;
|
||||
f.check_loop_vertex_tolerance = true;
|
||||
f.check_loop_projected_seam_ends = true;
|
||||
f.trim_boundary_seam_wrap = true;
|
||||
f.bspline_check_min_radius = true;
|
||||
f.oscillating_skips_failed_guesses = true;
|
||||
f.refine_cc_range_ends = true;
|
||||
f.flat_corners_test_bits = true;
|
||||
f.cylinder_rebuild_mean_radius = true;
|
||||
f.planar_path_uncapped_open_profiles = true;
|
||||
f.revolve_analytic_profile_faces = true;
|
||||
f.revolve_arc_centre_near_axis_is_sphere = true;
|
||||
f.taper_pivot_nearest_corner = true;
|
||||
f.taper_tangent_corner_planes = true;
|
||||
f.revolve_nested_shells_as_cavities = true;
|
||||
f.revolve_holed_profile_void = true;
|
||||
f.curve_sweep_caps_at_sweep_ends = true;
|
||||
f.join_touching_loops = true;
|
||||
f.uv_trim_reversal_on_surface = true;
|
||||
f.offset_resolve_crossing_loops = true;
|
||||
f.offset_walls_touch_refused = true;
|
||||
f.offset_analytic_mate_edges = true;
|
||||
f.precise_properties_split_at_surface_knots = true;
|
||||
f.nurb_cylinder_inside_out_frame = true;
|
||||
f.shell_inner_shell_of_statuses = true;
|
||||
f.local_operation_offset_status = true;
|
||||
f.local_operation_refuses_loose_tangent_edges = true;
|
||||
f.torus_major_radius_from_origin = true;
|
||||
f.fillet_corner_sphere_from_four_arcs = true;
|
||||
f.fillet_nx1_opposite_continuation_side_edge = true;
|
||||
f.fillet_trace_rejects_short_step = true;
|
||||
f.torus_iso_circle_axis_from_midpoint = true;
|
||||
f.torus_keeps_inside_out = true;
|
||||
f.internal_pole_row_through_knot = true;
|
||||
f.extrusion_origin_scaled_zero = true;
|
||||
f.transform_scales_each_tolerance = true;
|
||||
f.transform_model_size_from_body_box = true;
|
||||
f.offset_collapsed_rounds_mated = true;
|
||||
f.offset_missing_surface_fails = true;
|
||||
f.fillet_nx1_end_rib_global_merge = true;
|
||||
f.uv_trim_corner_end_points_only = true;
|
||||
f.bspline_trim_preserves_uv_spans = true;
|
||||
f.cone_parallel_line_midpoint = true;
|
||||
f.cone_parallel_line_domains = true;
|
||||
f.proto_edge_self_loop_on_vertex_degenerate = true;
|
||||
f.slice_coincident_faces_sheet_side = true;
|
||||
f.boolean_outer_shell_first = true;
|
||||
f.edge_reversal_keeps_failed_edge = true;
|
||||
f.topology_failed_evaluations_propagate = true;
|
||||
f.heal_stage3_failures_propagate = true;
|
||||
f.sweep_frame_on_planar_path_normal = true;
|
||||
f.cover_refuses_folded_continuation = true;
|
||||
f.boolean_vertex_by_best_crossing = true;
|
||||
f.boolean_cleanup_removes_slits = true;
|
||||
f.classify_edge_interval = true;
|
||||
f.ellipse_trim_seam_lines_span_cone_height = true;
|
||||
f.degenerate_proto_edge_in_one_vertex = true;
|
||||
f.new_edge_vertex_tolerance_covers_gap = true;
|
||||
f.torus_signed_gen_curve_interval = true;
|
||||
f.sheet_sweep_joins_profile_wires = true;
|
||||
f.point_solve_keeps_triangle_guess = true;
|
||||
f.fillet_setback_from_start_vertex = true;
|
||||
f.imprint_trims_edge_to_proto_vertex = true;
|
||||
f.imprint_extends_edge_to_proto_vertex = true;
|
||||
f.fillet_point_step_per_point = true;
|
||||
f.analytic_copy_keeps_parameterization = true;
|
||||
f.fillet_point_bigger_step_retry = true;
|
||||
f.fillet_through_extended_rail_end = true;
|
||||
f.precise_properties_body_length_scale = true;
|
||||
f.offset_extension_from_base = true;
|
||||
f.corner_blend_stays_inside_neighbours = true;
|
||||
f.fillet_cleanup_keeps_target_faces = true;
|
||||
f.fillet_nx1_tangent_side_edge = true;
|
||||
f.fillet_3x2_mixed_concave_side_edge = true;
|
||||
f.offset_closed_curve_trim_arc = true;
|
||||
f.offset_stitch_at_body_tolerance = true;
|
||||
f.curve_contacts_at_curve_ends = true;
|
||||
f.shell_empty_cavity_fails = true;
|
||||
f.fillet_refuse_ball_not_fitting = true;
|
||||
f.boolean_lists_virtual_surface_copies = true;
|
||||
f.sphere_small_circle_not_touching = true;
|
||||
f.ssi_tangent_contact_points = true;
|
||||
f.ssi_trace_tangent_curves_once = true;
|
||||
f.ssi_singular_start_inside_direction = true;
|
||||
f.found_torus_sphere_keep_tested_nurb = true;
|
||||
f.boolean_skips_faces_apart = true;
|
||||
f.uv_trim_checks_on_surface = true;
|
||||
f.loop_orientation_floor_from_loop_box = true;
|
||||
f.offset_extends_cones_as_themselves = true;
|
||||
f.local_operation_box_holds_new_surfaces = true;
|
||||
f.uv_trim_joins_drop_pieces = true;
|
||||
f.shell_edges_on_analytics_exact = true;
|
||||
f.ssi_no_reverse_of_empty_trace = true;
|
||||
f.ssi_pole_point_keeps_its_tangent = true;
|
||||
f.iso_curve_sliver_domain_is_degenerate = true;
|
||||
f.corner_blend_coons_patch_tangent_to_sides = true;
|
||||
f.fillet_patch_keeps_fillet_edges = true;
|
||||
f.fillet_corner_ball_center_from_axes = true;
|
||||
f.seam_point_solutions_are_one = true;
|
||||
f.boolean2d_plane_covers_far_faces = true;
|
||||
f.brep_area_between_solid_and_void = true;
|
||||
f.boolean_splits_definition_across_split = true;
|
||||
f.curve_tight_box_interval_knots = true;
|
||||
f.fillet_refuse_overlapping_bands = true;
|
||||
f.ssi_tangent_curve_tracer = true;
|
||||
f.torus_check_reads_v_interval = true;
|
||||
f.torus_check_spindle_apex = true;
|
||||
f.local_operation_step_walls = true;
|
||||
f.edge_check_orders_tangent_faces = true;
|
||||
f.revolve_arc_touching_axis_is_horn_torus = true;
|
||||
f.thicken_planar_side_walls = true;
|
||||
f.fillet_torus_span_from_start_section = true;
|
||||
f.fillet_knife_edge_rounds = true;
|
||||
f.taper_wall_closes_at_apex = true;
|
||||
f.revolution_round_offset_collapses = true;
|
||||
f.fillet_chamfer_cone_stored_as_cone = true;
|
||||
f.offset_point_vertex_caps = true;
|
||||
f.approximate_surface_bezier_starter = true;
|
||||
f.proto_edge_splits_at_captured_vertices = true;
|
||||
f.stitch_solid_nests_cavities = true;
|
||||
f.local_operation_closes_walls_to_nothing = true;
|
||||
f.linear_sweep_arc_to_cylinder_zero = true;
|
||||
f.imprint_reuses_coincident_face_edge = true;
|
||||
f.boolean_new_face_closure_both_sides = true;
|
||||
f.linear_sweep_arc_is_cylinder = true;
|
||||
f.offset_closes_loop_gaps = true;
|
||||
f.cylinder_plane_tangent_within_tolerance = true;
|
||||
f.boolean_wire_edge_keeps_faceuse_shell = true;
|
||||
f.fillet_nx1_tangent_corner_edge = true;
|
||||
f.fillet_torus_rail_on_axis = true;
|
||||
f.loops_go_on_round_the_vertex = true;
|
||||
f.revolve_splits_curves_on_axis = true;
|
||||
f.cover_wraps_across_closed_seam = true;
|
||||
f.imprint_extension_keeps_analytic_curve = true;
|
||||
f.fillet_trace_from_tangent_point_toward_next = true;
|
||||
f.fillet_tangent_point_one_sided_step = true;
|
||||
f.fillet_rails_meet_at_one_end_point = true;
|
||||
f.fillet_trace_reaches_rails = true;
|
||||
f.delete_vertex_joins_seam_iso_curve = true;
|
||||
f.box_face_failures_propagate = true;
|
||||
f.ellipse_adjust_step_interval_keeps_turns = true;
|
||||
f.fillet_cleanup_removes_input_slits = true;
|
||||
f.fillet_rail_end_on_continued_surface = true;
|
||||
f.glue_vertices_joins_every_region = true;
|
||||
f.fillet_nx1_tangent_side_edge_same_convexity = true;
|
||||
f.fillet_removal_takes_ring_fillets = true;
|
||||
// The fork's commits that reached `SMLIB_REV` at eaa998a (tangency-labs/smlib#68, #69, #70).
|
||||
f.fillet_trim_inside_by_best_dot = true;
|
||||
f.fillet_half_degree_crease = true;
|
||||
f.fillet_split_edge_keeps_near_half = true;
|
||||
f.curve_loop_normal_from_area = true;
|
||||
// The fork's commits in `SMLIB_REV` 2fc375f, ported to solidcore since.
|
||||
f.fillet_cliff_overflow = true;
|
||||
f.fillet_3x2_tangent_side_edge_roles = true;
|
||||
f.offset_extension_as_itself = true;
|
||||
f.fillet_mitre_concave_face_corners = true;
|
||||
f.fillet_mitre_gap_patch = true;
|
||||
f.fillet_run_on_past_far_end_only = true;
|
||||
f.good_drop_result_distances_in_order = true;
|
||||
f.face_tight_box_searches_trim = true;
|
||||
f.ssi_low_angle_start_on_probation = true;
|
||||
f.fillet_variable_span_follows_fillet = true;
|
||||
f.simple_coincidence_crossing_is_point = true;
|
||||
f.boolean_temporary_transform_statuses = true;
|
||||
f.primitive_path_curve_statuses = true;
|
||||
f.planar_path_sweep_statuses = true;
|
||||
f.planar_path_profile_placement_statuses = true;
|
||||
f.fillet_trim_status = true;
|
||||
f.perspective_sphere_offsets = true;
|
||||
f.plane_recognition_origin_after_axes = true;
|
||||
f.fillet_corner_face_curve_halves = true;
|
||||
f.brep_edit_statuses_pass_on = true;
|
||||
f.fillet_run_on_bspline_trims = true;
|
||||
f.offset_tangent_gap_statuses = true;
|
||||
f.ssi_alignment_statuses = true;
|
||||
f.fillet_corner_statuses = true;
|
||||
f.fillet_edge_curve_domain_statuses = true;
|
||||
f.query_statuses_pass_on = true;
|
||||
f.sweep_statuses_pass_on = true;
|
||||
f.ssi_proto_topology_statuses_pass_on = true;
|
||||
f.analytic_inversion_answers_apart = true;
|
||||
f.fillet_statuses_pass_on = true;
|
||||
f.fillet_corner_per_vertex_fan = true;
|
||||
f.fillet_rail_trim_at_near_knot = true;
|
||||
f.offset_splits_corner_vertices = true;
|
||||
f.offset_keep_cavity_regions = true;
|
||||
f.offset_four_face_vertex_split = true;
|
||||
f.gap_samples_non_bspline_v_stride = true;
|
||||
f.gap_array_append_updates_longest = true;
|
||||
f.imprint_skips_collapsed_definition = true;
|
||||
f.precise_properties_trim_base_v = true;
|
||||
f.fillet_blend_curve_revolved_on_arc = true;
|
||||
f.temporary_radius_setup_errors = true;
|
||||
f.ellipse_step_interval_checks = true;
|
||||
f.corner_patch_face_side_twist = true;
|
||||
f.boolean_union_refuses_solid_leak = true;
|
||||
f.curve_loop_start_after_previous_end = true;
|
||||
f.loop_use_reports_open_gaps = true;
|
||||
f.face_points_span_domain = true;
|
||||
f.drop_curve_v_domain_degeneracy = true;
|
||||
f.mirror_curve_keeps_euclidean_control_points = true;
|
||||
f.fillet_corner_variable_radius_rail_on_opposite_fillet = true;
|
||||
f.sphere_from_arcs_common_centre = true;
|
||||
f.dupin_bottom_hyperbola_axis = true;
|
||||
f.extent3d_contains_both_segment_endpoints = true;
|
||||
f.translation_sweep_regularize = true;
|
||||
f.gap_samples_use_trimmed_spans = true;
|
||||
f.twist_simple_duw_sign = true;
|
||||
f.offset_swap_uv_extended_distance = true;
|
||||
f.circumscribe_axis_map_by_basis = true;
|
||||
f.planar_path_profile_in_path_frame = true;
|
||||
f.validation_does_not_make_uv_trims = true;
|
||||
f.iso_curve_uv_line_both_params = true;
|
||||
f.extrusion_step_uses_clamped_uv = true;
|
||||
f.analytic_corner_failure_is_false = true;
|
||||
f.defeature_refuses_open_base = true;
|
||||
f.ssi_start_loop_trace_budget = true;
|
||||
f.fillet_refuse_crossing_rails = true;
|
||||
f.proto_edge_split_upper_keeps_the_old_max = true;
|
||||
f.point_on_start_point_squared_tolerance = true;
|
||||
f.bspline_trim_keeps_rational_end_spans = true;
|
||||
f.fillet_exact_round_classes = true;
|
||||
f.curve_check_zero_speed_tangent_end = true;
|
||||
f.cpoly_remove_poly_face_check_member = true;
|
||||
f.check_seam_crossing_unclosed = true;
|
||||
f.ascii_doubles_round_trip = true;
|
||||
f.transform_aligned_bbox_image = true;
|
||||
f.bspline_simple_offset_matches_parameterization = true;
|
||||
f.offset_edges_keep_source_tolerance = true;
|
||||
f.best_fit_line_largest_scatter_eigenvector = true;
|
||||
f.circle_silhouette_perspective_center = true;
|
||||
f.ellipse_convert_to_2d_uses_both_axes = true;
|
||||
f.circle_offset_antiparallel_normal = true;
|
||||
f.masked_array_uses_bit_shift = true;
|
||||
f.global_solver_identical_prefers_larger_when_maximizing = true;
|
||||
f.surface_continuity_checks_last_v_knot = true;
|
||||
f.surface_point_solver_propagates_bbox_failure = true;
|
||||
f.volume_lift_third_derivative_terms = true;
|
||||
f.volume_discontinuity_checks_vw_unaligned = true;
|
||||
f.periodic_extent_add_empty_interval = true;
|
||||
f.stop_tracking_checks_own_callback = true;
|
||||
f.plane_reparameterize_moves_origin = true;
|
||||
f.crv_in_volume_mults_by_break_index = true;
|
||||
f.curve_bounded_surface_read_uv_type_and_implicit_outer = true;
|
||||
f.polar_box_region_to_arc_transposes_domain = true;
|
||||
f.point_set_centroid_all_blocks = true;
|
||||
f.local_solve1d_unbounded_newton_step = true;
|
||||
f.composite_curve_bbox_segment_intervals = true;
|
||||
f.knot_vector_valid_requires_span = true;
|
||||
f.projected_curve_transform_when_owned = true;
|
||||
f.curve_property_gs_branch_bounds = true;
|
||||
f.volume_representational_end_continuities = true;
|
||||
f.integrator_propagates_failure = true;
|
||||
f.precise_properties_span_end_from_below = true;
|
||||
f.offset_open_edge_second_trim_point = true;
|
||||
f.close_loop_relates_only_coincident_edges = true;
|
||||
f.ssi_split_curves_at_singular_points = true;
|
||||
f.surface_parallel_to_vector_checks = true;
|
||||
f.fillet_round_ends_at_tangent_continuation = true;
|
||||
f.geometric_g2_continuity = true;
|
||||
f.hash_table_grow_preserves_removed_records = true;
|
||||
f.bend_volume_bbox_contains_wide_angle_and_inf_u = true;
|
||||
f.bend_volume_trim_param_box_across_period = true;
|
||||
f.point_set_tight_pbox_projects_points = true;
|
||||
f.degen_param_tiebreak_uses_actual_param = true;
|
||||
f.conic_evaluate_uses_computed_second_deriv = true;
|
||||
f.cylinder_plane_section_trims_exact = true;
|
||||
f.local_surface_intersect_guards_max_boundary_retry = true;
|
||||
f.curve_max_distance_single_sample_midpoint = true;
|
||||
f.curve_sweep_closed_path_joins_ends = true;
|
||||
f.validate_tolerances_fixes_reported_gaps = true;
|
||||
f.edgeuse_convexity_propagates_step_off_failure = true;
|
||||
f.mark_lock_pair_advances_and_locks_second_context = true;
|
||||
f.is_iso_curve_at_propagates_canonical_failure = true;
|
||||
f.database_io_byte_swaps_read_boolean_and_u16 = true;
|
||||
f.is_point_on_curve_propagates_eval_failure = true;
|
||||
f.is_topological_vertex_rejects_cusps_and_tangent_errors = true;
|
||||
f.cone_create_canonical_preserves_inside_out_and_checks_height = true;
|
||||
f.point_class_gap_3d_propagates_no_drop_error = true;
|
||||
f.fillet_surface_plane_curve_on_plane_equation = true;
|
||||
f.fillet_extended_rail_end_at_side_edge_crossing = true;
|
||||
f.fillet_gn_section_generator = true;
|
||||
f.face_trim_curves_kept_where_exact = true;
|
||||
f.swap_uv_keeps_analytic_definition = true;
|
||||
f.face_shrink_bound_on_knot = true;
|
||||
f.offset_updates_result_tolerances = true;
|
||||
f.cone_iso_circle_at_axial_height = true;
|
||||
f.boolean_coincident_strip_drops_crossing = true;
|
||||
f.fillet_overflow_trim_at_creases = true;
|
||||
f.fillet_section_end_trim_at_creases = true;
|
||||
f
|
||||
}
|
||||
|
||||
/// The fixes held off, each with the case and the numbers that show it broken: a volume or topology
|
||||
/// wrong against the analytic value, a check failure, a crash, sound geometry refused that builds
|
||||
/// without the flag, or a large slowdown. Differing from the C++ kernel is no such evidence: a fix
|
||||
/// the fork doesn't have stays on, and where solidcore builds what SMLib refuses, a result right
|
||||
/// against its closed form is a win.
|
||||
#[cfg(test)]
|
||||
const HELD_BACK: &[&str] = &[];
|
||||
|
||||
fn fresh() -> Kernel {
|
||||
let mut k = Kernel::new(&KernelOptions::default());
|
||||
k.fixes = fixes();
|
||||
k
|
||||
}
|
||||
|
||||
pub(crate) static KERNEL: Mutex<Option<Kernel>> = Mutex::new(None);
|
||||
|
||||
/// Runs `f` on the process's kernel. A panic in solidcore is caught: the kernel, which it may have
|
||||
/// left half-updated, is replaced by a fresh one and the call fails with [`Error::Internal`].
|
||||
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: where the kernel's time goes.
|
||||
let _span = solidcraft_profile::span("solidcore", call);
|
||||
dump::begin();
|
||||
dump::set_current(Some(call));
|
||||
let r = match catch_unwind(AssertUnwindSafe(|| f(k))) {
|
||||
Ok(Ok(v)) => Ok(v),
|
||||
Ok(Err(e)) => Err(Error::Solidcore(format!("{call}: {e}"))),
|
||||
Err(p) => {
|
||||
*guard = Some(fresh());
|
||||
let why = p.downcast_ref::<&str>().map(|s| (*s).to_owned()).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_default();
|
||||
log::warn!(target: "smlib", "solidcore panicked in {call}: {why}");
|
||||
Err(Error::Internal(format!("{call}: {why}")))
|
||||
}
|
||||
};
|
||||
dump::set_current(None);
|
||||
match &r {
|
||||
Ok(v) => {
|
||||
if let Some(why) = v.refusal() {
|
||||
dump::failed(call, &why);
|
||||
}
|
||||
}
|
||||
Err(e) => dump::failed(call, &e.to_string()),
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// A call the capi checks for `SM_SUCCESS`, treating any other status as no answer: an error
|
||||
/// that stands for an SMLib status exactly ([`SolidError::sm_status`](solidcore::SolidError::sm_status),
|
||||
/// solidcore#1452) is `None`. Every other solidcore error
|
||||
/// (unported code, invalid input to an adapter, a missing record...) is not a status the C++
|
||||
/// path returns, so it fails the call instead of hiding as a skipped entity.
|
||||
pub(crate) fn answer<T>(r: std::result::Result<T, solidcore::SolidError>) -> std::result::Result<Option<T>, solidcore::SolidError> {
|
||||
match r {
|
||||
Ok(v) => Ok(Some(v)),
|
||||
Err(e) if e.sm_status().is_some() => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `f` on the kernel and `body`, locking the kernel first.
|
||||
pub(crate) fn with_body<T: dump::Answer>(
|
||||
call: &'static str,
|
||||
body: &Body,
|
||||
f: impl FnOnce(&mut Kernel, &mut solidcore::topology::Brep) -> std::result::Result<T, solidcore::SolidError>,
|
||||
) -> Result<T> {
|
||||
// A body a panic interrupted may be half-edited: it is never used again.
|
||||
if body.0.is_poisoned() {
|
||||
return Err(Error::Internal(format!("{call}: the body was left unfinished by an earlier panic")));
|
||||
}
|
||||
run(call, |k| {
|
||||
let mut b = body.0.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
dump::input(k, "brep", &b);
|
||||
f(k, &mut b)
|
||||
})
|
||||
}
|
||||
|
||||
/// An axis-aligned box from its lower corner (`SmApiCreateBox`, as `smlib_make_box` checks it:
|
||||
/// every edge length positive).
|
||||
pub(crate) fn make_box(origin: [f64; 3], size: [f64; 3]) -> Result<Body> {
|
||||
if !origin.iter().chain(&size).all(|x| x.is_finite()) || !size.iter().all(|&s| s > 0.0) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let [x, y, z] = size;
|
||||
run("create_box", |k| primitives::create_box(k, Vec3::new(origin[0], origin[1], origin[2]), x, y, z)).map(Body::new)
|
||||
}
|
||||
|
||||
fn point(p: [f64; 3]) -> Result<Vec3> {
|
||||
if !p.iter().all(|x| x.is_finite()) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Vec3::new(p[0], p[1], p[2]))
|
||||
}
|
||||
|
||||
/// A cylinder about +Z from its base centre (`SmApiCreateCylinder`, as `smlib_make_cylinder`
|
||||
/// checks it: radius and height positive).
|
||||
pub(crate) fn make_cylinder(origin: [f64; 3], radius: f64, height: f64) -> Result<Body> {
|
||||
let o = point(origin)?;
|
||||
if !(radius > 0.0 && height > 0.0 && radius.is_finite() && height.is_finite()) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
run("create_cylinder", |k| primitives::create_cylinder(k, o, radius, height)).map(Body::new)
|
||||
}
|
||||
|
||||
/// A sphere (`SmApiCreateSphere`; radius positive).
|
||||
pub(crate) fn make_sphere(center: [f64; 3], radius: f64) -> Result<Body> {
|
||||
let o = point(center)?;
|
||||
if !(radius > 0.0 && radius.is_finite()) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
run("create_sphere", |k| primitives::create_sphere(k, o, radius)).map(Body::new)
|
||||
}
|
||||
|
||||
/// A cone or frustum about +Z (`SmApiCreateCone`; `smlib_make_cone`'s checks: base radius and
|
||||
/// height positive, top radius at least 0 and not the base radius).
|
||||
pub(crate) fn make_cone(origin: [f64; 3], base_radius: f64, top_radius: f64, height: f64) -> Result<Body> {
|
||||
let o = point(origin)?;
|
||||
let ok = base_radius > 0.0 && top_radius >= 0.0 && height > 0.0 && top_radius != base_radius;
|
||||
if !(ok && base_radius.is_finite() && top_radius.is_finite() && height.is_finite()) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
run("create_cone", |k| primitives::create_cone(k, o, base_radius, top_radius, height)).map(Body::new)
|
||||
}
|
||||
|
||||
/// A ring torus about +Z (`SmApiCreateTorus`; major radius > minor radius > 0).
|
||||
pub(crate) fn make_torus(center: [f64; 3], major_radius: f64, minor_radius: f64) -> Result<Body> {
|
||||
let o = point(center)?;
|
||||
if !(minor_radius > 0.0 && minor_radius < major_radius && major_radius.is_finite()) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
run("create_torus", |k| primitives::create_torus(k, o, major_radius, minor_radius)).map(Body::new)
|
||||
}
|
||||
|
||||
/// The enclosed volume (`SmApiBrepComputeVolume`).
|
||||
pub(crate) fn volume(body: &Body, relative_accuracy: f64) -> Result<f64> {
|
||||
if !relative_accuracy.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
with_body("brep_volume", body, |k, b| {
|
||||
let store = AttributeStore::default();
|
||||
let attrs = PropertyAttributes { store: &store, key_of: &|_| ObjectKey(0) };
|
||||
brep_volume(k, b, relative_accuracy, &attrs)
|
||||
})
|
||||
}
|
||||
|
||||
/// Face, edge and vertex counts.
|
||||
pub(crate) fn counts(body: &Body) -> Result<[usize; 3]> {
|
||||
with_body("counts", body, |_, b| Ok([b.face_list().len(), b.edge_list().len(), b.vertex_list().len()]))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_upstream_fix_is_on_unless_shown_broken() {
|
||||
let f = fixes();
|
||||
let flags: Vec<(&str, bool)> = f.flags().collect();
|
||||
let off: Vec<&str> = flags.iter().filter(|(name, on)| !on && !HELD_BACK.contains(name)).map(|(name, _)| *name).collect();
|
||||
assert!(off.is_empty(), "UpstreamFixes flags are off: {off:?}");
|
||||
for name in HELD_BACK {
|
||||
assert!(flags.contains(&(*name, false)), "{name} is on or gone: drop it from HELD_BACK");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_box_measures_exactly() {
|
||||
let b = make_box([1.0, -2.0, 3.0], [10.0, 20.0, 30.0]).unwrap();
|
||||
assert_eq!(counts(&b).unwrap(), [6, 12, 8]);
|
||||
let v = volume(&b, 1e-8).unwrap();
|
||||
assert!((v - 6000.0).abs() < 6000.0 * 1e-9, "{v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_numbers_are_refused() {
|
||||
assert!(make_box([f64::NAN, 0.0, 0.0], [1.0; 3]).is_err());
|
||||
assert!(make_box([0.0; 3], [1.0, -1.0, 1.0]).is_err());
|
||||
let b = make_box([0.0; 3], [1.0; 3]).unwrap();
|
||||
assert!(volume(&b, f64::INFINITY).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_panic_is_an_error_and_the_kernel_is_fresh_after() {
|
||||
let r: Result<()> = run("test", |_| panic!("boom"));
|
||||
assert!(matches!(r, Err(Error::Internal(ref m)) if m.contains("boom")), "{r:?}");
|
||||
assert!(make_box([0.0; 3], [1.0; 3]).is_ok());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,859 @@
|
||||
//! Fillets and chamfers on solidcore, against closed forms: every result exports to a record that
|
||||
//! validates.
|
||||
//!
|
||||
//! The bodies are solidcore's own boxes and cylinders.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::f64::consts::PI;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::blend::{Corners, blend, blend_dropping, blend_holes, chamfer_sides, fillet_variable};
|
||||
use super::boolean::boolean;
|
||||
use super::{Body, counts, make_box, make_cylinder, to_arrays, volume};
|
||||
use crate::export::native;
|
||||
use crate::{Blend, BooleanOp, ChamferSecond, Error};
|
||||
|
||||
/// A case longer than this is a hang.
|
||||
const TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
/// How near a pick point must be.
|
||||
const TOL: f64 = 0.01;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Shape {
|
||||
Box([f64; 3], [f64; 3]),
|
||||
Cylinder([f64; 3], f64, f64),
|
||||
/// A box (corner, size) less a cylinder along z (base centre, radius, height).
|
||||
Holed([f64; 3], [f64; 3], [f64; 3], f64, f64),
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
fn core(self) -> Body {
|
||||
match self {
|
||||
Shape::Box(o, s) => make_box(o, s),
|
||||
Shape::Cylinder(o, r, h) => make_cylinder(o, r, h),
|
||||
Shape::Holed(o, s, c, r, h) => {
|
||||
boolean(&make_box(o, s).unwrap(), &make_cylinder(c, r, h).unwrap(), BooleanOp::Subtract).map(|b| b.unwrap())
|
||||
}
|
||||
}
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Op {
|
||||
/// `Brep::blend` of one kind, one size for every edge.
|
||||
Blend(Shape, Blend, Vec<[f64; 3]>, f64),
|
||||
/// `Brep::chamfer_sides`.
|
||||
Sides(Shape, Vec<[f64; 3]>, f64, ChamferSecond, [f64; 3]),
|
||||
/// `Brep::blend_setback` of rounds: edges, radius, corners, setbacks.
|
||||
Setback(Shape, Vec<[f64; 3]>, f64, Vec<[f64; 3]>, Vec<f64>),
|
||||
/// `Brep::fillet_variable`: edge point, start, degree, knots, radii.
|
||||
Variable(Shape, [f64; 3], [f64; 3], usize, Vec<f64>, Vec<f64>),
|
||||
}
|
||||
|
||||
/// What a case must give.
|
||||
#[derive(Clone, Debug)]
|
||||
enum Expect {
|
||||
/// A body of this volume (relative 1e-9).
|
||||
Volume(f64),
|
||||
/// A body of this volume to this relative accuracy.
|
||||
Near(f64, f64),
|
||||
/// A body whose volume lies strictly between these.
|
||||
Between(f64, f64),
|
||||
/// This error.
|
||||
Fails(Error),
|
||||
}
|
||||
|
||||
struct Case {
|
||||
op: Op,
|
||||
expect: Expect,
|
||||
}
|
||||
|
||||
/// A 30 x 20 x 10 block from the origin.
|
||||
const BLOCK: Shape = Shape::Box([0.0; 3], [30.0, 20.0, 10.0]);
|
||||
const ABC: f64 = 6000.0;
|
||||
/// Its top front edge, along x at y = 0, z = 10, 30 long.
|
||||
const TOP_FRONT: [f64; 3] = [15.0, 0.0, 10.0];
|
||||
|
||||
/// The cross-section a round of radius r takes from a right-angled edge: (1 - π/4) r².
|
||||
fn spandrel(r: f64) -> f64 {
|
||||
(1.0 - PI / 4.0) * r * r
|
||||
}
|
||||
|
||||
fn case(name: &str) -> Case {
|
||||
let c = |op, expect| Case { op, expect };
|
||||
let r = 2.0;
|
||||
match name {
|
||||
// abc - (1 - π/4) r² L.
|
||||
"fillet_one_edge" => c(Op::Blend(BLOCK, Blend::Round, vec![TOP_FRONT], r), Expect::Volume(ABC - spandrel(r) * 30.0)),
|
||||
// Every edge rounded at r, the corners spheres: (a-2r)(b-2r)(c-2r) + 2r Σ(faces less
|
||||
// 2r) + πr² Σ(edges less 2r) + 4/3 πr³.
|
||||
"fillet_all_edges" => {
|
||||
let (a, b, h) = (30.0 - 2.0 * r, 20.0 - 2.0 * r, 10.0 - 2.0 * r);
|
||||
let v = a * b * h + 2.0 * r * (a * b + b * h + h * a) + PI * r * r * (a + b + h) + 4.0 / 3.0 * PI * r * r * r;
|
||||
let mut edges = Vec::new();
|
||||
for (y, z) in [(0.0, 0.0), (20.0, 0.0), (0.0, 10.0), (20.0, 10.0)] {
|
||||
edges.push([15.0, y, z]);
|
||||
}
|
||||
for (x, z) in [(0.0, 0.0), (30.0, 0.0), (0.0, 10.0), (30.0, 10.0)] {
|
||||
edges.push([x, 10.0, z]);
|
||||
}
|
||||
for (x, y) in [(0.0, 0.0), (30.0, 0.0), (0.0, 20.0), (30.0, 20.0)] {
|
||||
edges.push([x, y, 5.0]);
|
||||
}
|
||||
c(Op::Blend(BLOCK, Blend::Round, edges, r), Expect::Volume(v))
|
||||
}
|
||||
// The curvature section's default thumbweight is equal area (fork 6148773): on the
|
||||
// round's rails it takes off what the ball's circle does, abc - (1 - π/4) r² L.
|
||||
"fillet_curvature_one_edge" => {
|
||||
c(Op::Blend(BLOCK, Blend::Curvature, vec![TOP_FRONT], r), Expect::Volume(ABC - (1.0 - PI / 4.0) * r * r * 30.0))
|
||||
}
|
||||
// abc - d² L / 2.
|
||||
"chamfer_one_edge" => c(Op::Blend(BLOCK, Blend::Chamfer, vec![TOP_FRONT], r), Expect::Volume(ABC - r * r * 30.0 / 2.0)),
|
||||
// abc - d1 d2 L / 2: 2 back along the top (the face leaning most along +z), 3 down the
|
||||
// front.
|
||||
"chamfer_two_distances" => {
|
||||
c(Op::Sides(BLOCK, vec![TOP_FRONT], 2.0, ChamferSecond::Distance(3.0), [0.0, 0.0, 1.0]), Expect::Volume(ABC - 2.0 * 3.0 * 30.0 / 2.0))
|
||||
}
|
||||
// 2 back along the top, at 30° to it: d tan 30° down the front.
|
||||
"chamfer_distance_angle" => c(
|
||||
Op::Sides(BLOCK, vec![TOP_FRONT], 2.0, ChamferSecond::Angle(PI / 6.0), [0.0, 0.0, 1.0]),
|
||||
Expect::Volume(ABC - 2.0 * 2.0 * (PI / 6.0).tan() * 30.0 / 2.0),
|
||||
),
|
||||
// A 5 mm cylinder 10 high, its top rim rounded at 1. Pappus: the spandrel (1 - π/4) r²,
|
||||
// its centroid (10 - 3π)/(12 - 3π) r in from the rim, swept round the axis.
|
||||
"fillet_cylinder_rim" => {
|
||||
let (big, h, r) = (5.0, 10.0, 1.0);
|
||||
let inset = (10.0 - 3.0 * PI) / (12.0 - 3.0 * PI) * r;
|
||||
c(
|
||||
Op::Blend(Shape::Cylinder([0.0; 3], big, h), Blend::Round, vec![[0.0, big, h]], r),
|
||||
Expect::Volume(PI * big * big * h - 2.0 * PI * (big - inset) * spandrel(r)),
|
||||
)
|
||||
}
|
||||
// The three edges at the corner (30, 20, 10) rounded at r, the corner set back 4 along
|
||||
// each. Without a setback the corner is a sphere's octant, and the block keeps
|
||||
// abc - (1 - π/4) r² Σ(L - r) - (1 - π/6) r³; the setback changes only what lies within
|
||||
// s + r of the corner, a cube of side s + r.
|
||||
"setback_corner" => {
|
||||
let s = 4.0;
|
||||
let plain = ABC - spandrel(r) * (30.0 + 20.0 + 10.0 - 3.0 * r) - (1.0 - PI / 6.0) * r * r * r;
|
||||
let near = (s + r) * (s + r) * (s + r);
|
||||
c(
|
||||
Op::Setback(BLOCK, vec![[15.0, 20.0, 10.0], [30.0, 10.0, 10.0], [30.0, 20.0, 5.0]], r, vec![[30.0, 20.0, 10.0]], vec![s]),
|
||||
Expect::Between(plain - near, plain + near),
|
||||
)
|
||||
}
|
||||
// The top front edge rounded from 1 at x = 0 to 3 at x = 30, linearly, each section
|
||||
// square to the edge a quarter circle: abc - (1 - π/4) ∫ r(x)² dx
|
||||
// = abc - (1 - π/4) L (r0² + r0 r1 + r1²) / 3. The round is a B-spline surface fitted to
|
||||
// those sections, which both kernels build 2.5e-7 short of it (and within 1e-9 of each
|
||||
// other), within the solver's fitting tolerance.
|
||||
// The union of the balls, r from 1 to 3 along the 30 mm edge: in each plane across the
|
||||
// edge, the area between the corner and where each ray from it first enters a ball
|
||||
// (Gauss-Legendre in the ray's angle, Simpson along the edge, converged to 1e-8 of the
|
||||
// body), 27.857434; the balls' great circles would take (1 - π/4) L (r0² + r0 r1 + r1²) / 3,
|
||||
// 27.898239.
|
||||
"fillet_variable_linear" => {
|
||||
c(Op::Variable(BLOCK, TOP_FRONT, [0.0, 0.0, 10.0], 1, vec![0.0, 0.0, 1.0, 1.0], vec![1.0, 3.0]), Expect::Near(ABC - 27.857434, 1e-6))
|
||||
}
|
||||
// A round of 12 on a 10 mm cube's top front edge is larger than either face beside it:
|
||||
// the fork builds it over the cliffs, on the faces' continuations, and cuts away all of
|
||||
// the cube outside the round's cylinder (axis y = 12, z = -2, radius 12). What stays is
|
||||
// 10 times the square's part inside that circle, ∫ (√(144 - u²) - 2) du over
|
||||
// u = y - 12 from -√140 to -2: 72 (asin(√140/12) - asin(1/6)) - 2 (√140 - 2).
|
||||
"fillet_over_a_cliff" => {
|
||||
let w = 140f64.sqrt();
|
||||
let area = 72.0 * ((w / 12.0).asin() - (1.0f64 / 6.0).asin()) - 2.0 * (w - 2.0);
|
||||
c(Op::Blend(Shape::Box([0.0; 3], [10.0; 3]), Blend::Round, vec![[5.0, 0.0, 10.0]], 12.0), Expect::Volume(10.0 * area))
|
||||
}
|
||||
// A round of 6 on the top rim of a cylinder of radius 5 is wider than the cylinder.
|
||||
"fillet_too_large" => {
|
||||
c(Op::Blend(Shape::Cylinder([0.0; 3], 5.0, 10.0), Blend::Round, vec![[0.0, 5.0, 10.0]], 6.0), Expect::Fails(Error::NoRoom))
|
||||
}
|
||||
"pick_near_nothing" => {
|
||||
c(Op::Blend(BLOCK, Blend::Round, vec![TOP_FRONT, [100.0, 100.0, 100.0]], r), Expect::Fails(Error::NoMatch { point: 1 }))
|
||||
}
|
||||
// A 20 mm cube with a hole of radius 5 along z whose wall touches the face y = 0 along
|
||||
// x = 10 (#1390): a 3 mm chamfer of the hole's top rim runs out of that face where the
|
||||
// rim meets it tangentially, a vertex no blend corner handles. The tangent chain the app
|
||||
// blends is picked: the rim at 135° and 315° round the hole (away from the cylinder's
|
||||
// seam at 0° and the face at 270°), and the two pieces of the top's front edge, which the
|
||||
// rim runs on tangentially into at (10, 0, 20).
|
||||
"chamfer_hole_rim_touching_a_face" => {
|
||||
let d = 5.0 * std::f64::consts::FRAC_1_SQRT_2;
|
||||
c(
|
||||
Op::Blend(
|
||||
Shape::Holed([0.0; 3], [20.0; 3], [10.0, 5.0, -1.0], 5.0, 22.0),
|
||||
Blend::Chamfer,
|
||||
vec![[10.0 - d, 5.0 + d, 20.0], [10.0 + d, 5.0 - d, 20.0], [5.0, 0.0, 20.0], [15.0, 0.0, 20.0]],
|
||||
3.0,
|
||||
),
|
||||
Expect::Fails(Error::CornerUnsupported),
|
||||
)
|
||||
}
|
||||
// √3 from all three edges at the corner (0, 0, 10).
|
||||
"pick_ambiguous" => c(Op::Blend(BLOCK, Blend::Round, vec![[-1.0, -1.0, 11.0]], r), Expect::Fails(Error::Ambiguous { point: 0 })),
|
||||
_ => panic!("{name}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The tolerance a case picks with: the ambiguous point lies √3 from its edges.
|
||||
fn tolerance(name: &str) -> f64 {
|
||||
if name == "pick_ambiguous" { 5.0 } else { TOL }
|
||||
}
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Measured {
|
||||
volume: f64,
|
||||
counts: [usize; 3],
|
||||
kinds: BTreeMap<String, usize>,
|
||||
}
|
||||
|
||||
fn kinds(rec: &solidcraft_geom::brep::Brep) -> BTreeMap<String, usize> {
|
||||
let mut out = BTreeMap::new();
|
||||
for f in &rec.faces {
|
||||
let v = serde_json::to_value(&f.surface).unwrap();
|
||||
*out.entry(v["kind"].as_str().unwrap_or("?").to_owned()).or_default() += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn on_core(name: &str, c: &Case) -> crate::Result<Body> {
|
||||
let tol = tolerance(name);
|
||||
match &c.op {
|
||||
Op::Blend(s, kind, points, size) => blend(&s.core(), *kind, points, &[*size], tol, &[], &[], Corners::Filled),
|
||||
Op::Sides(s, points, d, second, first) => chamfer_sides(&s.core(), points, tol, *d, *second, *first),
|
||||
Op::Setback(s, points, size, corners, setbacks) => {
|
||||
blend(&s.core(), Blend::Round, points, &vec![*size; points.len()], tol, corners, setbacks, Corners::Filled)
|
||||
}
|
||||
Op::Variable(s, point, start, degree, knots, radii) => fillet_variable(&s.core(), *point, tol, *start, *degree, knots, radii),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `f` on its own thread: a hang fails the test instead of stopping the run.
|
||||
fn timed<T: Send + 'static>(name: &str, f: impl FnOnce() -> T + Send + 'static) -> T {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let worker = std::thread::spawn(move || {
|
||||
let _ = tx.send(f());
|
||||
});
|
||||
match rx.recv_timeout(TIMEOUT) {
|
||||
Ok(v) => v,
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => match worker.join() {
|
||||
Err(p) => std::panic::resume_unwind(p),
|
||||
Ok(()) => panic!("{name}: no result"),
|
||||
},
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => panic!("{name}: solidcore hung (no result after {TIMEOUT:?})"),
|
||||
}
|
||||
}
|
||||
|
||||
type Got = std::result::Result<Measured, Error>;
|
||||
|
||||
/// Checks a kernel's result against the case's closed form; what it measured, or its error.
|
||||
fn judge(name: &str, who: &str, expect: &Expect, got: Got) -> Got {
|
||||
match (expect, &got) {
|
||||
(Expect::Fails(e), Err(g)) => assert_eq!(g, e, "{name}: {who} failed otherwise"),
|
||||
(Expect::Fails(e), Ok(m)) => panic!("{name}: {who} gave {m:?}, expected {e:?}"),
|
||||
(_, Err(g)) => panic!("{name}: {who} failed: {g}"),
|
||||
(Expect::Volume(v), Ok(m)) => assert!(rel(m.volume, *v) < 1e-9, "{name}: {who} volume {} vs analytic {v}", m.volume),
|
||||
(Expect::Near(v, within), Ok(m)) => assert!(rel(m.volume, *v) < *within, "{name}: {who} volume {} vs analytic {v}", m.volume),
|
||||
(Expect::Between(lo, hi), Ok(m)) => assert!(*lo < m.volume && m.volume < *hi, "{name}: {who} volume {} not in ({lo}, {hi})", m.volume),
|
||||
}
|
||||
got
|
||||
}
|
||||
|
||||
fn solidcore_case(name: &'static str) -> Got {
|
||||
timed(name, move || {
|
||||
let c = case(name);
|
||||
let got = on_core(name, &c).map(|body| {
|
||||
let v = volume(&body, 1e-8).unwrap_or_else(|e| panic!("{name}: volume: {e}"));
|
||||
let n = counts(&body).unwrap();
|
||||
let (rec, _) = native(&to_arrays(&body).unwrap()).unwrap_or_else(|e| panic!("{name}: record: {e:?}"));
|
||||
rec.validate().unwrap_or_else(|e| panic!("{name}: record: {e}"));
|
||||
Measured { volume: v, counts: n, kinds: kinds(&rec) }
|
||||
});
|
||||
judge(name, "solidcore", &c.expect, got)
|
||||
})
|
||||
}
|
||||
|
||||
macro_rules! cases {
|
||||
($($m:ident $(: $ign:meta)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
#[test]
|
||||
$(#[$ign])?
|
||||
fn solidcore_matches_the_closed_form() {
|
||||
let _ = super::solidcore_case(stringify!($m));
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
cases!(
|
||||
fillet_one_edge,
|
||||
fillet_all_edges,
|
||||
fillet_curvature_one_edge,
|
||||
chamfer_one_edge,
|
||||
chamfer_two_distances,
|
||||
chamfer_distance_angle,
|
||||
fillet_cylinder_rim,
|
||||
setback_corner,
|
||||
fillet_variable_linear,
|
||||
fillet_over_a_cliff,
|
||||
fillet_too_large,
|
||||
chamfer_hole_rim_touching_a_face,
|
||||
pick_near_nothing,
|
||||
pick_ambiguous,
|
||||
);
|
||||
|
||||
/// What solidcore does not have yet is refused as not supported, after the capi's own checks.
|
||||
#[test]
|
||||
fn fork_only_blends_are_not_supported_yet() {
|
||||
let r = timed("unsupported", || {
|
||||
let b = BLOCK.core();
|
||||
// 1 to 2 and back to 1: a corner at the middle.
|
||||
let bent = fillet_variable(&b, TOP_FRONT, TOL, [0.0, 0.0, 10.0], 1, &[0.0, 0.0, 0.5, 1.0, 1.0], &[1.0, 2.0, 1.0]).err();
|
||||
// A pick that fails still says so first.
|
||||
let far = chamfer_sides(&b, &[[100.0; 3]], TOL, 2.0, ChamferSecond::Distance(3.0), [0.0, 0.0, 1.0]).err();
|
||||
(bent, far)
|
||||
});
|
||||
assert_eq!(r.0, Some(Error::Unsupported));
|
||||
assert_eq!(r.1, Some(Error::NoMatch { point: 0 }));
|
||||
}
|
||||
|
||||
/// A cliff overflow the fork refuses too (`fixes.fillet_cliff_overflow`, solidcore#960): the
|
||||
/// three edges at a 20 mm cube's corner rounded r20, their fillet's boundary on two faces'
|
||||
/// surfaces, has no room, as on the fork, not "not supported".
|
||||
#[test]
|
||||
fn a_cliff_overflow_the_fork_refuses_has_no_room() {
|
||||
let r = timed("cliff_refused", || {
|
||||
let corner = [[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]];
|
||||
blend(&Shape::Box([0.0; 3], [20.0; 3]).core(), Blend::Round, &corner, &[20.0; 3], TOL, &[], &[], Corners::Filled).err()
|
||||
});
|
||||
assert_eq!(r, Some(Error::NoRoom));
|
||||
}
|
||||
|
||||
/// The capi's `overflow_goes_first` (fork 241f7ba): R21 on a 20 cube's edge takes all of both faces
|
||||
/// that edge lies on, and rounds with it on other edges go after it. With R5 on the corner's other
|
||||
/// two edges those edges are gone (the two R5 points flagged) and the result is the R21's alone,
|
||||
/// 20 (F(w) - F(1) - (w - 1)) with F the integral of sqrt(441 - u²) and w = sqrt(440). With R5 on
|
||||
/// the back upright that upright's round runs above the R21 cylinder, taking the corner's piece
|
||||
/// over it too (Simpson on 4000 spans, against a bound of its quartic end edge's tolerance).
|
||||
#[test]
|
||||
fn overflow_goes_first() {
|
||||
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));
|
||||
let strip = |t: f64| {
|
||||
let y = 15.0 + 5.0 * t.sin();
|
||||
let zc = 21.0 - (441.0 - (y - 21.0) * (y - 21.0)).sqrt();
|
||||
5.0 * (1.0 - t.cos()) * (20.0 - zc) * 5.0 * t.cos()
|
||||
};
|
||||
let (n, h) = (4000, 0.5 * PI / 4000.0);
|
||||
let mut sum = strip(0.0) + strip(0.5 * PI);
|
||||
for i in 1..n {
|
||||
sum += f64::from(if i % 2 == 1 { 4 } else { 2 }) * strip(f64::from(i) * h);
|
||||
}
|
||||
let corner = sum * h / 3.0;
|
||||
assert!((corner - 106.612_300_588).abs() < 1e-6, "{corner}");
|
||||
let cases: [(&[[f64; 3]], &[f64], &[bool], f64, f64); 2] = [
|
||||
(&[[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]], &[21.0, 5.0, 5.0], &[false, true, true], alone, 1e-9 * alone),
|
||||
(&[[10.0, 0.0, 0.0], [0.0, 20.0, 10.0]], &[21.0, 5.0], &[false, false], alone - corner, 0.02),
|
||||
];
|
||||
for (points, sizes, dropped, want, bound) in cases {
|
||||
let got = timed("overflow_first", move || {
|
||||
let (b, d) = blend_dropping(&Shape::Box([0.0; 3], [20.0; 3]).core(), Blend::Round, points, sizes, 1.0).unwrap();
|
||||
native(&to_arrays(&b).unwrap()).unwrap();
|
||||
(volume(&b, 1e-8).unwrap(), d)
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// The capi's `rounds_at_a_concave_face_corner_are_mitred` (`fixes.fillet_mitre_concave_face_corners`,
|
||||
/// fork 4f06e72): each edge loses (convex) or gains (concave) the spandrel r²(1 - π/4) along its
|
||||
/// length, and in the quadrant beyond both walls the mitred rounds take or add r³(5/3 - π/2): a
|
||||
/// 60 x 40 x 10 plate's 30 x 20 x 5 pocket rim at r2 (15 faces) and r5 (11), a 60 x 60 x 5 plate's
|
||||
/// 20 x 20 x 15 boss root at r4 (15). And the pocket rim chamfered 5.5, past the pocket's depth:
|
||||
/// 21000 - (50 d² + 4/3 d³).
|
||||
#[test]
|
||||
fn rounds_at_a_concave_face_corner_are_mitred() {
|
||||
let made = |a: Body, b: Body, op| boolean(&a, &b, op).unwrap().unwrap();
|
||||
let pocket =
|
||||
|| made(make_box([0.0; 3], [60.0, 40.0, 10.0]).unwrap(), make_box([15.0, 10.0, 5.0], [30.0, 20.0, 6.0]).unwrap(), BooleanOp::Subtract);
|
||||
let boss = || made(make_box([0.0; 3], [60.0, 60.0, 5.0]).unwrap(), make_box([20.0, 20.0, 5.0], [20.0, 20.0, 15.0]).unwrap(), BooleanOp::Union);
|
||||
let rim = [[30.0, 10.0, 10.0], [30.0, 30.0, 10.0], [15.0, 20.0, 10.0], [45.0, 20.0, 10.0]];
|
||||
let root = [[30.0, 20.0, 5.0], [40.0, 30.0, 5.0], [30.0, 40.0, 5.0], [20.0, 30.0, 5.0]];
|
||||
let want = |v0: f64, length: f64, r: f64, sign: f64| v0 + sign * (length * r * r * (1.0 - PI / 4.0) + 4.0 * r * r * r * (5.0 / 3.0 - PI / 2.0));
|
||||
for (body, edges, r, v, faces) in [
|
||||
(pocket(), rim, 2.0, want(21000.0, 100.0, 2.0, -1.0), 15),
|
||||
(pocket(), rim, 5.0, want(21000.0, 100.0, 5.0, -1.0), 11),
|
||||
(boss(), root, 4.0, want(24000.0, 80.0, 4.0, 1.0), 15),
|
||||
] {
|
||||
let b = blend(&body, Blend::Round, &edges, &[r], 0.1, &[], &[], Corners::Filled).unwrap();
|
||||
native(&to_arrays(&b).unwrap()).unwrap();
|
||||
let got = volume(&b, 1e-12).unwrap();
|
||||
println!("mitred r{r}: {} faces, {got:.9}, expected {v:.9}", counts(&b).unwrap()[0]);
|
||||
assert_eq!(counts(&b).unwrap()[0], faces, "r{r}");
|
||||
assert!(rel(got, v) < 2e-8, "r{r}: {got} vs {v}");
|
||||
}
|
||||
// Through the flat arrays, as bodies reach a blend in the app (pocket_chamfer_on_a_boolean_body).
|
||||
let d = 5.5;
|
||||
let flat = super::from_arrays(&to_arrays(&pocket()).unwrap()).unwrap();
|
||||
let b = blend(&flat, Blend::Chamfer, &rim, &[d], 0.1, &[], &[], Corners::Filled).unwrap();
|
||||
let (got, v) = (volume(&b, 1e-8).unwrap(), 21000.0 - (50.0 * d * d + 4.0 / 3.0 * d * d * d));
|
||||
println!("mitred chamfer {d}: {got:.9}, expected {v:.9}");
|
||||
assert!(rel(got, v) < 1e-8, "{got} vs {v}");
|
||||
}
|
||||
|
||||
/// Solidcore alone: edge attributes across a cliff overflow. A 40 x 30 x 10 block notched below its
|
||||
/// top at (40, 0), its front and right top edges rounded r8 (their rails run off over the notch).
|
||||
/// The back and left top edges are each trimmed 8 by those rounds: (0, 30)-(32, 30) and (0, 8)-(0, 30)
|
||||
/// at z = 10, and each piece keeps its edge's attribute (`Behavior::Copy`), as on the fork.
|
||||
#[test]
|
||||
fn an_edge_tag_crosses_a_cliff_overflow() {
|
||||
use solidcore::blend::fillet_executive::session::FilletSession;
|
||||
use solidcore::boolean::api_brep::boolean_difference;
|
||||
use solidcore::geom::attribute::{Attribute, Behavior};
|
||||
use solidcore::primitive::api_primitives::create_box;
|
||||
use solidcore::topology::{Brep, EdgeId, edge, vertex};
|
||||
use solidcore::{EntityRef, Kernel, Point3};
|
||||
fn between(b: &Brep, a: Point3, q: Point3) -> Option<EdgeId> {
|
||||
b.edges().ids().find(|&e| {
|
||||
let (s, t) = edge::start_end_vertices(b, e).unwrap();
|
||||
let (s, t) = (vertex::point(b, s.unwrap()).unwrap(), vertex::point(b, t.unwrap()).unwrap());
|
||||
(s.distance(a) < 1e-6 && t.distance(q) < 1e-6) || (s.distance(q) < 1e-6 && t.distance(a) < 1e-6)
|
||||
})
|
||||
}
|
||||
let p = Point3::new;
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.fillet_refuse_rails_leaving_faces = true;
|
||||
k.fixes.fillet_trace_reaches_rails = true;
|
||||
k.fixes.fillet_rail_end_on_continued_surface = true;
|
||||
k.fixes.fillet_cliff_overflow = true;
|
||||
let block = create_box(&mut k, Point3::ZERO, 40.0, 30.0, 10.0).unwrap();
|
||||
let notch = create_box(&mut k, p(35.0, -1.0, -1.0), 6.0, 6.0, 4.0).unwrap();
|
||||
let mut b = boolean_difference(&mut k, block, notch).unwrap();
|
||||
const ID: u64 = 0x7000_0001;
|
||||
let edge_at = |b: &Brep, a, q| between(b, a, q).unwrap();
|
||||
for (a, q, v) in [(p(0.0, 30.0, 10.0), p(40.0, 30.0, 10.0), 1), (p(0.0, 0.0, 10.0), p(0.0, 30.0, 10.0), 2)] {
|
||||
let e = edge_at(&b, a, q);
|
||||
b.add_attribute(EntityRef::Edge(e), Attribute::long(ID, v, Behavior::Copy)).unwrap();
|
||||
}
|
||||
let rounds = [edge_at(&b, p(0.0, 0.0, 10.0), p(40.0, 0.0, 10.0)), edge_at(&b, p(40.0, 0.0, 10.0), p(40.0, 30.0, 10.0))];
|
||||
let mut s = FilletSession::new(&mut k, &mut b).unwrap();
|
||||
s.prepare_constant_radius_edges(&mut k, &rounds, 8.0, 1, 1, 1.0).unwrap();
|
||||
s.do_fillet(&mut k).unwrap();
|
||||
drop(s);
|
||||
b.refresh_cached_topology();
|
||||
for (a, q, which) in [(p(0.0, 30.0, 10.0), p(32.0, 30.0, 10.0), "back"), (p(0.0, 8.0, 10.0), p(0.0, 30.0, 10.0), "left")] {
|
||||
let e = edge_at(&b, a, q);
|
||||
assert!(b.entity_attributes(EntityRef::Edge(e)).iter().any(|(_, x)| x.attribute_id() == ID), "the {which} edge lost its attribute");
|
||||
}
|
||||
}
|
||||
|
||||
/// Solidcore alone: the pocket rim chamfered 5.5, as `fillet_mitre_concave_face_corners`' own test
|
||||
/// does it, with `keep_operand_uv_trims` on too, on the body the boolean gives:
|
||||
/// 21000 - (50 d² + 4/3 d³) = 19265.666666667 (the fork, and the same body through the flat
|
||||
/// arrays). Without `fillet_run_on_bspline_trims` it is refused: error 3, Corner Edge Computation
|
||||
/// Failure (solidcore#1981).
|
||||
#[test]
|
||||
fn pocket_chamfer_on_a_boolean_body() {
|
||||
use solidcore::blend::fillet_executive::session::FilletSession;
|
||||
use solidcore::boolean::api_brep::boolean_difference;
|
||||
use solidcore::primitive::api_primitives::create_box;
|
||||
use solidcore::topology::{EdgeId, edge, vertex};
|
||||
use solidcore::{Kernel, Point3};
|
||||
let p = Point3::new;
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.fillet_refuse_rails_leaving_faces = true;
|
||||
k.fixes.fillet_trace_reaches_rails = true;
|
||||
k.fixes.fillet_rail_end_on_continued_surface = true;
|
||||
k.fixes.fillet_cliff_overflow = true;
|
||||
k.fixes.fillet_mitre_concave_face_corners = true;
|
||||
k.fixes.keep_operand_uv_trims = true;
|
||||
k.fixes.fillet_run_on_bspline_trims = true;
|
||||
let plate = create_box(&mut k, Point3::ZERO, 60.0, 40.0, 10.0).unwrap();
|
||||
let cut = create_box(&mut k, p(15.0, 10.0, 5.0), 30.0, 20.0, 6.0).unwrap();
|
||||
let mut b = boolean_difference(&mut k, plate, cut).unwrap();
|
||||
let c = [p(15.0, 10.0, 10.0), p(45.0, 10.0, 10.0), p(45.0, 30.0, 10.0), p(15.0, 30.0, 10.0)];
|
||||
let on = |x: Point3, a: Point3, q: Point3| x.distance(a) < 1e-9 || x.distance(q) < 1e-9;
|
||||
let ids: Vec<EdgeId> = (0..4)
|
||||
.map(|i| {
|
||||
let (a, q) = (c[i], c[(i + 1) % 4]);
|
||||
b.edges()
|
||||
.ids()
|
||||
.find(|&e| {
|
||||
let (s, t) = edge::start_end_vertices(&b, e).unwrap();
|
||||
on(vertex::point(&b, s.unwrap()).unwrap(), a, q) && on(vertex::point(&b, t.unwrap()).unwrap(), a, q) && s != t
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
let mut s = FilletSession::new(&mut k, &mut b).unwrap();
|
||||
s.prepare_constant_radius_edges(&mut k, &ids, 5.5, 0, 1, 1.0).unwrap();
|
||||
let r = s.do_fillet(&mut k);
|
||||
assert!(r.is_ok(), "{} {}", s.error_info().code, s.error_info().comment);
|
||||
drop(s);
|
||||
b.refresh_cached_topology();
|
||||
let v = volume(&Body::new(b), 1e-8).unwrap();
|
||||
let d: f64 = 5.5;
|
||||
assert!(rel(v, 21000.0 - (50.0 * d * d + 4.0 / 3.0 * d * d * d)) < 1e-8, "{v}");
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[test]
|
||||
fn a_setback_corner_left_open_returns_its_hole() {
|
||||
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();
|
||||
blend_holes(&b, Blend::Round, &edges, &[5.0; 3], 0.1, &[[60.0, 40.0, 20.0]], &[8.0], Corners::Open).unwrap()
|
||||
});
|
||||
assert_eq!(counts(&open).unwrap(), [9, 30, 22]);
|
||||
assert!(!super::is_manifold_solid(&open).unwrap());
|
||||
let [hole] = holes.as_slice() else { panic!("one corner: {holes:?}") };
|
||||
assert_eq!(hole.edges.len(), 12);
|
||||
let flat = to_arrays(&open).unwrap();
|
||||
let (rec, _) = native(&flat).unwrap();
|
||||
rec.validate().unwrap();
|
||||
let ends = &flat.ints[&crate::flat::EDGE_VERTICES];
|
||||
let end = |h: &crate::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];
|
||||
assert_eq!(end(h, true), end(next, false), "edge {k} ends where the next starts");
|
||||
assert!(h.face < 9);
|
||||
}
|
||||
// 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();
|
||||
blend_holes(&b, Blend::Round, &edges[..2], &[5.0; 2], 0.1, &[[60.0, 40.0, 20.0]], &[8.0], Corners::Open).err()
|
||||
});
|
||||
assert_eq!(refused, Some(Error::CornerRefused { corner: 0 }));
|
||||
}
|
||||
|
||||
/// A curvature-continuous round at the equal-area thumbweight takes off what the ball's circle
|
||||
/// does, r² (cot(α/2) - (π - α)/2) per unit length on a wedge α (solidcore#2261): on a 40 x 30 x 20
|
||||
/// box with its top back edge chamfered 5, r 2 along the chamfer's top edge (α 135°) takes
|
||||
/// 0.0860579227 per mm, along the bottom front edge (α 90°) 0.8584073464. Both in one call are
|
||||
/// refused: every edge takes the first one's thumbweight, so the other would take 17% too little
|
||||
/// or 21% too much (solidcore#2250's comment 6563419897), whichever order they come in.
|
||||
#[test]
|
||||
fn a_curvature_round_takes_its_wedges_equal_area_and_mixed_wedges_are_refused() {
|
||||
let r = 2.0;
|
||||
let cut = |alpha: f64| r * r * (1.0 / (alpha / 2.0).tan() - (PI - alpha) / 2.0);
|
||||
let chamfered =
|
||||
blend(&Shape::Box([0.0; 3], [40.0, 30.0, 20.0]).core(), Blend::Chamfer, &[[20.0, 30.0, 20.0]], &[5.0], TOL, &[], &[], Corners::Filled)
|
||||
.unwrap();
|
||||
let v0 = volume(&chamfered, 1e-8).unwrap();
|
||||
assert!(rel(v0, 23500.0) < 1e-9, "{v0}");
|
||||
let (a, b) = ([20.0, 25.0, 20.0], [20.0, 0.0, 0.0]);
|
||||
for (at, alpha) in [(a, 0.75 * PI), (b, 0.5 * PI)] {
|
||||
let round = blend(&chamfered, Blend::Curvature, &[at], &[r], TOL, &[], &[], Corners::Filled).unwrap();
|
||||
let (rec, _) = native(&to_arrays(&round).unwrap()).unwrap();
|
||||
rec.validate().unwrap();
|
||||
let removed = v0 - volume(&round, 1e-8).unwrap();
|
||||
assert!(rel(removed, 40.0 * cut(alpha)) < 1e-6, "{removed} vs {}", 40.0 * cut(alpha));
|
||||
}
|
||||
for both in [[a, b], [b, a]] {
|
||||
let refused = blend(&chamfered, Blend::Curvature, &both, &[r], TOL, &[], &[], Corners::Filled).err();
|
||||
assert_eq!(refused, Some(Error::Unported(super::blend::CURVATURE_MIXED_WEDGES)));
|
||||
assert!(refused.is_some_and(|e| e.to_string().contains("issuecomment-6563419897")));
|
||||
}
|
||||
// Edges of one wedge go together.
|
||||
let two = blend(&chamfered, Blend::Curvature, &[b, [20.0, 0.0, 20.0]], &[r], TOL, &[], &[], Corners::Filled).unwrap();
|
||||
assert!(rel(v0 - volume(&two, 1e-8).unwrap(), 80.0 * cut(0.5 * PI)) < 1e-6);
|
||||
// A pick that misses is still named first.
|
||||
assert_eq!(blend(&chamfered, Blend::Curvature, &[[90.0; 3]], &[r], TOL, &[], &[], Corners::Filled).err(), Some(Error::NoMatch { point: 0 }));
|
||||
}
|
||||
|
||||
/// A variable round is the union of its balls (solidcore#2262, `fillet_gn_section_generator`).
|
||||
/// Along the 30 mm top front edge: a law of one radius 2 takes the constant round's
|
||||
/// (1 - π/4) 4 · 30 = 25.752220; 1 to 3, the balls' union's 27.857434 (the great circles would take
|
||||
/// 27.898239), either way along the edge. Both come out 5.3e-5 of that over, as on the C++
|
||||
/// kernel: the solver's surface fit. A law whose radius climbs faster than a rolling ball can
|
||||
/// follow (1 to 4 within a mm) folds its balls over one another: refused, not built.
|
||||
#[test]
|
||||
fn a_variable_round_is_the_union_of_its_balls() {
|
||||
let b = BLOCK.core();
|
||||
let removed = |radii: &[f64], start: [f64; 3]| {
|
||||
let r = fillet_variable(&b, TOP_FRONT, TOL, start, 1, &[0.0, 0.0, 1.0, 1.0], radii).unwrap();
|
||||
let (rec, _) = native(&to_arrays(&r).unwrap()).unwrap();
|
||||
rec.validate().unwrap();
|
||||
ABC - volume(&r, 1e-8).unwrap()
|
||||
};
|
||||
let constant = removed(&[2.0, 2.0], [0.0, 0.0, 10.0]);
|
||||
assert!(rel(constant, (1.0 - PI / 4.0) * 4.0 * 30.0) < 1e-4, "{constant}");
|
||||
let linear = removed(&[1.0, 3.0], [0.0, 0.0, 10.0]);
|
||||
assert!(rel(linear, 27.857434) < 1e-4, "{linear}");
|
||||
assert!(rel(removed(&[1.0, 3.0], [30.0, 0.0, 10.0]), linear) < 1e-8);
|
||||
let steep = [0.0, 0.0, 0.0, 0.0, 0.02, 0.04, 1.0, 1.0, 1.0, 1.0];
|
||||
let folds = fillet_variable(&b, TOP_FRONT, TOL, [0.0, 0.0, 10.0], 3, &steep, &[1.0, 1.0, 4.0, 4.0, 4.0, 4.0]).err();
|
||||
assert_eq!(folds, Some(Error::NoRoom));
|
||||
// A pick that misses is still named first.
|
||||
assert_eq!(fillet_variable(&b, [90.0; 3], TOL, [0.0; 3], 1, &[0.0, 0.0, 1.0, 1.0], &[1.0, 3.0]).err(), Some(Error::NoMatch { point: 0 }));
|
||||
}
|
||||
|
||||
/// The capi's argument checks, before any kernel call.
|
||||
#[test]
|
||||
fn hostile_arguments_are_refused() {
|
||||
let b = BLOCK.core();
|
||||
let none = Corners::Filled;
|
||||
assert_eq!(blend(&b, Blend::Round, &[], &[1.0], TOL, &[], &[], none).err(), Some(Error::Argument));
|
||||
assert_eq!(blend(&b, Blend::Round, &[[f64::NAN; 3]], &[1.0], TOL, &[], &[], none).err(), Some(Error::Argument));
|
||||
assert_eq!(blend(&b, Blend::Round, &[TOP_FRONT], &[-1.0], TOL, &[], &[], none).err(), Some(Error::Argument));
|
||||
assert_eq!(blend(&b, Blend::Round, &[TOP_FRONT], &[1.0], 0.0, &[], &[], none).err(), Some(Error::Argument));
|
||||
assert_eq!(blend(&b, Blend::Round, &[TOP_FRONT], &[f64::INFINITY], TOL, &[], &[], none).err(), Some(Error::Argument));
|
||||
// A chamfer's corners are bevelled, which builds no setback.
|
||||
assert_eq!(blend(&b, Blend::Chamfer, &[TOP_FRONT], &[1.0], TOL, &[[0.0, 0.0, 10.0]], &[1.0], none).err(), Some(Error::Unsupported));
|
||||
assert_eq!(fillet_variable(&b, TOP_FRONT, TOL, [0.0; 3], 1, &[0.0, 1.0], &[1.0, 2.0]).err(), Some(Error::Argument));
|
||||
// A radius that pinches to nothing inside the edge.
|
||||
assert_eq!(fillet_variable(&b, TOP_FRONT, TOL, [0.0; 3], 1, &[0.0, 0.0, 0.5, 1.0, 1.0], &[1.0, 0.0, 1.0]).err(), Some(Error::Unsupported));
|
||||
assert_eq!(chamfer_sides(&b, &[TOP_FRONT], TOL, 1.0, ChamferSecond::Angle(4.0), [0.0, 0.0, 1.0]).err(), Some(Error::Argument));
|
||||
assert_eq!(chamfer_sides(&b, &[TOP_FRONT], TOL, 1.0, ChamferSecond::Distance(1.0), [0.0; 3]).err(), Some(Error::Argument));
|
||||
}
|
||||
|
||||
/// A setback longer than an edge at its corner leaves no room, before any blend is made.
|
||||
#[test]
|
||||
fn a_setback_past_an_edge_has_no_room() {
|
||||
let r = timed("setback_long", || {
|
||||
let b = BLOCK.core();
|
||||
let corner = [[15.0, 20.0, 10.0], [30.0, 10.0, 10.0], [30.0, 20.0, 5.0]];
|
||||
blend(&b, Blend::Round, &corner, &[2.0; 3], TOL, &[[30.0, 20.0, 10.0]], &[10.0], Corners::Filled).err()
|
||||
});
|
||||
assert_eq!(r, Some(Error::NoRoom));
|
||||
}
|
||||
|
||||
/// The body blended is never changed.
|
||||
#[test]
|
||||
fn the_body_stays_as_it_was() {
|
||||
let r = timed("operand", || {
|
||||
let b = BLOCK.core();
|
||||
let _ = blend(&b, Blend::Round, &[TOP_FRONT], &[2.0], TOL, &[], &[], Corners::Filled);
|
||||
(counts(&b).unwrap(), volume(&b, 1e-8).unwrap())
|
||||
});
|
||||
assert_eq!(r.0, [6, 12, 8]);
|
||||
assert!(rel(r.1, ABC) < 1e-12, "{}", r.1);
|
||||
}
|
||||
|
||||
/// The chamfer's first distance goes on the face leaning most along `first`: 2 along the top and
|
||||
/// 3 down the front for +z, the other way round for -y. The volumes are the same; the centroid
|
||||
/// tells them apart. The removed prism's section is the triangle with legs t (back along the
|
||||
/// top, in y) and f (down the front, in z) at the edge (y = 0, z = 10), centroid
|
||||
/// (t/3, 10 - f/3), area t f / 2: the block keeps ȳ = (6000·10 - 15 t f · t/3) / (6000 - 15 t f)
|
||||
/// and z̄ = (6000·5 - 15 t f (10 - f/3)) / (6000 - 15 t f).
|
||||
#[test]
|
||||
fn the_first_distance_goes_on_the_face_along_first() {
|
||||
for (first, top, front) in [([0.0, 0.0, 1.0], 2.0, 3.0), ([0.0, -1.0, 0.0], 3.0, 2.0)] {
|
||||
let r = timed("sides_first", move || {
|
||||
let b = BLOCK.core();
|
||||
chamfer_sides(&b, &[TOP_FRONT], TOL, 2.0, ChamferSecond::Distance(3.0), first).and_then(|c| super::mass_properties(&c, 1e-8))
|
||||
});
|
||||
let m = r.unwrap_or_else(|e| panic!("{first:?}: {e}"));
|
||||
let cut = 15.0 * top * front;
|
||||
let y = (6000.0 * 10.0 - cut * top / 3.0) / (6000.0 - cut);
|
||||
let z = (6000.0 * 5.0 - cut * (10.0 - front / 3.0)) / (6000.0 - cut);
|
||||
assert!(rel(m.volume, 6000.0 - cut) < 1e-9, "{first:?}: volume {}", m.volume);
|
||||
assert!((m.centroid[1] - y).abs() < 1e-7 && (m.centroid[2] - z).abs() < 1e-7, "{first:?}: centroid {:?} vs ({y}, {z})", m.centroid);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// chord between the contacts of the ball of radius d, sqrt(4 R d) back on the flat face and where
|
||||
/// the line from the arc's axis to the ball's centre meets the wall. It takes the triangle flat
|
||||
/// contact, tip, wall contact less the wall's circular segment R²/2 (b - sin b),
|
||||
/// b = acos((R - d)/(R + d)): at d = 2 the chord is 3.822 wide and takes 159.23645 (each to
|
||||
/// 1e-12). Mirrored in x too.
|
||||
#[test]
|
||||
fn knife_edge_chamfers() {
|
||||
use super::sweep::extrude;
|
||||
use super::transform::transform;
|
||||
use crate::sweeps::{Curve, DraftCorner};
|
||||
|
||||
const R: f64 = 21.0;
|
||||
const TIP: f64 = 62.0 + 1.0 / 6.0;
|
||||
const H: f64 = 10.0;
|
||||
let line = |a: [f64; 2], b: [f64; 2]| Curve::Line { start: [a[0], a[1], 0.0], end: [b[0], b[1], 0.0] };
|
||||
let profile = [
|
||||
line([0.0, 0.0], [0.0, -R]),
|
||||
line([0.0, -R], [TIP - R, -R]),
|
||||
Curve::Arc { center: [TIP, -R, 0.0], axis: [0.0, 0.0, -1.0], reference: [1.0, 0.0, 0.0], radius: R, start: PI, end: 1.5 * PI },
|
||||
line([TIP, 0.0], [0.0, 0.0]),
|
||||
];
|
||||
let part = extrude(&profile, [0.0, 0.0, 1.0], H, 0.0, DraftCorner::Curvature, true).unwrap();
|
||||
let before = (TIP - R) * R * H + R * R * (1.0 - PI / 4.0) * H;
|
||||
assert!((volume(&part, 1e-8).unwrap() - before).abs() < 1e-9 * before);
|
||||
for d in [1.0, 2.0, 5.0] {
|
||||
let flat = TIP - (4.0 * R * d).sqrt();
|
||||
let (wx, wy) = (TIP - (4.0 * R * d).sqrt() * R / (R + d), -R + (R - d) * R / (R + d));
|
||||
let triangle = 0.5 * ((TIP - flat) * wy).abs();
|
||||
let b = ((R - d) / (R + d)).acos();
|
||||
let removed = (triangle - 0.5 * R * R * (b - b.sin())) * H;
|
||||
if d == 2.0 {
|
||||
assert!((removed - 159.236_450_742_901_18).abs() < 1e-12 * 159.24);
|
||||
assert!(((wx - flat).hypot(wy) - 3.822_132_343_623_634_7).abs() < 1e-12 * 3.83);
|
||||
}
|
||||
for mirrored in [false, true] {
|
||||
let sx = if mirrored { -1.0 } else { 1.0 };
|
||||
let m = [[sx, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]];
|
||||
let body = transform(&part, &m).unwrap();
|
||||
let tip = [sx * TIP, 0.0, H / 2.0];
|
||||
let cut = blend(&body, Blend::Chamfer, &[tip], &[d], TOL, &[], &[], Corners::Filled).unwrap();
|
||||
let v = volume(&cut, 1e-8).unwrap();
|
||||
let want = before - removed;
|
||||
assert!((v - want).abs() < 1e-9 * want, "d {d} mirrored {mirrored}: {v} vs {want}");
|
||||
native(&to_arrays(&cut).unwrap()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The part from SolidCraft #871: a plate with a narrower block on top and a window through both.
|
||||
fn part_871() -> Body {
|
||||
let (a, b) = (12.74437365538526, 22.69275963366936);
|
||||
let at = |lo: [f64; 3], hi: [f64; 3]| make_box(lo, [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]]).unwrap();
|
||||
let joined = boolean(&at([9.5, 8.0, 0.0], [23.5, 29.5, 10.0]), &at([9.5, a, 10.0], [23.5, b, 20.0]), BooleanOp::Union).unwrap().unwrap();
|
||||
boolean(&joined, &at([9.5, 14.0, 7.0], [23.5, 19.2, 14.1]), BooleanOp::Subtract).unwrap().unwrap()
|
||||
}
|
||||
|
||||
/// The 12 edges round each of the part's two end faces.
|
||||
fn edges_871() -> Vec<[f64; 3]> {
|
||||
let yz = [
|
||||
(12.74437365538526, 15.0),
|
||||
(10.37218682769263, 10.0),
|
||||
(8.0, 5.0),
|
||||
(18.75, 0.0),
|
||||
(29.5, 5.0),
|
||||
(26.09637981683468, 10.0),
|
||||
(22.69275963366936, 15.0),
|
||||
(17.71856664452731, 20.0),
|
||||
(16.6, 7.0),
|
||||
(14.0, 10.55),
|
||||
(16.6, 14.100000000000001),
|
||||
(19.200000000000003, 10.55),
|
||||
];
|
||||
[23.5, 9.5].iter().flat_map(|x| yz.iter().map(move |(y, z)| [*x, *y, *z])).collect()
|
||||
}
|
||||
|
||||
/// #871's rounds at r 0.5 fit (the wall between the window and the block's side is 1.26 thick):
|
||||
/// 38 faces, and the closed form `issue_871_tests::rounds_that_fit_are_closed` derives. At r 1 they
|
||||
/// overlap, which is refused.
|
||||
#[test]
|
||||
fn rounds_on_the_871_part() {
|
||||
let p = part_871();
|
||||
let e = edges_871();
|
||||
let sizes = |r: f64| vec![r; e.len()];
|
||||
let part_volume = 14.0 * (21.5 * 10.0 + (22.69275963366936 - 12.74437365538526) * 10.0 - 5.2 * 7.1);
|
||||
let exact = part_volume - 2.0 * (2.0 * (21.5 + 20.0) + 2.0 * (5.2 + 7.1)) * spandrel(0.5);
|
||||
let fit = blend(&p, Blend::Round, &e, &sizes(0.5), TOL, &[], &[], Corners::Filled).unwrap();
|
||||
let (v, n) = (volume(&fit, 1e-8).unwrap(), counts(&fit).unwrap());
|
||||
let refused = blend(&p, Blend::Round, &e, &sizes(1.0), TOL, &[], &[], Corners::Filled).map(|_| ()).unwrap_err();
|
||||
assert_eq!(refused, Error::NoRoom);
|
||||
assert_eq!(n[0], 38);
|
||||
assert!((v - exact).abs() < 1e-3, "{v} vs {exact}");
|
||||
}
|
||||
|
||||
/// The fork's `tabbed_plate_rounds_mitred_on_the_arcs_continuation`: a plate 2 thick, its outline an
|
||||
/// R25 arc to the sides x = +-4 of a tab whose top is 4 wide with R2 corners, every top edge of the
|
||||
/// 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).
|
||||
#[test]
|
||||
fn tabbed_plate_rounds_mitred_on_the_arcs_continuation() {
|
||||
use super::sweep::extrude;
|
||||
use crate::sweeps::{Curve, DraftCorner};
|
||||
const Z: [f64; 3] = [0.0, 0.0, 1.0];
|
||||
let theta = (4.0_f64 / 25.0).acos();
|
||||
let y = 609.0_f64.sqrt();
|
||||
let cy = 280.0 / 9.0;
|
||||
let line = |a: [f64; 2], b: [f64; 2]| Curve::Line { start: [a[0], a[1], 0.0], end: [b[0], b[1], 0.0] };
|
||||
let arc = |c: [f64; 2], radius, start, end| Curve::Arc { center: [c[0], c[1], 0.0], axis: Z, reference: [1.0, 0.0, 0.0], radius, start, end };
|
||||
let outline = [
|
||||
arc([0.0, 0.0], 25.0, PI - theta, 2.0 * PI + theta),
|
||||
line([4.0, y], [4.0, cy]),
|
||||
arc([2.0, cy], 2.0, 0.0, PI / 2.0),
|
||||
line([2.0, cy + 2.0], [-2.0, cy + 2.0]),
|
||||
arc([-2.0, cy], 2.0, PI / 2.0, PI),
|
||||
line([-4.0, cy], [-4.0, y]),
|
||||
];
|
||||
let area = |da: f64, dt: f64| {
|
||||
let (big, rho) = (25.0 - da, 2.0 - dt);
|
||||
let tab = rho * rho * PI + 2.0 * rho * (2.0 + cy) + 4.0 * (cy + rho);
|
||||
let yc = (big * big - (4.0 - dt) * (4.0 - dt)).sqrt();
|
||||
let at = yc.atan2(4.0 - dt);
|
||||
0.5 * (big * big * (PI + 2.0 * at) + tab) + (4.0 - dt) * (cy - yc)
|
||||
};
|
||||
let (r, dphi) = (0.5, (PI / 2.0) / 8192.0);
|
||||
let want: f64 = (0..8192)
|
||||
.map(|i| {
|
||||
let phi = (f64::from(i) + 0.5) * dphi;
|
||||
let d = r * (1.0 - phi.cos());
|
||||
(area(0.0, 0.0) - area(d, d)) * r * phi.cos() * dphi
|
||||
})
|
||||
.sum();
|
||||
let top = [
|
||||
[0.0, -25.0, 2.0],
|
||||
[4.0, 27.8945, 2.0],
|
||||
[3.41421356, 32.5253247, 2.0],
|
||||
[0.0, cy + 2.0, 2.0],
|
||||
[-3.41421356, 32.5253247, 2.0],
|
||||
[-4.0, 27.8945, 2.0],
|
||||
];
|
||||
let plate = extrude(&outline, Z, 2.0, 0.0, DraftCorner::default(), true).unwrap();
|
||||
assert!(rel(volume(&plate, 1e-8).unwrap(), 2.0 * area(0.0, 0.0)) < 1e-10);
|
||||
let round = blend(&plate, Blend::Round, &top, &[r; 6], TOL, &[], &[], Corners::Filled).unwrap();
|
||||
let removed = volume(&plate, 1e-8).unwrap() - volume(&round, 1e-8).unwrap();
|
||||
assert!(rel(removed, want) < 1e-6, "removed {removed}, expected {want}");
|
||||
let (rec, _) = native(&to_arrays(&round).unwrap()).unwrap();
|
||||
rec.validate().unwrap();
|
||||
assert!(super::is_manifold_solid(&round).unwrap());
|
||||
assert!(super::check(&round, crate::check::CheckLevel::All).unwrap().is_valid());
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
//! Booleans on solidcore: the C API's own logic round `SM_API`, ported from the fork's
|
||||
//! `capi/src/smlib_ops.cpp` (`smlib_boolean_with`, `smlib_boolean2d`,
|
||||
//! `smlib_boolean_union_all`, `finish`, `closed_nonmanifold`) and
|
||||
//! `capi/src/smlib_imprint.cpp` (`smlib_imprint`).
|
||||
//!
|
||||
//! As there, every operation works on copies of its operands (`SmApiBrepCopy`): `SM_API` edits
|
||||
//! the first operand in place and consumes the second, and a SolidCraft body never changes.
|
||||
|
||||
use std::sync::PoisonError;
|
||||
|
||||
use solidcore::Result as SolidResult;
|
||||
use solidcore::SolidError;
|
||||
use solidcore::boolean::api_brep::{self as sm, Boolean2dOutcome};
|
||||
use solidcore::boolean::merge::MergeOptions;
|
||||
use solidcore::geom::a_object::BooleanOperation;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::primitive::primitive_creation::p1::Boolean2d;
|
||||
use solidcore::topology::brep::p3::brep_copy;
|
||||
use solidcore::topology::brep::{p1, p2};
|
||||
use solidcore::topology::{Brep, edge};
|
||||
|
||||
use super::{Body, run};
|
||||
use crate::{BooleanOp, Error, Result};
|
||||
|
||||
/// What an operation's `SM_API` call gave, before it is a SolidCraft result: the capi's
|
||||
/// statuses besides success.
|
||||
enum Done {
|
||||
Body(Box<Brep>),
|
||||
/// `SMLIB_ERR_EMPTY`.
|
||||
Empty,
|
||||
/// `SMLIB_ERR_NOT_SOLID`.
|
||||
NotSolid,
|
||||
/// `SMLIB_ERR_NO_CUT` (`smlib_imprint`).
|
||||
NoCut,
|
||||
}
|
||||
|
||||
impl super::dump::Answer for Done {
|
||||
fn refusal(&self) -> Option<String> {
|
||||
matches!(self, Done::NotSolid).then(|| Error::NotSolid.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// `GetNumFaces`.
|
||||
fn num_faces(b: &Brep) -> usize {
|
||||
b.face_list().len()
|
||||
}
|
||||
|
||||
/// `closed_nonmanifold` (smlib_ops.cpp): a closed body whose solids touch only along edges or at
|
||||
/// vertices: at least two regions, no shell vertices, no wire edge, and four edgeuses (two faces,
|
||||
/// both sides) per face round every edge. As there, an edge whose query fails (`e == nullptr`, a
|
||||
/// failed `GetEdgeuses`) makes it false: [`edge_closes`].
|
||||
fn closed_nonmanifold(r: &Brep) -> SolidResult<bool> {
|
||||
if p1::regions(r, None)?.len() < 2 {
|
||||
return Ok(false);
|
||||
}
|
||||
if !p2::shell_vertices(r, None)?.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
for e in p1::edges(r, None)? {
|
||||
if !edge_closes(edge::is_wire(r, e), edge::edgeuses(r, e, None, None).map(|u| u.len())) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// The capi's test of one edge in `closed_nonmanifold`, given its `IsWire` and the count of its
|
||||
/// `GetEdgeuses`: not a wire, and four edgeuses per face round it. Either query failing is
|
||||
/// false, not an error: the capi returns false where `GetEdgeuses` fails.
|
||||
pub(super) fn edge_closes(wire: SolidResult<bool>, uses: SolidResult<usize>) -> bool {
|
||||
matches!(wire, Ok(false)) && matches!(uses, Ok(n) if n >= 4 && n % 4 == 0)
|
||||
}
|
||||
|
||||
/// `finish` (smlib_ops.cpp): the result must have faces, and a solid input must give a closed
|
||||
/// manifold solid or (`nonmanifold`) a closed one whose solids touch along an edge or at a vertex.
|
||||
fn finish(r: Brep, solid_in: bool, nonmanifold: bool) -> SolidResult<Done> {
|
||||
if num_faces(&r) == 0 {
|
||||
return Ok(Done::Empty);
|
||||
}
|
||||
if solid_in && !p2::is_manifold_solid(&r)? && !(nonmanifold && closed_nonmanifold(&r)?) {
|
||||
return Ok(Done::NotSolid);
|
||||
}
|
||||
Ok(Done::Body(Box::new(r)))
|
||||
}
|
||||
|
||||
/// What a failing call's dump calls its first operands.
|
||||
const OPERANDS: [&str; 4] = ["a", "b", "c", "d"];
|
||||
|
||||
/// Copies of `bodies`, in order (a body given twice is copied twice, as the capi copies each
|
||||
/// operand), all made under the kernel's lock.
|
||||
fn copies(k: &mut Kernel, bodies: &[&Body]) -> SolidResult<Vec<Brep>> {
|
||||
// Under the kernel's lock (`run` holds it), as every body lock is: two never deadlock.
|
||||
let mut out = Vec::with_capacity(bodies.len());
|
||||
for (n, b) in bodies.iter().enumerate() {
|
||||
let g = b.0.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
super::dump::input(k, OPERANDS.get(n).copied().unwrap_or("operand"), &g);
|
||||
let c = brep_copy(k, &g)?;
|
||||
drop(g);
|
||||
out.push(c);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn result(d: Done, empty: Result<Option<Body>>) -> Result<Option<Body>> {
|
||||
match d {
|
||||
Done::Body(b) => Ok(Some(Body::new(*b))),
|
||||
Done::Empty => empty,
|
||||
Done::NotSolid => Err(Error::NotSolid),
|
||||
Done::NoCut => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn operation(op: BooleanOp) -> BooleanOperation {
|
||||
match op {
|
||||
BooleanOp::Union => BooleanOperation::Union,
|
||||
BooleanOp::Subtract => BooleanOperation::Difference,
|
||||
BooleanOp::Intersect => BooleanOperation::Intersection,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether every body is a manifold solid (`IsManifoldSolid` of each copy).
|
||||
fn all_solid(bodies: &[Brep]) -> SolidResult<bool> {
|
||||
for b in bodies {
|
||||
if !p2::is_manifold_solid(b)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Fails when a panic left one of `bodies` half-edited: it is never used again.
|
||||
fn usable(call: &'static str, bodies: &[&Body]) -> Result<()> {
|
||||
if bodies.iter().any(|b| b.0.is_poisoned()) {
|
||||
return Err(Error::Internal(format!("{call}: the body was left unfinished by an earlier panic")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `smlib_boolean` (`smlib_boolean_with` with `SMLIB_RESULT_NONMANIFOLD`): `SmApiBoolean` on
|
||||
/// copies of `a` and `b`; `None` when the result is empty (`SMLIB_ERR_EMPTY`), and
|
||||
/// [`Error::NotSolid`] when solid operands give a body that is not closed.
|
||||
pub(crate) fn boolean(a: &Body, b: &Body, op: BooleanOp) -> Result<Option<Body>> {
|
||||
usable("SmApiBoolean", &[a, b])?;
|
||||
let d = run("SmApiBoolean", |k| {
|
||||
let mut c = copies(k, &[a, b])?.into_iter();
|
||||
let (Some(ca), Some(cb)) = (c.next(), c.next()) else {
|
||||
return Err(SolidError::invalid_input("smlib_boolean", "no operand"));
|
||||
};
|
||||
let solid_in = p2::is_manifold_solid(&ca)? && p2::is_manifold_solid(&cb)?;
|
||||
let r = sm::boolean(k, ca, cb, operation(op))?;
|
||||
finish(r.result, solid_in, true)
|
||||
})?;
|
||||
result(d, Ok(None))
|
||||
}
|
||||
|
||||
/// `smlib_boolean_union_all`: copies of `bodies` unioned pairwise, level by level (an odd one
|
||||
/// carried to the next level as it is), each union `SmApiBoolean`'s; then `finish` with
|
||||
/// non-manifold results allowed. An empty result is [`Error::Empty`].
|
||||
pub(crate) fn union_all(bodies: &[&Body]) -> Result<Body> {
|
||||
if bodies.is_empty() || bodies.len() > crate::MAX_UNION_BREPS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
usable("smlib_boolean_union_all", bodies)?;
|
||||
let d = run("smlib_boolean_union_all", |k| {
|
||||
let mut level = copies(k, bodies)?;
|
||||
let solid_in = all_solid(&level)?;
|
||||
while level.len() > 1 {
|
||||
let mut next = Vec::with_capacity(level.len().div_ceil(2));
|
||||
let mut it = level.into_iter();
|
||||
while let Some(a) = it.next() {
|
||||
match it.next() {
|
||||
Some(b) => next.push(sm::boolean(k, a, b, BooleanOperation::Union)?.result),
|
||||
None => next.push(a),
|
||||
}
|
||||
}
|
||||
level = next;
|
||||
}
|
||||
match level.pop() {
|
||||
Some(r) => finish(r, solid_in, true),
|
||||
None => Ok(Done::Empty),
|
||||
}
|
||||
})?;
|
||||
result(d, Err(Error::Empty))?.ok_or(Error::Empty)
|
||||
}
|
||||
|
||||
/// `smlib_boolean2d`: `SmApiBoolean2d` (`SmPrimitiveCreation::Boolean2D`) on copies of `a` and
|
||||
/// `b`, sheets of planar faces on one plane; `None` when either has no face (where
|
||||
/// `SmApiBoolean2d` succeeds without a result) or the result is empty.
|
||||
pub(crate) fn boolean2d(a: &Body, b: &Body, op: BooleanOp) -> Result<Option<Body>> {
|
||||
usable("SmApiBoolean2d", &[a, b])?;
|
||||
let d = run("SmApiBoolean2d", |k| {
|
||||
let mut c = copies(k, &[a, b])?.into_iter();
|
||||
let (Some(ca), Some(cb)) = (c.next(), c.next()) else {
|
||||
return Err(SolidError::invalid_input("smlib_boolean2d", "no operand"));
|
||||
};
|
||||
if num_faces(&ca) == 0 || num_faces(&cb) == 0 {
|
||||
return Ok(Done::Empty);
|
||||
}
|
||||
let kind = match op {
|
||||
BooleanOp::Union => Boolean2d::Union,
|
||||
BooleanOp::Subtract => Boolean2d::Difference,
|
||||
BooleanOp::Intersect => Boolean2d::Intersection,
|
||||
};
|
||||
match sm::boolean_2d(k, ca, cb, kind)? {
|
||||
Boolean2dOutcome::Result(r) => finish(r, false, false),
|
||||
Boolean2dOutcome::Unassigned { .. } => Ok(Done::Empty),
|
||||
}
|
||||
})?;
|
||||
result(d, Ok(None))
|
||||
}
|
||||
|
||||
/// `smlib_imprint`: `SmApiBooleanWithOptions(SM_BO_IMPRINT)` on copies of `body` and `tool`,
|
||||
/// imprinting and keeping the tool (cookie cutter off, imprinting on, post-process off,
|
||||
/// imprint-and-classify off, keep other on). `None` when no face was cut
|
||||
/// (`SMLIB_ERR_NO_CUT`); an empty result is [`Error::Empty`]; the same body twice is
|
||||
/// [`Error::Argument`].
|
||||
pub(crate) fn imprint(body: &Body, tool: &Body) -> Result<Option<Body>> {
|
||||
if std::ptr::eq(body, tool) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
usable("SmApiBooleanWithOptions", &[body, tool])?;
|
||||
let d = run("SmApiBooleanWithOptions", |k| {
|
||||
let mut c = copies(k, &[body, tool])?.into_iter();
|
||||
let (Some(a), Some(t)) = (c.next(), c.next()) else {
|
||||
return Err(SolidError::invalid_input("smlib_imprint", "no operand"));
|
||||
};
|
||||
let before = num_faces(&a);
|
||||
let options = MergeOptions::new(false, true, false, false, true);
|
||||
let r = sm::boolean_with_options(k, a, t, BooleanOperation::Imprint, options)?.result;
|
||||
let after = num_faces(&r);
|
||||
Ok(if after == 0 {
|
||||
Done::Empty
|
||||
} else if after == before {
|
||||
Done::NoCut
|
||||
} else {
|
||||
Done::Body(Box::new(r))
|
||||
})
|
||||
})?;
|
||||
match d {
|
||||
Done::Empty => Err(Error::Empty),
|
||||
d => result(d, Err(Error::Empty)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
//! 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.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::f64::consts::PI;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::mass::integrator::properties::brep_area;
|
||||
use solidcore::primitive::primitive_creation::p1::create_rectangle;
|
||||
|
||||
use super::boolean::{boolean, boolean2d, edge_closes, imprint, union_all};
|
||||
use super::{Body, counts, make_box, make_cylinder, make_sphere, run, solid_count, to_arrays, volume, with_body};
|
||||
use crate::export::native;
|
||||
use crate::{BooleanOp, Error};
|
||||
|
||||
/// A case longer than this is a hang.
|
||||
const TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
/// An operand.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Shape {
|
||||
Box([f64; 3], [f64; 3]),
|
||||
Cylinder([f64; 3], f64, f64),
|
||||
Sphere([f64; 3], f64),
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
fn core(self) -> Body {
|
||||
match self {
|
||||
Shape::Box(o, s) => make_box(o, s),
|
||||
Shape::Cylinder(o, r, h) => make_cylinder(o, r, h),
|
||||
Shape::Sphere(c, r) => make_sphere(c, r),
|
||||
}
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// What a case does.
|
||||
#[derive(Clone, Debug)]
|
||||
enum Op {
|
||||
Boolean(Shape, Shape, BooleanOp),
|
||||
UnionAll(Vec<Shape>),
|
||||
Imprint(Shape, Shape),
|
||||
}
|
||||
|
||||
/// A case: the operation, its analytic volume (`None`: the result is empty), the faces it
|
||||
/// must have where they are known analytically, and its solids (`smlib_brep_solid_count`).
|
||||
struct Case {
|
||||
op: Op,
|
||||
volume: Option<f64>,
|
||||
faces: Option<usize>,
|
||||
solids: usize,
|
||||
}
|
||||
|
||||
const A: Shape = Shape::Box([0.0; 3], [10.0; 3]);
|
||||
|
||||
fn case(name: &str) -> Case {
|
||||
let plain = |op, volume: Option<f64>, faces| Case { op, volume, faces, solids: 1 };
|
||||
match name {
|
||||
// |A| + |B| - |A ∩ B|: two 10 mm cubes overlapping in a 5 mm cube.
|
||||
"union_overlapping" => plain(Op::Boolean(A, Shape::Box([5.0; 3], [10.0; 3]), BooleanOp::Union), Some(1000.0 + 1000.0 - 125.0), Some(12)),
|
||||
// Two 10 mm cubes 10 mm apart: one body of two solids, 12 faces.
|
||||
"union_disjoint" => {
|
||||
Case { solids: 2, ..plain(Op::Boolean(A, Shape::Box([20.0, 0.0, 0.0], [10.0; 3]), BooleanOp::Union), Some(2000.0), Some(12)) }
|
||||
}
|
||||
"intersect_overlapping" => plain(Op::Boolean(A, Shape::Box([5.0; 3], [10.0; 3]), BooleanOp::Intersect), Some(125.0), Some(6)),
|
||||
// A slot 5 mm wide and 5 mm deep from the top, right through a 30 x 20 x 10 block.
|
||||
"subtract_through_slot" => plain(
|
||||
Op::Boolean(Shape::Box([0.0; 3], [30.0, 20.0, 10.0]), Shape::Box([10.0, -1.0, 5.0], [5.0, 22.0, 6.0]), BooleanOp::Subtract),
|
||||
Some(6000.0 - 5.0 * 20.0 * 5.0),
|
||||
Some(10),
|
||||
),
|
||||
// A notch whose tool is flush with the block's front, back and top faces.
|
||||
"subtract_flush_notch" => plain(
|
||||
Op::Boolean(Shape::Box([0.0; 3], [30.0, 20.0, 10.0]), Shape::Box([10.0, 0.0, 5.0], [10.0, 20.0, 5.0]), BooleanOp::Subtract),
|
||||
Some(6000.0 - 10.0 * 20.0 * 5.0),
|
||||
Some(10),
|
||||
),
|
||||
// A 4 mm cube inside a 10 mm one: a void, still one solid.
|
||||
"subtract_inside_void" => plain(Op::Boolean(A, Shape::Box([3.0; 3], [4.0; 3]), BooleanOp::Subtract), Some(1000.0 - 64.0), Some(12)),
|
||||
// abc - πr²c: a 3 mm hole through a 20 x 20 x 10 block.
|
||||
"subtract_cylinder_hole" => plain(
|
||||
Op::Boolean(Shape::Box([0.0; 3], [20.0, 20.0, 10.0]), Shape::Cylinder([10.0, 10.0, -1.0], 3.0, 12.0), BooleanOp::Subtract),
|
||||
Some(4000.0 - PI * 9.0 * 10.0),
|
||||
None,
|
||||
),
|
||||
// Nine 10 mm cubes, each touching its neighbours face to face.
|
||||
"union_all_grid" => plain(
|
||||
Op::UnionAll((0..9).map(|i| Shape::Box([f64::from(i % 3) * 10.0, f64::from(i / 3) * 10.0, 0.0], [10.0; 3])).collect()),
|
||||
Some(9000.0),
|
||||
None,
|
||||
),
|
||||
"intersect_disjoint" => plain(Op::Boolean(A, Shape::Box([20.0; 3], [10.0; 3]), BooleanOp::Intersect), None, None),
|
||||
// A 10 mm sphere less a cap of height h = 5: 4/3 πr³ - πh²(3r - h)/3.
|
||||
// (solidcore#1431, brep_volume of a whole sphere failing at 1e-8, does not touch the cut
|
||||
// sphere: it measures at 1e-8.)
|
||||
"sphere_minus_box" => plain(
|
||||
Op::Boolean(Shape::Sphere([0.0; 3], 10.0), Shape::Box([-20.0, -20.0, 5.0], [40.0, 40.0, 20.0]), BooleanOp::Subtract),
|
||||
Some(4.0 / 3.0 * PI * 1000.0 - PI * 25.0 * 25.0 / 3.0),
|
||||
Some(2),
|
||||
),
|
||||
// A 4 mm square tool through the cube's top face: that face is cut in two (a square and
|
||||
// the ring round it), nothing else changes.
|
||||
"imprint_top_face" => plain(Op::Imprint(A, Shape::Box([3.0, 3.0, 8.0], [4.0; 3])), Some(1000.0), Some(7)),
|
||||
_ => panic!("{name}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs()
|
||||
}
|
||||
|
||||
/// What a kernel's result measures.
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Measured {
|
||||
volume: f64,
|
||||
counts: [usize; 3],
|
||||
/// The record's faces by surface kind.
|
||||
kinds: BTreeMap<String, usize>,
|
||||
solids: usize,
|
||||
}
|
||||
|
||||
fn kinds(rec: &solidcraft_geom::brep::Brep) -> BTreeMap<String, usize> {
|
||||
let mut out = BTreeMap::new();
|
||||
for f in &rec.faces {
|
||||
let v = serde_json::to_value(&f.surface).unwrap();
|
||||
*out.entry(v["kind"].as_str().unwrap_or("?").to_owned()).or_default() += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The case on solidcore: `None` for an empty result.
|
||||
fn on_core(c: &Case) -> crate::Result<Option<Body>> {
|
||||
match &c.op {
|
||||
Op::Boolean(a, b, op) => boolean(&a.core(), &b.core(), *op),
|
||||
Op::UnionAll(shapes) => {
|
||||
let bodies: Vec<Body> = shapes.iter().map(|s| s.core()).collect();
|
||||
union_all(&bodies.iter().collect::<Vec<_>>()).map(Some)
|
||||
}
|
||||
Op::Imprint(a, t) => imprint(&a.core(), &t.core()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `f` on its own thread: a hang fails the test instead of stopping the run.
|
||||
fn timed<T: Send + 'static>(name: &str, f: impl FnOnce() -> T + Send + 'static) -> T {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let worker = std::thread::spawn(move || {
|
||||
let _ = tx.send(f());
|
||||
});
|
||||
match rx.recv_timeout(TIMEOUT) {
|
||||
Ok(v) => v,
|
||||
// The case panicked: its own message is the failure.
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => match worker.join() {
|
||||
Err(p) => std::panic::resume_unwind(p),
|
||||
Ok(()) => panic!("{name}: no result"),
|
||||
},
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => panic!("{name}: solidcore hung (no result after {TIMEOUT:?})"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the case on solidcore and checks it against the analytic answer; what it measured.
|
||||
fn solidcore_case(name: &'static str) -> Option<Measured> {
|
||||
timed(name, move || {
|
||||
let c = case(name);
|
||||
let r = on_core(&c).unwrap_or_else(|e| panic!("{name}: solidcore failed: {e}"));
|
||||
let (Some(body), Some(expected)) = (&r, c.volume) else {
|
||||
assert!(r.is_none() && c.volume.is_none(), "{name}: solidcore gave {}, expected {:?}", r.is_some(), c.volume);
|
||||
return None;
|
||||
};
|
||||
let v = volume(body, 1e-8).unwrap_or_else(|e| panic!("{name}: volume: {e}"));
|
||||
assert!(rel(v, expected) < 1e-9, "{name}: solidcore volume {v} vs analytic {expected}");
|
||||
let n = counts(body).unwrap();
|
||||
if let Some(f) = c.faces {
|
||||
assert_eq!(n[0], f, "{name}: faces {n:?}");
|
||||
}
|
||||
let solids = solid_count(body).unwrap_or_else(|e| panic!("{name}: solid count: {e}"));
|
||||
assert_eq!(solids, c.solids, "{name}: solids");
|
||||
let (rec, _) = native(&to_arrays(body).unwrap()).unwrap_or_else(|e| panic!("{name}: record: {e:?}"));
|
||||
rec.validate().unwrap_or_else(|e| panic!("{name}: record: {e}"));
|
||||
Some(Measured { volume: v, counts: n, kinds: kinds(&rec), solids })
|
||||
})
|
||||
}
|
||||
|
||||
macro_rules! cases {
|
||||
($($m:ident $(: $ign:meta)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
#[test]
|
||||
$(#[$ign])?
|
||||
fn solidcore_matches_the_analytic_volume() {
|
||||
super::solidcore_case(stringify!($m));
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
cases!(
|
||||
union_overlapping,
|
||||
union_disjoint,
|
||||
intersect_overlapping,
|
||||
subtract_through_slot,
|
||||
subtract_flush_notch,
|
||||
subtract_inside_void,
|
||||
subtract_cylinder_hole,
|
||||
union_all_grid,
|
||||
intersect_disjoint,
|
||||
sphere_minus_box,
|
||||
imprint_top_face,
|
||||
);
|
||||
|
||||
/// A `w` x `h` planar sheet on z = 0 from `(x, y)`, solidcore's `CreateRectangle`.
|
||||
fn rectangle(x: f64, y: f64, w: f64, h: f64) -> Body {
|
||||
let frame = Axis2Placement::new(Vec3::new(x, y, 0.0), Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0));
|
||||
run("create_rectangle", |k| create_rectangle(k, 0.0, w, h, &frame)).map(Body::new).unwrap()
|
||||
}
|
||||
|
||||
fn area(b: &Body) -> f64 {
|
||||
with_body("brep_area", b, |k, b| brep_area(k, b, 1e-8)).unwrap()
|
||||
}
|
||||
|
||||
/// Two 10 mm squares overlapping in a 5 mm one, as regions of their plane.
|
||||
#[test]
|
||||
fn boolean2d_combines_overlapping_squares() {
|
||||
for (op, expected) in [(BooleanOp::Union, 175.0), (BooleanOp::Intersect, 25.0), (BooleanOp::Subtract, 75.0)] {
|
||||
let (a, b) = (rectangle(0.0, 0.0, 10.0, 10.0), rectangle(5.0, 5.0, 10.0, 10.0));
|
||||
let r = timed("boolean2d", move || boolean2d(&a, &b, op).map(|r| r.map(|r| (area(&r), counts(&r).unwrap()))));
|
||||
let (got, n) = r.unwrap_or_else(|e| panic!("{op:?}: {e}")).unwrap_or_else(|| panic!("{op:?}: empty"));
|
||||
assert!(rel(got, expected) < 1e-9, "{op:?}: area {got} vs {expected}");
|
||||
assert_eq!(n[0], 1, "{op:?}: {n:?}");
|
||||
}
|
||||
let (a, far) = (rectangle(0.0, 0.0, 10.0, 10.0), rectangle(20.0, 0.0, 10.0, 10.0));
|
||||
assert!(boolean2d(&a, &far, BooleanOp::Intersect).unwrap().is_none(), "disjoint squares intersected");
|
||||
}
|
||||
|
||||
/// `closed_nonmanifold`'s test of an edge, as the capi's: an edge whose wire or edgeuse query
|
||||
/// fails makes the body not closed non-manifold (false), never an error that fails the boolean.
|
||||
#[test]
|
||||
fn closed_nonmanifold_takes_a_failed_edge_query_as_false() {
|
||||
let failed = || solidcore::SolidError::null_pointer("GetEdgeuses");
|
||||
assert!(edge_closes(Ok(false), Ok(4)));
|
||||
assert!(edge_closes(Ok(false), Ok(8)));
|
||||
assert!(!edge_closes(Ok(false), Ok(2)));
|
||||
assert!(!edge_closes(Ok(false), Ok(6)));
|
||||
assert!(!edge_closes(Ok(true), Ok(4)));
|
||||
assert!(!edge_closes(Ok(false), Err(failed())));
|
||||
assert!(!edge_closes(Err(failed()), Ok(4)));
|
||||
}
|
||||
|
||||
/// Two 10 mm cubes touching along one edge, joined: a closed body whose two solids meet along
|
||||
/// that edge (four edgeuses round it), 2000 mm³.
|
||||
#[test]
|
||||
fn cubes_touching_along_an_edge_join_closed_nonmanifold() {
|
||||
let r = timed("edge_touching", || {
|
||||
let a = Shape::Box([0.0; 3], [10.0; 3]).core();
|
||||
let b = Shape::Box([10.0, 10.0, 0.0], [10.0; 3]).core();
|
||||
boolean(&a, &b, BooleanOp::Union).map(|r| r.map(|r| (volume(&r, 1e-8).unwrap(), solid_count(&r).unwrap())))
|
||||
});
|
||||
let (v, solids) = r.unwrap().unwrap();
|
||||
assert!(rel(v, 2000.0) < 1e-9, "volume {v}");
|
||||
assert_eq!(solids, 2);
|
||||
}
|
||||
|
||||
/// The capi's argument checks: the same body imprinted on itself, an empty union.
|
||||
#[test]
|
||||
fn hostile_operands_are_refused() {
|
||||
let a = A.core();
|
||||
assert_eq!(imprint(&a, &a).err(), Some(Error::Argument));
|
||||
assert_eq!(union_all(&[]).err(), Some(Error::Argument));
|
||||
}
|
||||
|
||||
/// A tool that meets no face cuts none.
|
||||
#[test]
|
||||
fn imprint_of_a_far_tool_is_none() {
|
||||
let r = timed("imprint_far", || imprint(&A.core(), &Shape::Box([20.0; 3], [1.0; 3]).core()).map(|r| r.is_none()));
|
||||
assert!(r.unwrap());
|
||||
}
|
||||
|
||||
/// An operand is never changed: the same body twice, and again after.
|
||||
#[test]
|
||||
fn operands_stay_as_they_were() {
|
||||
let r = timed("operands", || {
|
||||
let a = A.core();
|
||||
let b = Shape::Box([5.0; 3], [10.0; 3]).core();
|
||||
let u = boolean(&a, &b, BooleanOp::Union).map(|r| r.is_some());
|
||||
let same = boolean(&a, &a, BooleanOp::Union).map(|r| r.map(|r| volume(&r, 1e-8)));
|
||||
(u, same, counts(&a).unwrap(), volume(&a, 1e-8).unwrap(), counts(&b).unwrap())
|
||||
});
|
||||
let (u, same, ca, va, cb) = r;
|
||||
assert!(u.unwrap());
|
||||
assert_eq!(ca, [6, 12, 8]);
|
||||
assert_eq!(cb, [6, 12, 8]);
|
||||
assert!(rel(va, 1000.0) < 1e-12, "{va}");
|
||||
let v = same.unwrap().unwrap().unwrap();
|
||||
assert!(rel(v, 1000.0) < 1e-9, "A ∪ A: {v}");
|
||||
}
|
||||
|
||||
/// `moved`: a closest-point query that fails, even with an error that is no SMLib status (an edge
|
||||
/// without its curve here), counts as off the piece, as `SmApiEdgeClosestPoint` failing does in the
|
||||
/// capi (smlib_history.cpp:246): the face is Modified, the operation stands.
|
||||
#[test]
|
||||
fn a_failed_closest_point_counts_as_off_the_piece() {
|
||||
use solidcore::query::api_queries::edge_closest_point;
|
||||
use solidcore::topology::edge;
|
||||
let body = A.core();
|
||||
super::with_body("moved", &body, |k, b| {
|
||||
let e = b.edge_list()[0];
|
||||
let (Some(s), Some(f)) = edge::start_end_vertices(b, e)? else { panic!("an edge with no ends") };
|
||||
let (s, f) = (b.vertex(s)?.point, b.vertex(f)?.point);
|
||||
let at = [s, f, s + (f - s) * 0.5];
|
||||
let tol = b.tolerance();
|
||||
assert!(!super::history::moved(k, b, &[(e, &at)], tol)?, "still where it lay");
|
||||
b.edge_mut(e)?.curve = None;
|
||||
assert!(edge_closest_point(k, b, e, at[2]).is_err_and(|e| e.sm_status().is_none()), "fails with no status");
|
||||
assert!(super::history::moved(k, b, &[(e, &at)], tol)?, "off the piece");
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// is 113 pi whatever e is.
|
||||
/// This path passes with `simple_coincidence_crossing_is_point` off too, so the test holds the
|
||||
/// result here and does not single out that flag (solidcore's own test does).
|
||||
#[test]
|
||||
fn a_ring_and_a_lip_with_walls_nearly_together() {
|
||||
use super::sweep::extrude;
|
||||
use crate::sweeps::Curve;
|
||||
use crate::sweeps::DraftCorner;
|
||||
const Z: [f64; 3] = [0.0, 0.0, 1.0];
|
||||
let up = |p: &[Curve], d| extrude(p, Z, d, 0.0, DraftCorner::default(), true).unwrap();
|
||||
let arc = |c: [f64; 3], radius, start, end| Curve::Arc { center: c, axis: Z, reference: [1.0, 0.0, 0.0], radius, start, end };
|
||||
let disc = |r| up(&[arc([0.0, 10.0, 0.0], r, 0.0, 2.0 * PI)], 2.0);
|
||||
let ring = boolean(&disc(5.0), &disc(3.0), BooleanOp::Subtract).unwrap().unwrap();
|
||||
for e in [0.0, 1e-4, 1e-3, 1e-2] {
|
||||
let c = [e, 10.0, 2.0];
|
||||
let lip = up(
|
||||
&[
|
||||
arc(c, 5.0, 0.0, PI),
|
||||
Curve::Line { start: [e - 5.0, 10.0, 2.0], end: [e - 4.0, 10.0, 2.0] },
|
||||
Curve::Arc { center: c, axis: [0.0, 0.0, -1.0], reference: [-1.0, 0.0, 0.0], radius: 4.0, start: 0.0, end: PI },
|
||||
Curve::Line { start: [e + 4.0, 10.0, 2.0], end: [e + 5.0, 10.0, 2.0] },
|
||||
],
|
||||
18.0,
|
||||
);
|
||||
let u = boolean(&ring, &lip, BooleanOp::Union).unwrap().unwrap();
|
||||
let v = volume(&u, 1e-8).unwrap();
|
||||
let report = super::check(&u, crate::check::CheckLevel::All).unwrap();
|
||||
assert!(report.is_valid(), "e {e}: {report}");
|
||||
assert_eq!(solid_count(&u).unwrap(), 1, "e {e}");
|
||||
assert!(super::is_manifold_solid(&u).unwrap(), "e {e}");
|
||||
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:?}");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,284 @@
|
||||
//! solidcore's primitives through the flat arrays: each exports to a valid record of the right
|
||||
//! 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;
|
||||
|
||||
use solidcraft_geom::brep as b;
|
||||
|
||||
use super::boolean::boolean;
|
||||
use super::{Body, from_arrays, make_box, make_cone, make_cylinder, make_sphere, make_torus, to_arrays, volume};
|
||||
use crate::BooleanOp;
|
||||
use crate::export::native;
|
||||
use crate::import::{self, Arrays};
|
||||
|
||||
/// A primitive: solidcore's body and its analytic volume.
|
||||
struct Case {
|
||||
body: Body,
|
||||
volume: f64,
|
||||
}
|
||||
|
||||
fn case(name: &str) -> Case {
|
||||
let (body, volume) = match name {
|
||||
"box" => (make_box([1.0, -2.0, 3.0], [10.0, 20.0, 30.0]), 6000.0),
|
||||
"cylinder" => (make_cylinder([1.0, 2.0, -3.0], 5.0, 12.0), PI * 25.0 * 12.0),
|
||||
"sphere" => (make_sphere([1.0, 2.0, 3.0], 7.0), 4.0 / 3.0 * PI * 343.0),
|
||||
"cone" => (make_cone([0.0, 1.0, 2.0], 6.0, 0.0, 9.0), PI * 36.0 * 9.0 / 3.0),
|
||||
"frustum" => (make_cone([0.0, 1.0, 2.0], 6.0, 2.0, 9.0), PI * 9.0 / 3.0 * (36.0 + 12.0 + 4.0)),
|
||||
"torus" => (make_torus([1.0, -1.0, 0.5], 10.0, 3.0), 2.0 * PI * PI * 10.0 * 9.0),
|
||||
_ => panic!("{name}"),
|
||||
};
|
||||
Case { body: body.unwrap(), volume }
|
||||
}
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs()
|
||||
}
|
||||
|
||||
/// The record of solidcore's arrays, validated and its geometry checked.
|
||||
fn record(a: &Arrays) -> b::Brep {
|
||||
let (rec, _) = native(a).unwrap();
|
||||
rec.validate().unwrap();
|
||||
rec.check_geometry().unwrap();
|
||||
rec
|
||||
}
|
||||
|
||||
/// Every number in `a` within `tol` (relative past 1) of `b`'s, everything else equal.
|
||||
fn close(a: &serde_json::Value, b: &serde_json::Value, tol: f64, at: &str) {
|
||||
use serde_json::Value as V;
|
||||
match (a, b) {
|
||||
(V::Number(x), V::Number(y)) => {
|
||||
let (x, y) = (x.as_f64().unwrap(), y.as_f64().unwrap());
|
||||
assert!((x - y).abs() <= tol * x.abs().max(1.0), "{at}: {x} vs {y}");
|
||||
}
|
||||
(V::Array(x), V::Array(y)) => {
|
||||
assert_eq!(x.len(), y.len(), "{at}: length");
|
||||
for (i, (x, y)) in x.iter().zip(y).enumerate() {
|
||||
close(x, y, tol, &format!("{at}[{i}]"));
|
||||
}
|
||||
}
|
||||
(V::Object(x), V::Object(y)) => {
|
||||
assert_eq!(x.keys().collect::<Vec<_>>(), y.keys().collect::<Vec<_>>(), "{at}: keys");
|
||||
for (k, x) in x {
|
||||
close(x, &y[k], tol, &format!("{at}.{k}"));
|
||||
}
|
||||
}
|
||||
_ => assert_eq!(a, b, "{at}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// to_arrays gives a record that validates and whose geometry checks.
|
||||
fn exports(name: &str) {
|
||||
record(&to_arrays(&case(name).body).unwrap());
|
||||
}
|
||||
|
||||
/// solidcore's body measures its analytic volume.
|
||||
fn measures(name: &str) {
|
||||
let c = case(name);
|
||||
let v = volume(&c.body, 1e-8).unwrap();
|
||||
assert!(rel(v, c.volume) < 1e-9, "{name}: solidcore volume {v} vs {}", c.volume);
|
||||
}
|
||||
|
||||
/// record -> arrays -> from_arrays -> to_arrays -> record is the record, the volume exact.
|
||||
fn round_trips(name: &str) {
|
||||
let c = case(name);
|
||||
let rec = record(&to_arrays(&c.body).unwrap());
|
||||
let back = from_arrays(&import::arrays(&rec, true).unwrap()).unwrap();
|
||||
let again = record(&to_arrays(&back).unwrap());
|
||||
close(&serde_json::to_value(&rec).unwrap(), &serde_json::to_value(&again).unwrap(), 1e-12, name);
|
||||
let v = volume(&back, 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 {
|
||||
#[test]
|
||||
fn exports_a_valid_record() {
|
||||
super::exports($name);
|
||||
}
|
||||
#[test]
|
||||
$(#[$vol])?
|
||||
fn measures_its_volume() {
|
||||
super::measures($name);
|
||||
}
|
||||
#[test]
|
||||
$(#[$rt])?
|
||||
fn round_trips_through_the_record() {
|
||||
super::round_trips($name);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
primitive!(box_, "box");
|
||||
primitive!(cylinder, "cylinder");
|
||||
primitive!(sphere, "sphere");
|
||||
primitive!(cone, "cone");
|
||||
primitive!(frustum, "frustum");
|
||||
primitive!(torus, "torus");
|
||||
|
||||
#[test]
|
||||
fn hostile_arrays_are_refused() {
|
||||
let c = case("box");
|
||||
let good = to_arrays(&c.body).unwrap();
|
||||
assert!(from_arrays(&Arrays::default()).is_err());
|
||||
for id in good.ints.keys().copied().collect::<Vec<_>>() {
|
||||
let mut a = good.clone();
|
||||
if let Some(v) = a.ints.get_mut(&id) {
|
||||
v.push(7);
|
||||
}
|
||||
let _ = from_arrays(&a);
|
||||
let mut a = good.clone();
|
||||
if let Some(v) = a.ints.get_mut(&id).and_then(|v| v.first_mut()) {
|
||||
*v = i64::MAX;
|
||||
}
|
||||
let _ = from_arrays(&a);
|
||||
}
|
||||
for id in good.reals.keys().copied().collect::<Vec<_>>() {
|
||||
let mut a = good.clone();
|
||||
if let Some(v) = a.reals.get_mut(&id).and_then(|v| v.first_mut()) {
|
||||
*v = f64::NAN;
|
||||
assert!(from_arrays(&a).is_err(), "NaN in {id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `GetOrCreateGwNurbPointer` on a parabola: the one with its NURB gives it; one built over an
|
||||
/// interval too small for two end tangents to cross has none and makes none, an error naming
|
||||
/// `MakeNurb`.
|
||||
#[test]
|
||||
fn a_parabola_gives_its_nurb_or_says_it_cannot() {
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::curve::parabola::Parabola;
|
||||
use solidcore::geom::extent1d::Extent1d;
|
||||
use solidcore::geom::vector3d::{Point3, Vec3};
|
||||
let (x, y) = (Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0));
|
||||
let part = |lo: f64, hi: f64| -> crate::Result<bool> {
|
||||
let c = Curve::from(Parabola::new(Point3::new(0.0, 0.0, 0.0), x, y, Extent1d::from_min_max(lo, hi), 1.0, 3));
|
||||
let k = solidcore::kernel::Kernel::default();
|
||||
Ok(super::convert::bspline_curve_part(&k, &c)?.is_some_and(|b| b.gw_nurb().is_some()))
|
||||
};
|
||||
assert_eq!(part(-1.0, 1.0), Ok(true));
|
||||
let none = part(0.5, 0.5);
|
||||
assert!(matches!(&none, Err(e) if e.to_string().contains("MakeNurb")), "{none:?}");
|
||||
}
|
||||
|
||||
/// 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 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() {
|
||||
use crate::flat::{EDGE_TOLERANCE, TOLERANCE, VERTEX_TOLERANCE};
|
||||
let mut a = to_arrays(&case("box").body).unwrap();
|
||||
let base = a.reals[&TOLERANCE][0];
|
||||
assert!(a.reals[&EDGE_TOLERANCE].iter().chain(&a.reals[&VERTEX_TOLERANCE]).all(|&t| t == base), "{a:?}");
|
||||
a.reals.get_mut(&EDGE_TOLERANCE).unwrap()[0] = 8.0 * base;
|
||||
a.reals.get_mut(&TOLERANCE).unwrap()[0] = 8.0 * base;
|
||||
for round in 0..3 {
|
||||
let body = from_arrays(&a).unwrap();
|
||||
assert_eq!(body.0.lock().unwrap().tolerance(), base / 2.0, "round {round}");
|
||||
a = to_arrays(&body).unwrap();
|
||||
assert_eq!(a.reals[&TOLERANCE], [base], "round {round}");
|
||||
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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A 20 mm box minus a centred 10 mm box: a sealed void, so its solid region has two shells.
|
||||
fn sealed_void() -> Body {
|
||||
boolean(&make_box([-10.0; 3], [20.0; 3]).unwrap(), &make_box([-5.0; 3], [10.0; 3]).unwrap(), BooleanOp::Subtract).unwrap().unwrap()
|
||||
}
|
||||
|
||||
/// 20³ − 10³.
|
||||
const SEALED_VOID_VOLUME: f64 = 8000.0 - 1000.0;
|
||||
|
||||
/// Each bounded region's shells, as each shell's reach: the largest |coordinate| of its vertices
|
||||
/// (10 for the 20 mm box, 5 for the 10 mm one).
|
||||
fn reaches(rec: &b::Brep) -> Vec<Vec<f64>> {
|
||||
let reach = |s: u32| {
|
||||
let Some(b::Shell::Faces { faces, .. }) = rec.shells.get(s as usize) else { panic!("shell {s}") };
|
||||
let mut r = 0.0f64;
|
||||
for f in faces {
|
||||
for l in &rec.faces[f.face as usize].loops {
|
||||
let b::Loop::Edges { edges } = l else { continue };
|
||||
for e in edges {
|
||||
for &v in &rec.edges[e.edge as usize].vertices {
|
||||
r = rec.vertices[v as usize].point.iter().fold(r, |r, c| r.max(c.abs()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
r
|
||||
};
|
||||
rec.regions.iter().skip(1).map(|r| r.shells.iter().map(|&s| reach(s)).collect()).collect()
|
||||
}
|
||||
|
||||
/// The sealed void's record: the solid region lists the outer box first, then the void's box;
|
||||
/// the void region its box alone.
|
||||
fn outer_first(a: &Arrays, at: &str) {
|
||||
let mut got = reaches(&record(a));
|
||||
got.sort_by_key(|x| std::cmp::Reverse(x.len()));
|
||||
assert_eq!(got, [vec![10.0, 5.0], vec![5.0]], "{at}");
|
||||
}
|
||||
|
||||
/// The arrays with every region's shells listed in reverse: each shell's block (its counts and
|
||||
/// its face uses) moved, nothing else touched.
|
||||
fn shells_reversed(a: &Arrays) -> Arrays {
|
||||
use crate::flat::{FACEUSE_FACE, FACEUSE_ORIENTATION, REGION_SHELL_COUNT, SHELL_FACEUSE_COUNT, SHELL_POINT_TYPE, SHELL_WIRE_EDGE_COUNT};
|
||||
let mut p = a.clone();
|
||||
let blocks = [SHELL_FACEUSE_COUNT, SHELL_WIRE_EDGE_COUNT, SHELL_POINT_TYPE, FACEUSE_FACE, FACEUSE_ORIENTATION];
|
||||
for id in blocks {
|
||||
p.ints.get_mut(&id).unwrap().clear();
|
||||
}
|
||||
let (mut shell, mut fu) = (0usize, 0usize);
|
||||
for &n in &a.ints[®ION_SHELL_COUNT] {
|
||||
let mut region = Vec::new();
|
||||
for _ in 0..n {
|
||||
let uses = usize::try_from(a.ints[&SHELL_FACEUSE_COUNT][shell]).unwrap();
|
||||
region.push((shell, fu..fu + uses));
|
||||
shell += 1;
|
||||
fu += uses;
|
||||
}
|
||||
for (s, uses) in region.into_iter().rev() {
|
||||
for id in [SHELL_FACEUSE_COUNT, SHELL_WIRE_EDGE_COUNT, SHELL_POINT_TYPE] {
|
||||
let v = a.ints[&id][s];
|
||||
p.ints.get_mut(&id).unwrap().push(v);
|
||||
}
|
||||
for id in [FACEUSE_FACE, FACEUSE_ORIENTATION] {
|
||||
p.ints.get_mut(&id).unwrap().extend_from_slice(&a.ints[&id][uses.clone()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_ne!(p.ints, a.ints, "nothing to reverse");
|
||||
p
|
||||
}
|
||||
|
||||
/// solidcore's sealed void exports with its outer shell first, comes back through the arrays
|
||||
/// with it first, and keeps its volume.
|
||||
#[test]
|
||||
fn a_sealed_void_crosses_the_arrays_outer_shell_first() {
|
||||
let body = sealed_void();
|
||||
let v = volume(&body, 1e-9).unwrap();
|
||||
assert!(rel(v, SEALED_VOID_VOLUME) < 1e-9, "{v}");
|
||||
let a = to_arrays(&body).unwrap();
|
||||
outer_first(&a, "export");
|
||||
let back = from_arrays(&import::arrays(&record(&a), true).unwrap()).unwrap();
|
||||
outer_first(&to_arrays(&back).unwrap(), "round trip");
|
||||
let v = volume(&back, 1e-9).unwrap();
|
||||
assert!(rel(v, SEALED_VOID_VOLUME) < 1e-9, "round trip: {v}");
|
||||
}
|
||||
|
||||
/// Arrays listing the solid region's void shell before the box around it import with the box
|
||||
/// first (SMLib takes a region's first shell as its enclosing one), the volume unchanged.
|
||||
#[test]
|
||||
fn shells_listed_inner_first_import_outer_first() {
|
||||
let a = shells_reversed(&to_arrays(&sealed_void()).unwrap());
|
||||
let back = from_arrays(&a).unwrap();
|
||||
outer_first(&to_arrays(&back).unwrap(), "inner first");
|
||||
let v = volume(&back, 1e-9).unwrap();
|
||||
assert!(rel(v, SEALED_VOID_VOLUME) < 1e-9, "inner first: {v}");
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//! What a failing solidcore call leaves behind, and which one is running: `smlib_set_dump_dir` and
|
||||
//! `smlib_current_call` for this kernel. While a folder is set, each call's B-rep inputs are
|
||||
//! written to memory before it runs (it may edit them) and to files only if it fails:
|
||||
//! `<n>-<call>.txt`, what failed, and `<n>-<call>-<input>.smb`, each input as a binary `.smb`, which
|
||||
//! `Brep::read` reads back.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU8, AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::{Mutex, PoisonError};
|
||||
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::topology::Brep;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
static DIR: Mutex<Option<PathBuf>> = Mutex::new(None);
|
||||
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
/// The inputs of the call running now, in the order it took them.
|
||||
static INPUTS: Mutex<Vec<(&'static str, std::result::Result<Vec<u8>, String>)>> = Mutex::new(Vec::new());
|
||||
|
||||
/// Where failing calls leave their inputs; `None` stops it.
|
||||
pub(crate) fn set_dir(dir: Option<PathBuf>) {
|
||||
*DIR.lock().unwrap_or_else(PoisonError::into_inner) = dir;
|
||||
}
|
||||
|
||||
fn dumping() -> bool {
|
||||
DIR.lock().unwrap_or_else(PoisonError::into_inner).is_some()
|
||||
}
|
||||
|
||||
/// A new call, with no inputs yet.
|
||||
pub(super) fn begin() {
|
||||
if dumping() {
|
||||
INPUTS.lock().unwrap_or_else(PoisonError::into_inner).clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// `b` is an input of the running call, by the name `what`.
|
||||
pub(crate) fn input(k: &Kernel, what: &'static str, b: &Brep) {
|
||||
if dumping() {
|
||||
let bytes = super::file::bytes(k, b, false).map_err(|e| e.to_string());
|
||||
INPUTS.lock().unwrap_or_else(PoisonError::into_inner).push((what, bytes));
|
||||
}
|
||||
}
|
||||
|
||||
fn safe(call: &str) -> String {
|
||||
call.chars().map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' }).collect()
|
||||
}
|
||||
|
||||
/// The running call failed: its inputs go to the folder. Failing to write them fails nothing else.
|
||||
pub(super) fn failed(call: &str, why: &str) {
|
||||
let Some(dir) = DIR.lock().unwrap_or_else(PoisonError::into_inner).clone() else { return };
|
||||
let n = SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
||||
let stem = format!("{n:06}-{}", safe(call));
|
||||
let mut text = format!("{call} -> {why}\nnumeric arguments are not recorded\n");
|
||||
for (what, bytes) in INPUTS.lock().unwrap_or_else(PoisonError::into_inner).drain(..) {
|
||||
match bytes {
|
||||
Ok(b) => {
|
||||
let name = format!("{stem}-{what}.smb");
|
||||
if let Err(e) = std::fs::write(dir.join(&name), b) {
|
||||
log::warn!(target: "smlib", "{name}: {e}");
|
||||
}
|
||||
text.push_str(&format!("input {what}: {name}\n"));
|
||||
}
|
||||
Err(e) => text.push_str(&format!("input {what}: could not be written: {e}\n")),
|
||||
}
|
||||
}
|
||||
if let Err(e) = std::fs::write(dir.join(format!("{stem}.txt")), text) {
|
||||
log::warn!(target: "smlib", "{stem}.txt: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// How a call's value reads: whether it is a refusal, for the calls that answer with one.
|
||||
pub(crate) trait Answer {
|
||||
fn refusal(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Answer for Result<T> {
|
||||
fn refusal(&self) -> Option<String> {
|
||||
self.as_ref().err().map(Error::to_string)
|
||||
}
|
||||
}
|
||||
|
||||
/// The values that are no refusal.
|
||||
macro_rules! plain {
|
||||
($($t:ty),* $(,)?) => { $(impl Answer for $t {})* };
|
||||
}
|
||||
plain!(
|
||||
(),
|
||||
bool,
|
||||
f64,
|
||||
usize,
|
||||
[usize; 3],
|
||||
([f64; 3], [f64; 3]),
|
||||
Vec<u8>,
|
||||
Vec<[f64; 3]>,
|
||||
Brep,
|
||||
solidcore::geom::vector3d::Point3,
|
||||
crate::history::Raw,
|
||||
crate::MassProperties
|
||||
);
|
||||
|
||||
impl<T> Answer for Option<T> {}
|
||||
|
||||
/// The longest call name [`current`] gives.
|
||||
const NAME_MAX: usize = 96;
|
||||
static NAME: [AtomicU8; NAME_MAX] = [const { AtomicU8::new(0) }; NAME_MAX];
|
||||
static NAME_LEN: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// `call` is running now (`None`: none is). One at a time, under the kernel's lock.
|
||||
pub(super) fn set_current(call: Option<&str>) {
|
||||
NAME_LEN.store(0, Ordering::SeqCst);
|
||||
let Some(call) = call else { return };
|
||||
let bytes = call.as_bytes();
|
||||
let n = bytes.len().min(NAME_MAX);
|
||||
for (slot, b) in NAME.iter().zip(bytes) {
|
||||
slot.store(*b, Ordering::Relaxed);
|
||||
}
|
||||
NAME_LEN.store(n, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// The call running now. Takes no lock, for crash reports, so it may be cut short or mixed with the
|
||||
/// next call's name if read just as one starts.
|
||||
pub(crate) fn current() -> Option<String> {
|
||||
let n = NAME_LEN.load(Ordering::SeqCst);
|
||||
if n == 0 {
|
||||
return None;
|
||||
}
|
||||
let bytes: Vec<u8> = NAME.iter().take(n).map(|b| b.load(Ordering::Relaxed)).collect();
|
||||
Some(String::from_utf8_lossy(&bytes).into_owned())
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! `.smb` files on solidcore (`smlib_brep_read`, `smlib_brep_write`): the B-rep's contents through
|
||||
//! `SmApiReadBrepFromFile` and `SmApiWriteBrepToFile`, the file itself read and written here (the
|
||||
//! library touches no files, so that it builds for the browser).
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::topology::api_files::{read_brep_from_file, write_brep_to_file};
|
||||
use solidcore::topology::brep::p10::TopologyEnv;
|
||||
use solidcore::topology::brep_data_write::FromBrepEnv;
|
||||
use solidcore::topology::topology::NotifyRecords;
|
||||
use solidcore::topology::{Brep, EdgeId, EdgeuseId, FaceId};
|
||||
|
||||
use super::convert::key_of;
|
||||
use super::{Body, run, with_body};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// Where the keys of a file's curves and surfaces start, clear of [`key_of`]'s entity keys.
|
||||
const FILE_KEYS: u64 = 1 << 50;
|
||||
|
||||
fn io(what: &str, path: &Path, e: &std::io::Error) -> Error {
|
||||
Error::Solidcore(format!("{what} {}: {e}", path.display()))
|
||||
}
|
||||
|
||||
/// The body in the file at `path`, ASCII or binary. Its UV trims are checked, not rebuilt, as the
|
||||
/// C API reads them.
|
||||
pub(crate) fn read(path: &Path, ascii: bool) -> Result<Body> {
|
||||
let data = std::fs::read(path).map_err(|e| io("reading", path, &e))?;
|
||||
let b = run("smlib_brep_read", |k| {
|
||||
let mut attributes = AttributeStore::default();
|
||||
let mut records = NotifyRecords::new();
|
||||
let mut env = TopologyEnv { all_attributes: &[], attributes: &mut attributes, key_of: &key_of, records: &mut records, next_key: FILE_KEYS };
|
||||
let mut b = read_brep_from_file(k, Some(data), ascii, false, &mut env)?;
|
||||
b.refresh_cached_topology();
|
||||
Ok(b)
|
||||
})?;
|
||||
Ok(Body::new(b))
|
||||
}
|
||||
|
||||
/// `b` as the contents of a `.smb` file, ASCII or binary.
|
||||
pub(super) fn bytes(k: &Kernel, b: &Brep, ascii: bool) -> solidcore::Result<Vec<u8>> {
|
||||
let store = AttributeStore::default();
|
||||
let records = NotifyRecords::new();
|
||||
let env = FromBrepEnv {
|
||||
attributes: &store,
|
||||
key_of: &key_of,
|
||||
records: &records,
|
||||
curve_key: &|_: EdgeId| ObjectKey(0),
|
||||
uv_curve_key: &|_: EdgeuseId| ObjectKey(0),
|
||||
surface_key: &|_: FaceId| ObjectKey(0),
|
||||
};
|
||||
write_brep_to_file(k, Some(b), &env, ascii)
|
||||
}
|
||||
|
||||
/// `body` written to the file at `path`, ASCII or binary.
|
||||
pub(crate) fn write(body: &Body, path: &Path, ascii: bool) -> Result<()> {
|
||||
let bytes = with_body("smlib_brep_write", body, |k, b| bytes(k, b, ascii))?;
|
||||
std::fs::write(path, bytes).map_err(|e| io("writing", path, &e))
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
//! Face and edge history on solidcore: a port of the fork's `capi/src/smlib_history.cpp`
|
||||
//! (`smlib_history_tag`, `smlib_history_read`) onto solidcore's B-rep-owned attribute store
|
||||
//! ([`Brep::add_attribute`]), which solidcore's operations carry through their edits as SMLib's
|
||||
//! `SmAObject::Notify` does: Split when a face or edge is cut in two, Merge when two become one,
|
||||
//! Copy when an object is copied (a boolean copies the second operand into the first), Coincident
|
||||
//! where a boolean finds one operand's entity lying on the other's. New topology (a boolean's
|
||||
//! intersection edges) is made without any.
|
||||
//!
|
||||
//! The capi's `Tag` is one application attribute ([`Attribute::application`]) per face or edge,
|
||||
//! with the capi's own handlers ([`AttributeHandlers`]): a split child, a merge's result and the
|
||||
//! entity a coincidence lands on join the origins in, where SMLib's defaults keep only one; a copy
|
||||
//! is SMLib's default. The data, the handlers and the reading are the capi's.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use solidcore::geom::attribute::{
|
||||
ApplicationValue, Attribute, AttributeHandle, AttributeHandlers, AttributeValue, AttributeView, Behavior, ObjectKey, Placement, ids,
|
||||
};
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::query::api_queries::edge_closest_point;
|
||||
use solidcore::topology::face::p4 as face4;
|
||||
use solidcore::topology::{Brep, EdgeId, VertexId, edge};
|
||||
use solidcore::{EntityRef, Point3, SolidError};
|
||||
use solidcraft_geom::brep::history::Change;
|
||||
|
||||
use super::{Body, answer, with_body};
|
||||
use crate::history::{Raw, RawEdge, RawFace};
|
||||
use crate::{Error, Result};
|
||||
use solidcore::topology::brep::p3::brep_copy;
|
||||
|
||||
/// `TAG_ID`: above SMLib's own ids (all below `SM_AI_USER_1`).
|
||||
const TAG_ID: u64 = ids::USER_1 + 7;
|
||||
|
||||
/// Operand numbers and indices a tag accepts (`TAG_MAX`: keeps every value inside 30 bits).
|
||||
const TAG_MAX: usize = 1 << 30;
|
||||
|
||||
/// One (operand, index) pair a face or edge stands for.
|
||||
type Origin = (usize, usize);
|
||||
|
||||
/// The capi's `Tag`: what a face or edge stands for, the operand entities it came from, for a face
|
||||
/// tagged directly its own boundary edges then (to tell a face that is still whole), and for an
|
||||
/// edge tagged directly where it lay then (points along it, ends first: to tell an edge that
|
||||
/// moved). Copy behaviour (`SM_AB_COPY`); a copy (`MakeCopy`) is the same data.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
struct Tag {
|
||||
origins: BTreeSet<Origin>,
|
||||
edges: BTreeSet<i64>,
|
||||
at: Vec<Point3>,
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
fn attribute(self) -> Attribute {
|
||||
Attribute::application(TAG_ID, Behavior::Copy, self)
|
||||
}
|
||||
}
|
||||
|
||||
/// `Tag::on`: the tag on `x`, if the attribute of its id there is a [`Tag`].
|
||||
fn on<'a>(view: &AttributeView<'a>, x: ObjectKey) -> Option<(AttributeHandle, &'a Tag)> {
|
||||
let (h, a) = view.find_attribute(x, TAG_ID)?;
|
||||
a.application_value::<Tag>().map(|t| (h, t))
|
||||
}
|
||||
|
||||
/// `Tag::join`: adds `o` and `e` to `x`'s tag (its `at` as it was), giving it one with no `at` if
|
||||
/// it has none (an attribute of the tag's id that is no tag is replaced). Nothing for no `o`.
|
||||
fn join(view: &AttributeView<'_>, x: ObjectKey, o: &BTreeSet<Origin>, e: &BTreeSet<i64>) -> Vec<Placement> {
|
||||
if o.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
if let Some((handle, t)) = on(view, x) {
|
||||
let mut t = t.clone();
|
||||
t.origins.extend(o.iter().copied());
|
||||
t.edges.extend(e.iter().copied());
|
||||
return vec![Placement::Edit { handle, value: AttributeValue::Application(ApplicationValue::new(t)) }];
|
||||
}
|
||||
set(view, x, o.clone(), e.clone(), Vec::new())
|
||||
}
|
||||
|
||||
/// `Tag::set`: replaces whatever tag `x` has (the attribute of the tag's id) with exactly `o`, `e`
|
||||
/// and `p`.
|
||||
fn set(view: &AttributeView<'_>, x: ObjectKey, o: BTreeSet<Origin>, e: BTreeSet<i64>, p: Vec<Point3>) -> Vec<Placement> {
|
||||
let mut out = Vec::with_capacity(2);
|
||||
if let Some((handle, _)) = view.find_attribute(x, TAG_ID) {
|
||||
out.push(Placement::Remove { on: x, handle });
|
||||
}
|
||||
out.push(Placement::Add { on: x, attribute: Tag { origins: o, edges: e, at: p }.attribute() });
|
||||
out
|
||||
}
|
||||
|
||||
impl AttributeHandlers for Tag {
|
||||
/// A split: both children stand for what the split object stood for, and lie where it lay
|
||||
/// (each a piece of it): a child with no tag gets this one's `at` too.
|
||||
fn split(&self, view: &AttributeView<'_>, split: ObjectKey, a: ObjectKey, b: ObjectKey) -> solidcore::Result<Vec<Placement>> {
|
||||
let mut out = Vec::new();
|
||||
for x in [a, b] {
|
||||
if x == split {
|
||||
continue;
|
||||
}
|
||||
if on(view, x).is_none() && !self.origins.is_empty() {
|
||||
out.extend(set(view, x, self.origins.clone(), self.edges.clone(), self.at.clone()));
|
||||
} else {
|
||||
out.extend(join(view, x, &self.origins, &self.edges));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// A merge: the result stands for both (the tags on the sources other than the result).
|
||||
fn merge(&self, view: &AttributeView<'_>, a: ObjectKey, b: ObjectKey, result: ObjectKey) -> solidcore::Result<Vec<Placement>> {
|
||||
let (mut o, mut e) = (BTreeSet::new(), BTreeSet::new());
|
||||
for x in [a, b] {
|
||||
if x != result
|
||||
&& let Some((_, t)) = on(view, x)
|
||||
{
|
||||
o.extend(t.origins.iter().copied());
|
||||
e.extend(t.edges.iter().copied());
|
||||
}
|
||||
}
|
||||
Ok(join(view, result, &o, &e))
|
||||
}
|
||||
|
||||
/// One operand's entity found lying on the other's, which survives: it stands for both.
|
||||
fn coincident(&self, view: &AttributeView<'_>, _from: ObjectKey, to: ObjectKey) -> solidcore::Result<Vec<Placement>> {
|
||||
Ok(join(view, to, &self.origins, &self.edges))
|
||||
}
|
||||
}
|
||||
|
||||
/// `read`: what `e`'s tag says (the attribute of the tag's id, if it is a [`Tag`]).
|
||||
fn tag_on(b: &Brep, e: EntityRef) -> Option<&Tag> {
|
||||
let (_, a) = b.entity_attributes(e).into_iter().find(|(_, a)| a.attribute_id() == TAG_ID)?;
|
||||
a.application_value::<Tag>()
|
||||
}
|
||||
|
||||
/// `Tag::set` at tagging: replaces whatever tag `e` has with exactly origin `o`, the boundary
|
||||
/// edges `edges` (a face's) and the points `at` (an edge's).
|
||||
fn set_on(b: &mut Brep, e: EntityRef, o: Origin, edges: BTreeSet<i64>, at: Vec<Point3>) -> std::result::Result<(), SolidError> {
|
||||
let old = b.entity_attributes(e).into_iter().find(|(_, a)| a.attribute_id() == TAG_ID).map(|(h, _)| h);
|
||||
if let Some(h) = old {
|
||||
b.remove_attribute(e, h)?;
|
||||
}
|
||||
b.add_attribute(e, Tag { origins: BTreeSet::from([o]), edges, at }.attribute())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `samples`: points along `e`, its two ends, then three between, evenly in its parameter. Empty
|
||||
/// where it can't be evaluated.
|
||||
fn samples(k: &Kernel, b: &Brep, e: EdgeId) -> std::result::Result<Vec<Point3>, SolidError> {
|
||||
let edge = b.edge(e)?;
|
||||
let Some(curve) = edge.curve.as_ref() else { return Ok(Vec::new()) };
|
||||
let (lo, hi) = (edge.interval.min(), edge.interval.max());
|
||||
let mut p = Vec::with_capacity(5);
|
||||
for t in [0.0, 1.0, 0.25, 0.5, 0.75] {
|
||||
let Some(v) = answer(curve.evaluate(k, lo + t * (hi - lo), 0, true, false))? else { return Ok(Vec::new()) };
|
||||
let Some(v) = v.first() else { return Ok(Vec::new()) };
|
||||
p.push(Point3::new(v.x, v.y, v.z));
|
||||
}
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
/// `moved`: have `pieces`, the result edges standing for one edge, moved from where its tag says
|
||||
/// it lay (`at`, the first piece's record of at least two points)? Together they must run between
|
||||
/// its ends (the vertices only one piece ends on; none for an edge closed on itself, whose end some
|
||||
/// piece still ends on), and every point it ran through must lie on one of them (within `tol`).
|
||||
/// One piece is the edge still whole; more, the edge cut by vertices. Pieces with no record of
|
||||
/// where the edge lay have not moved.
|
||||
pub(super) fn moved(k: &Kernel, b: &Brep, pieces: &[(EdgeId, &[Point3])], tol: f64) -> std::result::Result<bool, SolidError> {
|
||||
let Some(at) = pieces.iter().map(|(_, at)| *at).find(|at| at.len() >= 2) else { return Ok(false) };
|
||||
let [a, z, ..] = at else { return Ok(false) };
|
||||
let near = |p: &Point3, q: &Point3| p.distance(*q) <= tol;
|
||||
let mut ends: BTreeMap<VertexId, usize> = BTreeMap::new();
|
||||
for (e, _) in pieces {
|
||||
let (Some(s), Some(f)) = edge::start_end_vertices(b, *e)? else { return Ok(true) };
|
||||
*ends.entry(s).or_default() += 1;
|
||||
*ends.entry(f).or_default() += 1;
|
||||
}
|
||||
let mut open = Vec::new();
|
||||
for (v, n) in &ends {
|
||||
if n % 2 == 1 {
|
||||
open.push(b.vertex(*v)?.point);
|
||||
}
|
||||
}
|
||||
let span = match open.as_slice() {
|
||||
[p, q] => (near(p, a) && near(q, z)) || (near(p, z) && near(q, a)),
|
||||
[] => {
|
||||
let mut on_end = false;
|
||||
for v in ends.keys() {
|
||||
on_end = on_end || near(&b.vertex(*v)?.point, a);
|
||||
}
|
||||
near(a, z) && on_end
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !span {
|
||||
return Ok(true);
|
||||
}
|
||||
for q in at.iter().skip(2) {
|
||||
// `SmApiEdgeClosestPoint` failing, however it fails, counts as off a piece, as a distance
|
||||
// above `tol` does: the face is then Modified, and the operation stands.
|
||||
if !pieces.iter().any(|(e, _)| edge_closest_point(k, b, *e, *q).is_ok_and(|c| c.closest.distance <= tol)) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// `smlib_history_tag`: a copy of `body` (`SmApiBrepCopy`) whose face i and edge i stand for face
|
||||
/// i and edge i of operand `operand` (in its face and edge order, which the copy keeps), a face
|
||||
/// also recording its boundary edges then, an edge the points it ran through.
|
||||
pub(crate) fn tag(body: &Body, operand: usize) -> Result<Body> {
|
||||
if operand >= TAG_MAX {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
with_body("smlib_history_tag", body, |k, b| {
|
||||
let mut r = brep_copy(k, b)?;
|
||||
let faces = r.face_list().to_vec();
|
||||
let edges = r.edge_list().to_vec();
|
||||
if faces.len() >= TAG_MAX || edges.len() >= TAG_MAX {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
let edge_at: HashMap<EdgeId, usize> = edges.iter().enumerate().map(|(i, e)| (*e, i)).collect();
|
||||
for (i, f) in faces.iter().enumerate() {
|
||||
let own: BTreeSet<i64> =
|
||||
face4::edges(&r, *f, None, false, None)?.iter().filter_map(|e| edge_at.get(e).and_then(|j| i64::try_from(*j).ok())).collect();
|
||||
set_on(&mut r, EntityRef::Face(*f), (operand, i), own, Vec::new())?;
|
||||
}
|
||||
for (i, e) in edges.iter().enumerate() {
|
||||
let at = samples(k, &r, *e)?;
|
||||
set_on(&mut r, EntityRef::Edge(*e), (operand, i), BTreeSet::new(), at)?;
|
||||
}
|
||||
Ok(Ok(r))
|
||||
})?
|
||||
.map(Body::new)
|
||||
}
|
||||
|
||||
/// `smlib_history_read`: each face's and edge's origins in `result`, a face's change from how
|
||||
/// many origins it has, how many result faces share them and whether its boundary is still
|
||||
/// exactly its origin's edges; each edge's faces. In the body's own face and edge order.
|
||||
pub(crate) fn read(result: &Body) -> Result<Raw> {
|
||||
with_body("smlib_history_read", result, |k, b| {
|
||||
let faces = b.face_list().to_vec();
|
||||
let edges = b.edge_list().to_vec();
|
||||
let face_at: HashMap<_, usize> = faces.iter().enumerate().map(|(i, f)| (*f, i)).collect();
|
||||
let edge_at: HashMap<EdgeId, usize> = edges.iter().enumerate().map(|(i, e)| (*e, i)).collect();
|
||||
let tol = b.tolerance();
|
||||
|
||||
// Each face's origins and the boundary edges they had; each edge's origins and points.
|
||||
let mut fo: Vec<BTreeSet<Origin>> = Vec::with_capacity(faces.len());
|
||||
let mut fe: Vec<BTreeSet<usize>> = Vec::with_capacity(faces.len());
|
||||
let mut eo: Vec<BTreeSet<Origin>> = Vec::with_capacity(edges.len());
|
||||
let mut at: Vec<Vec<Point3>> = Vec::with_capacity(edges.len());
|
||||
let mut face_uses: BTreeMap<Origin, usize> = BTreeMap::new();
|
||||
let mut edge_uses: BTreeMap<Origin, usize> = BTreeMap::new();
|
||||
for f in &faces {
|
||||
let (o, e) = tag_on(b, EntityRef::Face(*f)).map(|t| (t.origins.clone(), t.edges.clone())).unwrap_or_default();
|
||||
for x in &o {
|
||||
*face_uses.entry(*x).or_default() += 1;
|
||||
}
|
||||
fe.push(e.into_iter().filter_map(|x| usize::try_from(x).ok()).collect());
|
||||
fo.push(o);
|
||||
}
|
||||
for e in &edges {
|
||||
let (o, p) = tag_on(b, EntityRef::Edge(*e)).map(|t| (t.origins.clone(), t.at.clone())).unwrap_or_default();
|
||||
for x in &o {
|
||||
*edge_uses.entry(*x).or_default() += 1;
|
||||
}
|
||||
at.push(p);
|
||||
eo.push(o);
|
||||
}
|
||||
|
||||
let mut out_faces = Vec::with_capacity(faces.len());
|
||||
for (i, f) in faces.iter().enumerate() {
|
||||
let (Some(o), Some(own)) = (fo.get(i), fe.get(i)) else { continue };
|
||||
let change = match o.iter().next() {
|
||||
None => Change::Generated,
|
||||
Some(_) if o.len() > 1 => Change::Merged,
|
||||
Some(first) if face_uses.get(first).copied().unwrap_or(0) > 1 => Change::Split,
|
||||
Some(first) => {
|
||||
// Kept when its boundary is still exactly where the edges it had were: each
|
||||
// boundary edge one of its own operand's, standing for one edge only, every
|
||||
// piece of that edge on this face (an edge only cut by a vertex, as splitting
|
||||
// the face beside it cuts it, is still all there), the pieces where it lay (an
|
||||
// edge an operation moved, as a draft moves a tilted face's neighbours' edges,
|
||||
// leaves those faces trimmed differently), and those edges exactly the ones it
|
||||
// had.
|
||||
let mut pieces: BTreeMap<usize, Vec<(EdgeId, &[Point3])>> = BTreeMap::new();
|
||||
let mut whole = true;
|
||||
for e in face4::edges(b, *f, None, false, None)? {
|
||||
let Some(j) = edge_at.get(&e).copied() else {
|
||||
whole = false;
|
||||
break;
|
||||
};
|
||||
let (Some(origins), Some(points)) = (eo.get(j), at.get(j)) else {
|
||||
whole = false;
|
||||
break;
|
||||
};
|
||||
let only = match origins.iter().next() {
|
||||
Some(x) if origins.len() == 1 => *x,
|
||||
_ => {
|
||||
whole = false;
|
||||
break;
|
||||
}
|
||||
};
|
||||
whole = only.0 == first.0;
|
||||
if !whole {
|
||||
break;
|
||||
}
|
||||
let p = pieces.entry(only.1).or_default();
|
||||
if !p.iter().any(|(x, _)| *x == e) {
|
||||
p.push((e, points.as_slice()));
|
||||
}
|
||||
}
|
||||
let mut now = BTreeSet::new();
|
||||
for (edge, p) in &pieces {
|
||||
whole = whole && p.len() == edge_uses.get(&(first.0, *edge)).copied().unwrap_or(0) && !moved(k, b, p, tol)?;
|
||||
now.insert(*edge);
|
||||
}
|
||||
if whole && now == *own { Change::Kept } else { Change::Modified }
|
||||
}
|
||||
};
|
||||
out_faces.push(RawFace { change, origins: o.iter().copied().collect() });
|
||||
}
|
||||
|
||||
let mut out_edges = Vec::with_capacity(edges.len());
|
||||
for (i, e) in edges.iter().enumerate() {
|
||||
let bounds: BTreeSet<usize> = edge::faces(b, *e, None)?.iter().filter_map(|f| face_at.get(f).copied()).collect();
|
||||
let origins = eo.get(i).map(|o| o.iter().copied().collect()).unwrap_or_default();
|
||||
out_edges.push(RawEdge { faces: bounds.into_iter().collect(), origins });
|
||||
}
|
||||
Ok(Raw { faces: out_faces, edges: out_edges })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
//! The Measure and Check families on solidcore: what `capi/src/smlib_measure.cpp`,
|
||||
//! `smlib_brep_solid_count` (`smlib_ops.cpp`), `smlib_brep_is_manifold_solid`
|
||||
//! (`smlib_flat_api.cpp`) and `smlib_brep_check` (`smlib_check.cpp`) do on the C++ kernel, here
|
||||
//! composed from solidcore's ports of the same SMLib routines.
|
||||
//!
|
||||
//! [`check`] fails with "not supported yet" while a test it reaches is unported in solidcore.
|
||||
|
||||
use solidcore::error::EntityRef;
|
||||
use solidcore::geom::assert_array::{AssertArray, AssertReport, obj_type_string};
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::geom::tol::{SmallTopoType, brep_zone_tol_3d, context_xsect_tol_3d};
|
||||
use solidcore::heal::gap::GapFunction;
|
||||
use solidcore::intersect::point_class::PointClassType;
|
||||
use solidcore::mass::integrator::properties::{MassPropertyBehavior, PropertyAttributes, brep_properties, face_properties};
|
||||
use solidcore::nurbs::b_spline_surface_n_lib::p2::OrientType;
|
||||
use solidcore::query::api_queries::{brep_bounding_box, brep_is_manifold_solid, brep_material_census, face_bounding_box};
|
||||
use solidcore::query::brep::BrepClassifier;
|
||||
use solidcore::topology::brep::p1 as bl;
|
||||
use solidcore::topology::brep::validation::{CheckLevel as SolidcoreCheckLevel, check_brep};
|
||||
use solidcore::topology::face::p2::{PPF_AREA, PPF_AREA_FIRST_MOMENTS};
|
||||
use solidcore::topology::{FaceId, FaceuseId, NamingScheme};
|
||||
use solidcore::{Kernel, Point3, SolidError, Vec3};
|
||||
|
||||
use super::blend::status;
|
||||
use super::{Body, with_body};
|
||||
use crate::check::{CheckLevel, CheckReport};
|
||||
use crate::measure::{MassProperties, PointClass};
|
||||
use crate::{Error, Result};
|
||||
|
||||
const OP: &str = "smlib_measure";
|
||||
|
||||
/// The accuracy the C API passes on: finite, clamped to [1e-8, 1e-1].
|
||||
fn accuracy(relative_accuracy: f64) -> Result<f64> {
|
||||
if !relative_accuracy.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(relative_accuracy.clamp(1e-8, 1e-1))
|
||||
}
|
||||
|
||||
fn invalid(why: &'static str) -> SolidError {
|
||||
SolidError::invalid_input(OP, why)
|
||||
}
|
||||
|
||||
fn finite(v: &[f64]) -> bool {
|
||||
v.iter().all(|x| x.is_finite())
|
||||
}
|
||||
|
||||
/// `faces_area` of `smlib_measure.cpp`: every face counted once (SmApiBrepComputeArea, with the
|
||||
/// [1e-8, 1e-1] envelope instead of its [1e-4, 1e-1]), the total area and, when `first` is set,
|
||||
/// the area's first moments about `o`. With `only_sheets`, only [`sheet_face`]s (the capi's
|
||||
/// `counted`).
|
||||
fn faces_area(k: &Kernel, b: &mut solidcore::Brep, accuracy: f64, o: Point3, first: bool, only_sheets: bool) -> solidcore::Result<(f64, Vec3)> {
|
||||
let mut area = 0.0;
|
||||
let mut sum = Vec3::new(0.0, 0.0, 0.0);
|
||||
let flags = if first { PPF_AREA | PPF_AREA_FIRST_MOMENTS } else { PPF_AREA };
|
||||
for f in bl::faces(b, None)? {
|
||||
if only_sheets && !sheet_face(b, f)? {
|
||||
continue;
|
||||
}
|
||||
let size = face_bounding_box(k, b, f, false)?.size();
|
||||
let edge = size.z.max(size.x.max(size.y));
|
||||
let p = face_properties(k, b, f, OrientType::Same, accuracy, o, edge * edge / 100.0, 0.0, flags)?;
|
||||
// The area and its moments share one integrand, so a trim boundary integrated the other
|
||||
// way round flips both.
|
||||
let sign = if p.area < 0.0 { -1.0 } else { 1.0 };
|
||||
area += sign * p.area;
|
||||
if first {
|
||||
sum += p.moments[0] * sign;
|
||||
}
|
||||
}
|
||||
if !area.is_finite() {
|
||||
return Err(invalid("non-finite area"));
|
||||
}
|
||||
Ok((area, sum))
|
||||
}
|
||||
|
||||
/// `sheet_face`: a face with material on neither side, standing free or hung on a solid (a blend
|
||||
/// sheet stitched between two boxes). `SmBrep::ComputePreciseProperties` takes its area only from
|
||||
/// faces between a solid region and a void, so such a face is counted on its own.
|
||||
fn sheet_face(b: &solidcore::Brep, f: FaceId) -> solidcore::Result<bool> {
|
||||
let void_side = |u: Option<FaceuseId>| -> solidcore::Result<bool> {
|
||||
let Some(u) = u else { return Ok(true) };
|
||||
let Some(shell) = b.faceuse(u)?.shell else { return Ok(true) };
|
||||
let Some(region) = b.shell(shell)?.region else { return Ok(true) };
|
||||
// IsVoid's UNSURE (2) is true in C++: only FALSE is material.
|
||||
Ok(b.region(region)?.is_void() != 0)
|
||||
};
|
||||
let (x, y) = solidcore::topology::face::p2::faceuses(b, f)?;
|
||||
Ok(void_side(x)? && void_side(y)?)
|
||||
}
|
||||
|
||||
/// `smlib_brep_mass_properties`: `SmBrep::ComputePreciseProperties` directly, as the C API calls
|
||||
/// it (not `SmApiBrepComputeMassProperties`: the [1e-8, 1e-1] accuracy envelope, no manifold
|
||||
/// refusal, the sum over material regions), the face-by-face area and its centroid for a body
|
||||
/// with no material, and moments about the middle of the box moved to the centroid. A body's area
|
||||
/// is every face's: the solid's boundary and its sheet faces (fork 9767916).
|
||||
pub(crate) fn mass_properties(body: &Body, relative_accuracy: f64) -> Result<MassProperties> {
|
||||
let accuracy = accuracy(relative_accuracy)?;
|
||||
with_body("mass_properties", body, |k, b| {
|
||||
let mut material = false;
|
||||
for r in bl::regions(b, None)? {
|
||||
// IsVoid's UNSURE (2) is true in C++: only FALSE is material.
|
||||
material |= b.region(r)?.is_void() == 0;
|
||||
}
|
||||
// The C API refuses a body with per-entity mass attributes (they would override the unit
|
||||
// density). Bodies here carry none: the attribute store passed in is empty.
|
||||
let store = AttributeStore::default();
|
||||
let attrs = PropertyAttributes { store: &store, key_of: &|_| ObjectKey(0) };
|
||||
// Moments about the middle of the box, moved to the centroid below: about the world
|
||||
// origin, a body far from it would lose its inertia to cancellation.
|
||||
let o = brep_bounding_box(k, b, false)?.mid();
|
||||
if !finite(&[o.x, o.y, o.z]) {
|
||||
return Err(invalid("non-finite box"));
|
||||
}
|
||||
if !material {
|
||||
let (area, first) = faces_area(k, b, accuracy, o, true, false)?;
|
||||
if !(area > 0.0) {
|
||||
return Err(invalid("no area"));
|
||||
}
|
||||
let c = [o.x + first.x / area, o.y + first.y / area, o.z + first.z / area];
|
||||
if !finite(&[area, c[0], c[1], c[2]]) {
|
||||
return Err(invalid("non-finite result"));
|
||||
}
|
||||
return Ok(MassProperties { area, volume: 0.0, centroid: c, moments: [0.0; 3], products: [0.0; 3] });
|
||||
}
|
||||
let p = brep_properties(k, b, accuracy, o, MassPropertyBehavior { density: 1.0, ..MassPropertyBehavior::default() }, &attrs)?;
|
||||
let (mass, volume) = (p.mass, p.volume);
|
||||
if !mass.is_finite() || !(mass > 0.0) || !(volume > 0.0) {
|
||||
return Err(invalid("no mass"));
|
||||
}
|
||||
// m[4]: first moments about o; m[7]: (y²+z², z²+x², x²+y²) about o; m[6]: (yz, zx, xy)
|
||||
// about o. Parallel axes from o to the centroid (unit density: mass = volume).
|
||||
let [_, _, _, _, m4, _, m6, m7] = p.moments;
|
||||
let d = [m4.x / mass, m4.y / mass, m4.z / mass];
|
||||
let (sheets, _) = faces_area(k, b, accuracy, o, false, true)?;
|
||||
let r = MassProperties {
|
||||
area: p.area + sheets,
|
||||
volume,
|
||||
centroid: [o.x + d[0], o.y + d[1], o.z + d[2]],
|
||||
moments: [
|
||||
m7.x - mass * (d[1] * d[1] + d[2] * d[2]),
|
||||
m7.y - mass * (d[0] * d[0] + d[2] * d[2]),
|
||||
m7.z - mass * (d[0] * d[0] + d[1] * d[1]),
|
||||
],
|
||||
products: [m6.x - mass * d[1] * d[2], m6.y - mass * d[2] * d[0], m6.z - mass * d[0] * d[1]],
|
||||
};
|
||||
if !finite(&[r.area, r.volume]) || !finite(&r.centroid) || !finite(&r.moments) || !finite(&r.products) {
|
||||
return Err(invalid("non-finite result"));
|
||||
}
|
||||
Ok(r)
|
||||
})
|
||||
}
|
||||
|
||||
/// `smlib_brep_area`: every face counted once.
|
||||
pub(crate) fn area(body: &Body, relative_accuracy: f64) -> Result<f64> {
|
||||
let accuracy = accuracy(relative_accuracy)?;
|
||||
with_body("area", body, |k, b| faces_area(k, b, accuracy, Point3::new(0.0, 0.0, 0.0), false, false).map(|(a, _)| a))
|
||||
}
|
||||
|
||||
/// `smlib_brep_bounds`: `SmBrep::CalculateTightBoundingBox`.
|
||||
pub(crate) fn bounds(body: &Body) -> Result<([f64; 3], [f64; 3])> {
|
||||
with_body("bounds", body, |k, b| {
|
||||
let x = brep_bounding_box(k, b, true)?;
|
||||
let (lo, hi) = (x.min(), x.max());
|
||||
let (lo, hi) = ([lo.x, lo.y, lo.z], [hi.x, hi.y, hi.z]);
|
||||
if !finite(&lo) || !finite(&hi) || lo.iter().zip(&hi).any(|(a, b)| a > b) {
|
||||
return Err(invalid("no box"));
|
||||
}
|
||||
Ok((lo, hi))
|
||||
})
|
||||
}
|
||||
|
||||
/// `smlib_brep_edge_face_distance`: SMLib's own edge/face gap (`SmEdgeuse::CalcMaxEdgeFaceGap`'s
|
||||
/// `SmCrvSrfGapFunction` with 20 samples, its largest sample). Indices in the body's edge and
|
||||
/// face order.
|
||||
pub(crate) fn edge_face_distance(body: &Body, edge: usize, face: usize) -> Result<f64> {
|
||||
let found = with_body("edge_face_distance", body, |k, b| {
|
||||
let (Some(&e), Some(&f)) = (bl::edges(b, None)?.get(edge), bl::faces(b, None)?.get(face)) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let interval = b.edge(e)?.interval;
|
||||
let (Some(curve), Some(surface)) = (b.edge(e)?.curve.as_ref(), b.face(f)?.surface.as_ref()) else {
|
||||
return Err(invalid("edge without curve or face without surface"));
|
||||
};
|
||||
if !(interval.max() > interval.min()) {
|
||||
return Err(invalid("empty edge interval"));
|
||||
}
|
||||
// SmTol::GetXSectTol3d(brep's context): the process context. The tolerance only
|
||||
// classifies the samples; it does not change the distances.
|
||||
let tol = context_xsect_tol_3d(k, None, SmallTopoType::NormalSize);
|
||||
let mut gaps = GapFunction::crv_srf(tol, curve, &interval, surface, 20);
|
||||
let d = gaps.max_gap_sample(k, false)?.length();
|
||||
if !d.is_finite() {
|
||||
return Err(invalid("non-finite gap"));
|
||||
}
|
||||
Ok(Some(d))
|
||||
})?;
|
||||
found.ok_or(Error::Argument)
|
||||
}
|
||||
|
||||
/// `smlib_brep_classify_point`: `SmBrep::Point3DClassify` at the body's own zone tolerance
|
||||
/// (`SmTol::GetZoneTol3d` of the body), with boundary intersections, so a point within that
|
||||
/// tolerance of a vertex, an edge or a face is on the boundary and never decided by a ray; else
|
||||
/// the region it lies in, outside when that region is void (`IsVoid() == TRUE`; `UNSURE` is
|
||||
/// material, as the capi compares with `TRUE`). Runs on a private copy when a solve needs one
|
||||
/// (`BrepClassifier`): upstream's `const` method may add UV trim curves to the body.
|
||||
pub(crate) fn classify_point(body: &Body, p: [f64; 3]) -> Result<PointClass> {
|
||||
if !finite(&p) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
with_body("classify_point", body, |k, b| {
|
||||
let tol = brep_zone_tol_3d(k, b, SmallTopoType::NormalSize);
|
||||
let mut classifier = BrepClassifier::of_brep(b);
|
||||
let pc = match classifier.point_3d_classify(k, Point3::new(p[0], p[1], p[2]), tol, true) {
|
||||
Ok(pc) => pc,
|
||||
// Its status, as the capi passes it on (no ray finding a region is SM_ERR).
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
};
|
||||
Ok(match (pc.point_class(), pc.object()) {
|
||||
(PointClassType::Vertex | PointClassType::Edge | PointClassType::Face, _) => Ok(PointClass::OnBoundary),
|
||||
(PointClassType::Region, Some(EntityRef::Region(r))) => {
|
||||
Ok(if classifier.brep().region(r)?.is_void() == 1 { PointClass::Outside } else { PointClass::Inside })
|
||||
}
|
||||
// Point3DClassify reports only the four classes above on success: SM_ERR.
|
||||
_ => Err(Error::Status(SM_ERR)),
|
||||
})
|
||||
})?
|
||||
}
|
||||
|
||||
/// `SM_ERR`.
|
||||
const SM_ERR: i32 = 1001;
|
||||
|
||||
/// `smlib_brep_is_manifold_solid`: `SmApiBrepIsManifoldSolid`.
|
||||
pub(crate) fn is_manifold_solid(body: &Body) -> Result<bool> {
|
||||
with_body("is_manifold_solid", body, |_, b| brep_is_manifold_solid(b))
|
||||
}
|
||||
|
||||
/// `smlib_brep_solid_count`: the solids of `SmApiBrepMaterialCensus` (its voids are dropped).
|
||||
pub(crate) fn solid_count(body: &Body) -> Result<usize> {
|
||||
with_body("solid_count", body, |_, b| brep_material_census(b).map(|(solids, _voids)| solids))
|
||||
}
|
||||
|
||||
/// SMLib's `SM_SUCCESS`, the status `SmBrep::ValidatePointers` returns when it finds nothing.
|
||||
const SM_SUCCESS: i32 = 1000;
|
||||
|
||||
/// The capi's `text()`: an empty string reads as `?`.
|
||||
fn text(s: &str) -> &str {
|
||||
if s.is_empty() { "?" } else { s }
|
||||
}
|
||||
|
||||
/// The capi's `line()`: `<reporting type>::Test #<n> <name>: <message> [owner <type>]`, the owner
|
||||
/// tag only when the report names the owner's type.
|
||||
fn report_line(r: &AssertReport) -> String {
|
||||
let msg = r.format_log_message();
|
||||
let mut s = format!("{}::Test #{} {}: {}", text(obj_type_string(r.reporting_type)), r.test_index, text(r.name), text(&msg));
|
||||
if let Some(owner) = r.owner_type_string.filter(|o| !o.is_empty()) {
|
||||
s.push_str(" [owner ");
|
||||
s.push_str(owner);
|
||||
s.push(']');
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// The capi's `collect()`: appends the failed reports of `list`, one line each, and counts them;
|
||||
/// SMLib's warnings ([`AssertReport::is_warning`]: `SmBrep` test 21, "Suspicious Shell Count", a
|
||||
/// body of several faces with one shell each, is one) are neither.
|
||||
fn collect(list: &AssertArray, out: &mut String, failures: &mut usize) {
|
||||
for r in list.reports().iter().filter(|r| !r.ok && !r.is_warning()) {
|
||||
*failures += 1;
|
||||
out.push_str(&report_line(r));
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/// Why [`check`] could not run.
|
||||
enum Refusal {
|
||||
/// A test solidcore has not ported (its [`SolidError::Unported`]).
|
||||
Unported(&'static str, &'static str),
|
||||
/// A body whose geometry owners' identities the check cannot know: a
|
||||
/// [`NamingScheme::Unknown`] body.
|
||||
Naming(NamingScheme),
|
||||
}
|
||||
|
||||
impl<T> super::dump::Answer for std::result::Result<T, Refusal> {
|
||||
fn refusal(&self) -> Option<String> {
|
||||
self.is_err().then(|| "the check could not run".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// `smlib_brep_check` (`smlib_check.cpp`): `SmBrep::ValidatePointers` at `level`, then, when it
|
||||
/// failed nothing, `SmBrep::AssertValid` at `level` over the whole topology graph, with every
|
||||
/// failed test as one line of the report and the capi's own lines for a failed status without a
|
||||
/// report. solidcore's [`check_brep`] runs the same two calls.
|
||||
///
|
||||
/// The geometry owners are named as the body's naming scheme names them: `check_brep` reads a
|
||||
/// [`NamingScheme::Caller`] body's keys from where its maker recorded them (solidcore#1585: an
|
||||
/// array import through `make_topology_from_data`, `api_brep_copy`); where it recorded none, the
|
||||
/// arena identities the binders name such an entity by. A [`NamingScheme::Unknown`] body would be
|
||||
/// checked under arena identities no maker uses: every edge would fail `SmBrep` test 61 on the
|
||||
/// owner of its curve.
|
||||
///
|
||||
/// A test solidcore has not ported yet (its [`SolidError::Unported`]), or a body whose geometry
|
||||
/// owners' identities the check cannot know, fails the check with "not supported yet": the check
|
||||
/// never reports a body valid on fewer tests than the C++ kernel runs.
|
||||
pub(crate) fn check(body: &Body, level: CheckLevel) -> Result<CheckReport> {
|
||||
let lv = match level {
|
||||
CheckLevel::Basic => SolidcoreCheckLevel::Level0,
|
||||
CheckLevel::Geometry => SolidcoreCheckLevel::Level1,
|
||||
CheckLevel::All => SolidcoreCheckLevel::Level2,
|
||||
};
|
||||
let c = match with_body("check", body, |k, b| {
|
||||
let naming = b.naming();
|
||||
if naming == NamingScheme::Unknown {
|
||||
return Ok(Err(Refusal::Naming(naming)));
|
||||
}
|
||||
match check_brep(k, b, lv) {
|
||||
Err(SolidError::Unported { upstream, unit }) => Ok(Err(Refusal::Unported(upstream, unit))),
|
||||
r => r.map(Ok),
|
||||
}
|
||||
})? {
|
||||
Ok(c) => c,
|
||||
Err(Refusal::Unported(upstream, unit)) => {
|
||||
return Err(Error::Solidcore(format!("smlib_brep_check: {upstream}: not supported yet: solidcore has not ported it (unit {unit})")));
|
||||
}
|
||||
Err(Refusal::Naming(n)) => {
|
||||
return Err(Error::Solidcore(format!(
|
||||
"smlib_brep_check: not supported yet: the body names its geometry owners by its maker's keys (NamingScheme::{n:?}), which the check cannot know"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let mut text = String::new();
|
||||
let mut failures = 0usize;
|
||||
collect(&c.pointer_reports, &mut text, &mut failures);
|
||||
if c.pointers_status != SM_SUCCESS && failures == 0 {
|
||||
failures += 1;
|
||||
text.push_str(&format!("SmBrep::ValidatePointers failed with status {}\n", c.pointers_status));
|
||||
}
|
||||
if failures > 0 {
|
||||
// The capi does not run AssertValid after a failed pointer check. check_brep runs it when
|
||||
// the status failed without a report; that result is dropped here, as the capi never
|
||||
// computes it.
|
||||
text.push_str("AssertValid skipped: ValidatePointers failed\n");
|
||||
} else {
|
||||
let Some(ok) = c.assert_valid else {
|
||||
return Err(Error::Solidcore("smlib_brep_check: AssertValid did not run after a clean pointer check".into()));
|
||||
};
|
||||
collect(&c.reports, &mut text, &mut failures);
|
||||
if !ok && failures == 0 {
|
||||
failures += 1;
|
||||
text.push_str("SmBrep::AssertValid failed without a report\n");
|
||||
}
|
||||
}
|
||||
Ok(CheckReport { failures, text })
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
//! 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.
|
||||
|
||||
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};
|
||||
use crate::check::CheckLevel;
|
||||
use crate::measure::MEASURE_ACCURACY;
|
||||
use crate::measure::closed::{self, Closed};
|
||||
|
||||
/// The tolerance every quantity meets, relative.
|
||||
const TOL: f64 = 1e-9;
|
||||
|
||||
/// The primitives, as `convert_tests` makes them, with their closed forms.
|
||||
fn case(name: &str) -> (Body, Closed) {
|
||||
let (body, c) = match name {
|
||||
"box" => (make_box([1.0, -2.0, 3.0], [10.0, 20.0, 30.0]), closed::cuboid([1.0, -2.0, 3.0], [10.0, 20.0, 30.0])),
|
||||
"cylinder" => (make_cylinder([1.0, 2.0, -3.0], 5.0, 12.0), closed::cylinder([1.0, 2.0, -3.0], 5.0, 12.0)),
|
||||
"sphere" => (make_sphere([1.0, 2.0, 3.0], 7.0), closed::sphere([1.0, 2.0, 3.0], 7.0)),
|
||||
"cone" => (make_cone([0.0, 1.0, 2.0], 6.0, 0.0, 9.0), closed::cone([0.0, 1.0, 2.0], 6.0, 0.0, 9.0)),
|
||||
"frustum" => (make_cone([0.0, 1.0, 2.0], 6.0, 2.0, 9.0), closed::cone([0.0, 1.0, 2.0], 6.0, 2.0, 9.0)),
|
||||
"torus" => (make_torus([1.0, -1.0, 0.5], 10.0, 3.0), closed::torus([1.0, -1.0, 0.5], 10.0, 3.0)),
|
||||
_ => panic!("{name}"),
|
||||
};
|
||||
(body.unwrap(), c)
|
||||
}
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs()
|
||||
}
|
||||
|
||||
/// The body's size: twice its largest radius of gyration, the scale of centroid errors.
|
||||
fn size(c: &Closed) -> f64 {
|
||||
2.0 * (c.moments.iter().fold(0.0f64, |a, &b| a.max(b)) / c.volume).sqrt()
|
||||
}
|
||||
|
||||
fn mass_properties_match(name: &str) {
|
||||
let (b, c) = case(name);
|
||||
let m = mass_properties(&b, MEASURE_ACCURACY).unwrap();
|
||||
assert!(rel(m.area, c.area) < TOL, "{name}: area {} vs {}", m.area, c.area);
|
||||
assert!(rel(m.volume, c.volume) < TOL, "{name}: volume {} vs {}", m.volume, c.volume);
|
||||
let imax = c.moments.iter().fold(0.0f64, |a, &b| a.max(b));
|
||||
for i in 0..3 {
|
||||
assert!((m.centroid[i] - c.centroid[i]).abs() < TOL * size(&c), "{name}: centroid {:?} vs {:?}", m.centroid, c.centroid);
|
||||
assert!(rel(m.moments[i], c.moments[i]) < TOL, "{name}: moments {:?} vs {:?}", m.moments, c.moments);
|
||||
assert!(m.products[i].abs() < TOL * imax, "{name}: products {:?}", m.products);
|
||||
}
|
||||
}
|
||||
|
||||
fn area_matches(name: &str) {
|
||||
let (b, c) = case(name);
|
||||
let a = area(&b, MEASURE_ACCURACY).unwrap();
|
||||
assert!(rel(a, c.area) < TOL, "{name}: area {a} vs {}", c.area);
|
||||
}
|
||||
|
||||
fn bounds_match(name: &str) {
|
||||
let (b, c) = case(name);
|
||||
let (lo, hi) = bounds(&b).unwrap();
|
||||
let s = size(&c);
|
||||
for i in 0..3 {
|
||||
assert!((lo[i] - c.bounds.0[i]).abs() < TOL * s, "{name}: low {lo:?} vs {:?}", c.bounds.0);
|
||||
assert!((hi[i] - c.bounds.1[i]).abs() < TOL * s, "{name}: high {hi:?} vs {:?}", c.bounds.1);
|
||||
}
|
||||
}
|
||||
|
||||
fn one_solid(name: &str) {
|
||||
let (b, _) = case(name);
|
||||
assert_eq!(solid_count(&b).unwrap(), 1, "{name}");
|
||||
}
|
||||
|
||||
fn manifold_solid(name: &str) {
|
||||
let (b, _) = case(name);
|
||||
assert!(is_manifold_solid(&b).unwrap(), "{name}");
|
||||
}
|
||||
|
||||
/// Every edge lies on the faces it bounds: its distance to the nearest face is 0.
|
||||
fn edges_lie_on_their_faces(name: &str) {
|
||||
let (b, c) = case(name);
|
||||
let [faces, edges, _] = counts(&b).unwrap();
|
||||
for e in 0..edges {
|
||||
let mut d: Vec<f64> = (0..faces).filter_map(|f| edge_face_distance(&b, e, f).ok()).collect();
|
||||
d.sort_by(f64::total_cmp);
|
||||
assert!(d.first().is_some_and(|&d| d < TOL * size(&c)), "{name}: edge {e}: {d:?}");
|
||||
}
|
||||
assert!(edge_face_distance(&b, edges, 0).is_err() && edge_face_distance(&b, 0, faces).is_err());
|
||||
}
|
||||
|
||||
fn checks_valid(name: &str) {
|
||||
let (b, _) = case(name);
|
||||
for level in [CheckLevel::Basic, CheckLevel::Geometry, CheckLevel::All] {
|
||||
let r = check(&b, level).unwrap();
|
||||
assert!(r.is_valid(), "{name}, {level:?}: {r}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Refusing is decided on the body's naming scheme alone. An Unknown body is refused rather than
|
||||
/// reported invalid on every edge; a Caller one without recorded keys is checked under the arena
|
||||
/// identities (`check_brep` has no error for it, so no error of its can turn into a refusal).
|
||||
#[test]
|
||||
fn check_refuses_only_an_unknown_naming() {
|
||||
use solidcore::topology::NamingScheme;
|
||||
let name = |naming| {
|
||||
let (b, _) = case("box");
|
||||
b.0.lock().unwrap().set_naming(naming);
|
||||
check(&b, CheckLevel::All)
|
||||
};
|
||||
let refused = name(NamingScheme::Unknown).unwrap_err().to_string();
|
||||
assert!(refused.contains("not supported yet") && refused.contains("NamingScheme::Unknown"), "{refused}");
|
||||
name(NamingScheme::Caller).unwrap();
|
||||
}
|
||||
|
||||
macro_rules! primitive {
|
||||
($m:ident, $name:literal $(, mass: $mass:meta)? $(, area: $area:meta)?) => {
|
||||
mod $m {
|
||||
use super::*;
|
||||
#[test]
|
||||
$(#[$mass])?
|
||||
fn mass_properties_match_the_closed_forms() {
|
||||
mass_properties_match($name);
|
||||
}
|
||||
#[test]
|
||||
$(#[$area])?
|
||||
fn area_matches_the_closed_form() {
|
||||
area_matches($name);
|
||||
}
|
||||
#[test]
|
||||
fn bounds_match_the_closed_form() {
|
||||
bounds_match($name);
|
||||
}
|
||||
#[test]
|
||||
fn is_one_solid() {
|
||||
one_solid($name);
|
||||
}
|
||||
#[test]
|
||||
fn is_a_manifold_solid() {
|
||||
manifold_solid($name);
|
||||
}
|
||||
#[test]
|
||||
fn edges_lie_on_their_faces() {
|
||||
super::edges_lie_on_their_faces($name);
|
||||
}
|
||||
#[test]
|
||||
fn checks_valid() {
|
||||
super::checks_valid($name);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
primitive!(box_, "box");
|
||||
primitive!(cylinder, "cylinder");
|
||||
primitive!(sphere, "sphere");
|
||||
primitive!(cone, "cone");
|
||||
primitive!(frustum, "frustum");
|
||||
primitive!(torus, "torus");
|
||||
|
||||
/// A valid box passes every test at level 2 (SolidCraft's level), with an empty report.
|
||||
#[test]
|
||||
fn a_valid_box_passes_level_2() {
|
||||
let (b, _) = case("box");
|
||||
let r = check(&b, 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.
|
||||
#[test]
|
||||
fn a_vertex_off_its_edges_fails_the_check() {
|
||||
use crate::flat::VERTEX_POSITION;
|
||||
let (b, _) = case("box");
|
||||
let mut a = super::to_arrays(&b).unwrap();
|
||||
a.reals.get_mut(&VERTEX_POSITION).unwrap()[0] += 1.0;
|
||||
let broken = super::from_arrays(&a).unwrap();
|
||||
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}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_numbers_are_refused() {
|
||||
let (b, _) = case("box");
|
||||
assert_eq!(mass_properties(&b, f64::NAN).unwrap_err(), crate::Error::Argument);
|
||||
assert_eq!(area(&b, f64::INFINITY).unwrap_err(), crate::Error::Argument);
|
||||
assert_eq!(edge_face_distance(&b, usize::MAX, 0).unwrap_err(), crate::Error::Argument);
|
||||
// Out of range is clamped, not refused.
|
||||
assert!(rel(mass_properties(&b, 1e-30).unwrap().volume, 6000.0) < TOL);
|
||||
assert!(rel(mass_properties(&b, 10.0).unwrap().volume, 6000.0) < TOL);
|
||||
}
|
||||
|
||||
/// A 20 mm box minus a centred 10 mm box (a sealed void), as solidcore's boolean makes it and as
|
||||
/// it comes back from arrays listing its solid region's shells inner first.
|
||||
fn sealed_voids() -> [(&'static str, Body); 2] {
|
||||
use crate::BooleanOp;
|
||||
use crate::flat::{FACEUSE_FACE, FACEUSE_ORIENTATION, REGION_SHELL_COUNT, SHELL_FACEUSE_COUNT};
|
||||
let body = super::boolean::boolean(&make_box([-10.0; 3], [20.0; 3]).unwrap(), &make_box([-5.0; 3], [10.0; 3]).unwrap(), BooleanOp::Subtract)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
// The bounded solid region of two shells: swap their blocks (the shell counts match, as do
|
||||
// the face-use counts, six faces each), so only the face uses move.
|
||||
let mut a = super::to_arrays(&body).unwrap();
|
||||
let regions = &a.ints[®ION_SHELL_COUNT];
|
||||
let first = regions.iter().position(|&n| n == 2).unwrap();
|
||||
let shell: usize = regions[..first].iter().map(|&n| usize::try_from(n).unwrap()).sum();
|
||||
let counts = &a.ints[&SHELL_FACEUSE_COUNT];
|
||||
assert_eq!((counts[shell], counts[shell + 1]), (6, 6));
|
||||
let fu: usize = counts[..shell].iter().map(|&n| usize::try_from(n).unwrap()).sum();
|
||||
for id in [FACEUSE_FACE, FACEUSE_ORIENTATION] {
|
||||
let v = a.ints.get_mut(&id).unwrap();
|
||||
let (x, y) = v[fu..fu + 12].split_at_mut(6);
|
||||
x.swap_with_slice(y);
|
||||
}
|
||||
let reordered = super::from_arrays(&a).unwrap();
|
||||
[("solidcore's", body), ("inner first", reordered)]
|
||||
}
|
||||
|
||||
/// The sealed void's mass properties: 8000 − 1000 to 1e-9, centroid at the origin, one solid,
|
||||
/// in either shell order.
|
||||
#[test]
|
||||
fn a_sealed_void_measures_its_volume() {
|
||||
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;
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Two 10 mm squares apart in z = 0, one body of two faces with a shell each (as the top and
|
||||
/// front of a box offset apart across their edge make, kernel case
|
||||
/// `surfaces/cov-offset-two-faces-across-a-sharp-edge-come-apart`): `SmBrep` test 21,
|
||||
/// "Suspicious Shell Count", warns of it, and the capi's check counts no warning.
|
||||
#[test]
|
||||
fn faces_apart_with_a_shell_each_check_valid() {
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::primitive::primitive_creation::p1::create_rectangle;
|
||||
|
||||
use super::boolean::boolean2d;
|
||||
use super::run;
|
||||
use crate::BooleanOp;
|
||||
|
||||
let square = |x: f64| {
|
||||
let frame = Axis2Placement::new(Vec3::new(x, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0));
|
||||
run("create_rectangle", |k| create_rectangle(k, 0.0, 10.0, 10.0, &frame)).map(Body::new).unwrap()
|
||||
};
|
||||
let b = boolean2d(&square(0.0), &square(20.0), BooleanOp::Union).unwrap().unwrap();
|
||||
assert_eq!(counts(&b).unwrap()[0], 2);
|
||||
for level in [CheckLevel::Basic, CheckLevel::Geometry, CheckLevel::All] {
|
||||
let r = check(&b, level).unwrap();
|
||||
assert!(r.is_valid(), "{level:?}: {r}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `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.
|
||||
#[test]
|
||||
fn points_classify_as_the_capi() {
|
||||
use super::classify_point;
|
||||
use crate::measure::PointClass::{Inside, OnBoundary, Outside};
|
||||
|
||||
let (bx, _) = case("box");
|
||||
let (cyl, _) = case("cylinder");
|
||||
let c45 = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let wall = |r: f64| [1.0 + r * c45, 2.0 + r * c45, 3.0];
|
||||
let cases = [
|
||||
("box", [6.0, 8.0, 18.0], Inside),
|
||||
("box", [6.0, 8.0, 34.0], Outside),
|
||||
("box", [0.0, -3.0, 2.0], Outside),
|
||||
("box", [6.0, 8.0, 33.0], OnBoundary),
|
||||
("box", [11.0, 8.0, 33.0], OnBoundary),
|
||||
("box", [11.0, 18.0, 33.0], OnBoundary),
|
||||
("box", [1.0, -2.0, 3.0], OnBoundary),
|
||||
("cylinder", wall(5.0 + 1e-8), OnBoundary),
|
||||
("cylinder", wall(5.0 - 1e-8), OnBoundary),
|
||||
("cylinder", wall(5.0 + 1e-4), Outside),
|
||||
("cylinder", wall(5.0 - 1e-4), Inside),
|
||||
("cylinder", [1.0, 2.0, 3.0], Inside),
|
||||
("cylinder", [1.0, 2.0, 9.0], OnBoundary),
|
||||
];
|
||||
for (name, p, want) in cases {
|
||||
let body = if name == "box" { &bx } else { &cyl };
|
||||
assert_eq!(classify_point(body, p).unwrap(), want, "{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);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[test]
|
||||
fn a_sheet_hung_on_a_solid_counts_in_the_area() {
|
||||
use solidcore::boolean::api_brep::boolean_merge;
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::primitive::api_primitives::{create_box, create_plane};
|
||||
let both = super::run("cube and flap", |k| {
|
||||
let cube = create_box(k, Vec3::new(0.0, 0.0, 0.0), 10.0, 10.0, 10.0)?;
|
||||
let flap = create_plane(k, Vec3::new(10.0, 0.0, 10.0), 10.0, 10.0)?;
|
||||
boolean_merge(k, cube, flap)
|
||||
})
|
||||
.map(Body::new)
|
||||
.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:?}");
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
//! Sewing and merging on solidcore: the C API's `smlib_stitch` (the fork's
|
||||
//! `capi/src/smlib_sheets.cpp`) and `smlib_merge_faces` (`capi/src/smlib_ops.cpp`, with
|
||||
//! `capi/src/smlib_pick.h`'s `pick_edges`).
|
||||
//!
|
||||
//! As there, each works on a copy of its body (`SmApiBrepCopy`). A stitch is `SmBrep::StitchFaces`
|
||||
//! at the caller's tolerance, once, then `SmBrep::OrientTrimmedSurfaces`: outward where the faces
|
||||
//! close into a solid, else only made to agree across their shared edges. `StitchAndOrient` is not
|
||||
//! used, as the capi does not use it: it caps the tolerance and stitches a second time, so the
|
||||
//! caller's tolerance would not be the one that decides.
|
||||
//!
|
||||
//! `StitchFaces` glues an edge only to an edge one face uses: a sheet laid on edges two faces of
|
||||
//! a solid already share (a cover over a box's cut top) is left beside the solid. As the fork's
|
||||
//! e5d9bba, edges still lamina after that pass are glued at the same tolerance to the edges they
|
||||
//! lie on whatever uses those (`bAllowSpineEdges`), on a copy ([`close_onto_shared_edges`]).
|
||||
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::topology::brep::p1 as bl;
|
||||
use solidcore::topology::brep::p2::{ArenaNotifyState, is_manifold_solid};
|
||||
use solidcore::topology::brep::p3::{DeleteFaceOptions, delete_face, orient_trimmed_surfaces, with_kernel_callbacks};
|
||||
use solidcore::topology::brep::p4::stitch_faces;
|
||||
use solidcore::topology::brep::p5::remove_topological_edges_and_vertices;
|
||||
use solidcore::topology::face::p2 as face_p2;
|
||||
use solidcore::topology::topology_traverser::Marks;
|
||||
use solidcore::topology::{Brep, FaceId, RegionId, edge};
|
||||
|
||||
use super::Body;
|
||||
use super::blend::{edge_miss, finish, on_copy, pick_edges, points_ok, status, tolerance_ok};
|
||||
use crate::{Error, Result};
|
||||
use solidcore::topology::brep::p3::brep_copy;
|
||||
|
||||
type Out<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// `smlib_stitch`: the faces of a copy of `body` sewn along the vertices and edges that coincide
|
||||
/// within `tolerance` (finite, positive, at most 1e3), then oriented; with whether the result is a
|
||||
/// closed solid (no lamina edge left and `IsManifoldSolid`).
|
||||
pub(crate) fn stitch(body: &Body, tolerance: f64) -> Result<(Body, bool)> {
|
||||
if !tolerance.is_finite() || !(tolerance > 0.0) || tolerance > 1e3 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut closed = false;
|
||||
let r = on_copy("SmBrep::StitchFaces", body, |k, mut b| {
|
||||
let mut lamina = match stitch_faces(k, &mut b, tolerance, None, false) {
|
||||
Ok(o) => o.lamina_edges,
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
};
|
||||
if lamina != 0 {
|
||||
b = match close_onto_shared_edges(k, b, tolerance, &mut lamina)? {
|
||||
Ok(b) => b,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
}
|
||||
let oriented = with_kernel_callbacks(k, &mut b, |k, b, env| {
|
||||
let mut marks = Marks::default();
|
||||
orient_trimmed_surfaces(k, b, env, &mut marks, true, lamina != 0, None)
|
||||
});
|
||||
if let Err(e) = oriented {
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
b.refresh_cached_topology();
|
||||
closed = lamina == 0 && is_manifold_solid(&b)?;
|
||||
Ok(Ok(b))
|
||||
})?;
|
||||
Ok((r, closed))
|
||||
}
|
||||
|
||||
/// How many regions of `b` are material: neither the infinite region nor a void (`IsVoid`'s
|
||||
/// `UNSURE` counts as void, as the capi's `!IsVoid()`).
|
||||
fn material_regions(b: &Brep) -> solidcore::Result<usize> {
|
||||
let infinite = b.infinite_region();
|
||||
let mut n = 0;
|
||||
for r in bl::regions(b, None)? {
|
||||
n += usize::from(Some(r) != infinite && b.region(r)?.is_void() == 0);
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// The region on the side of `f`'s faceuse `fu` (its shell's), if any.
|
||||
fn region_of(b: &Brep, fu: Option<solidcore::topology::FaceuseId>) -> solidcore::Result<Option<RegionId>> {
|
||||
let Some(fu) = fu else { return Ok(None) };
|
||||
match b.faceuse(fu)?.shell {
|
||||
Some(sh) => Ok(b.shell(sh)?.region),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// `remove_faces_inside`: every face of `b` lying between two material regions is inside the
|
||||
/// part and removed (`DeleteFace(f, TRUE, TRUE)` with the B-rep open for editing): its regions
|
||||
/// are joined, and its edges and vertices nothing else uses go with it.
|
||||
fn remove_faces_inside(k: &mut Kernel, b: &mut Brep) -> solidcore::Result<Out<()>> {
|
||||
let infinite = b.infinite_region();
|
||||
let mut inside: Vec<FaceId> = Vec::new();
|
||||
for f in bl::faces(b, None)? {
|
||||
let (u1, u2) = face_p2::faceuses(b, f)?;
|
||||
let (Some(r1), Some(r2)) = (region_of(b, u1)?, region_of(b, u2)?) else { continue };
|
||||
let material = |r: RegionId| -> solidcore::Result<bool> { Ok(Some(r) != infinite && b.region(r)?.is_void() == 0) };
|
||||
if r1 != r2 && material(r1)? && material(r2)? {
|
||||
inside.push(f);
|
||||
}
|
||||
}
|
||||
let editing = b.editing_enabled();
|
||||
b.set_editing_enabled(true);
|
||||
let mut deleted = Ok(());
|
||||
{
|
||||
let mut state = ArenaNotifyState::new(b);
|
||||
let mut env = state.env(None);
|
||||
let mut marks = Marks::default();
|
||||
for f in inside {
|
||||
let options = DeleteFaceOptions { delete_edges_vertices: true, do_region_nesting: true, ..DeleteFaceOptions::default() };
|
||||
if let Err(e) = delete_face(k, b, &mut env, &mut marks, f, options) {
|
||||
deleted = Err(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
b.set_editing_enabled(editing);
|
||||
b.refresh_cached_topology();
|
||||
Ok(deleted.map_err(status))
|
||||
}
|
||||
|
||||
/// `close_onto_shared_edges` (fork e5d9bba): `b`, stitched as a manifold with `lamina` edges
|
||||
/// left free, those glued at `tolerance` to the edges they lie on whatever uses those
|
||||
/// (`StitchFaces` with `bAllowSpineEdges`) on a copy. The copy is taken, with its count of
|
||||
/// lamina edges, when the gluing closed a space (a new, material region) and, the faces then
|
||||
/// between two material regions removed, no edge has more than two faces; a sheet that closes
|
||||
/// nothing with the solid's faces stays as the manifold stitch left it. A gluing that leaves
|
||||
/// fewer material regions than it found (a sheet with no sector between the faces at its rim:
|
||||
/// glued elsewhere it joins the solid's inside to the outside) is refused, as the capi's
|
||||
/// `SMLIB_ERR_UNSUPPORTED`.
|
||||
fn close_onto_shared_edges(k: &mut Kernel, b: Brep, tolerance: f64, lamina: &mut u32) -> solidcore::Result<Out<Brep>> {
|
||||
let mut c = brep_copy(k, &b)?;
|
||||
let material = material_regions(&c)?;
|
||||
let outcome = match stitch_faces(k, &mut c, tolerance, None, true) {
|
||||
Ok(o) => o,
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
};
|
||||
if outcome.stitched_edges == 0 {
|
||||
return Ok(Ok(b));
|
||||
}
|
||||
if material_regions(&c)? < material {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
if let Err(e) = remove_faces_inside(k, &mut c)? {
|
||||
return Ok(Err(e));
|
||||
}
|
||||
for e in bl::edges(&c, None)? {
|
||||
if edge::is_spine(&c, e)? {
|
||||
return Ok(Ok(b));
|
||||
}
|
||||
}
|
||||
*lamina = outcome.lamina_edges;
|
||||
Ok(Ok(c))
|
||||
}
|
||||
|
||||
/// `smlib_merge_faces`: `SmBrep::RemoveTopologicalEdgesAndVertices` on a copy of `body`, keeping
|
||||
/// the edges nearest `keep` (each within `tolerance`); then `finish` (faces left, and a solid in
|
||||
/// gives a manifold solid).
|
||||
pub(crate) fn merge_faces(body: &Body, keep: &[[f64; 3]], tolerance: f64) -> Result<Body> {
|
||||
if !points_ok(keep, true) || !tolerance_ok(tolerance) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
on_copy("SmBrep::RemoveTopologicalEdgesAndVertices", body, |k, mut b| {
|
||||
let kept = if keep.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
match pick_edges(k, &b, keep, tolerance)? {
|
||||
Ok((edges, _)) => edges,
|
||||
Err(m) => return Ok(Err(edge_miss(m))),
|
||||
}
|
||||
};
|
||||
let solid_in = is_manifold_solid(&b)?;
|
||||
let keep_edges = (!kept.is_empty()).then_some(kept.as_slice());
|
||||
if let Err(e) = remove_topological_edges_and_vertices(k, &mut b, keep_edges, None) {
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
b.refresh_cached_topology();
|
||||
finish(b, solid_in)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! 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
|
||||
//! two boxes unioned, on solidcore.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::curve::line::Line;
|
||||
use solidcore::geom::extent2d::Extent2d;
|
||||
use solidcore::geom::vector2d::Vec2;
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::nurbs::b_spline_curve::p1::KnotType;
|
||||
use solidcore::nurbs::b_spline_surface::p1::{BSplineSurface, BSplineSurfaceForm, CanonicalSurfaceData};
|
||||
use solidcore::primitive::api_primitives::{create_box, create_plane};
|
||||
use solidcore::primitive::api_trim_ops::trim_surface_with_3d_curves;
|
||||
use solidcore::surface::plane::p1::Plane;
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::topology::Brep;
|
||||
use solidcore::topology::brep::p2::ArenaNotifyState;
|
||||
use solidcore::topology::brep::p3::{merge_brep, new_brep};
|
||||
use solidcore::topology::brep::p8::create_face_from_surface;
|
||||
|
||||
use super::merge::{merge_faces, stitch};
|
||||
use super::sweep::build_faces;
|
||||
use super::{Body, area, bounds, counts, make_box, run, to_arrays, volume};
|
||||
use crate::export::native;
|
||||
use crate::{BooleanOp, Error};
|
||||
|
||||
const TOL: f64 = 0.01;
|
||||
|
||||
/// The sides of the cube [0, 10]^3 as (origin, u axis, v axis), each over [0, 10]^2.
|
||||
const SIDES: [([f64; 3], [f64; 3], [f64; 3]); 6] = [
|
||||
([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 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, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]),
|
||||
([10.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]),
|
||||
([0.0, 10.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]),
|
||||
([0.0, 0.0, 10.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
|
||||
];
|
||||
|
||||
fn v3(p: [f64; 3]) -> Vec3 {
|
||||
Vec3::new(p[0], p[1], p[2])
|
||||
}
|
||||
|
||||
/// The sides `sides` of the cube as separate faces of one body, the top lifted by `lift`.
|
||||
fn loose_faces(sides: &[usize], lift: f64) -> Body {
|
||||
run("loose faces", |k| {
|
||||
let mut b = new_brep(k, solidcore::geom::tol::USE_DEFAULT, solidcore::geom::tol::USE_DEFAULT)?;
|
||||
for &i in sides {
|
||||
let Some(&(mut o, u, v)) = SIDES.get(i) else { continue };
|
||||
if i == 5 {
|
||||
o[2] += lift;
|
||||
}
|
||||
let plane = Plane::new(v3(o), v3(u), v3(v), Vec2::new(1.0, 1.0), &Extent2d::from_bounds(0.0, 0.0, 10.0, 10.0));
|
||||
let domain = Extent2d::from_bounds(0.0, 0.0, 10.0, 10.0);
|
||||
build_faces(&mut b, |b, fenv| create_face_from_surface(k, b, Some(Surface::from(plane)), &domain, fenv))?;
|
||||
}
|
||||
b.refresh_cached_topology();
|
||||
Ok(b)
|
||||
})
|
||||
.map(Body::new)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Two 10 mm cubes side by side along x, unioned: one 20 x 10 x 10 block.
|
||||
fn two_boxes() -> Body {
|
||||
let a = make_box([0.0; 3], [10.0; 3]).unwrap();
|
||||
let b = make_box([10.0, 0.0, 0.0], [10.0; 3]).unwrap();
|
||||
super::boolean::boolean(&a, &b, BooleanOp::Union).unwrap().unwrap()
|
||||
}
|
||||
|
||||
/// The cube with its four faces across x = 5 cut there (`split_faces`): ten faces.
|
||||
fn split_cube() -> Body {
|
||||
let plane = crate::ssi::Spec::Plane { origin: [5.0, 0.0, 0.0], xaxis: [0.0, 1.0, 0.0], yaxis: [0.0, 0.0, 1.0], u: [-1.0, 11.0], v: [-1.0, 11.0] };
|
||||
let cube = make_box([0.0; 3], [10.0; 3]).unwrap();
|
||||
super::split::split_faces(&cube, &[0, 1, 2, 3, 4, 5], &[&plane]).unwrap().unwrap().0
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Input {
|
||||
SplitCube,
|
||||
/// [`loose_faces`]: which sides, the top's lift.
|
||||
Loose(&'static [usize], f64),
|
||||
TwoBoxes,
|
||||
Box,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
fn core(self) -> Body {
|
||||
match self {
|
||||
Input::Loose(sides, lift) => loose_faces(sides, lift),
|
||||
Input::TwoBoxes => two_boxes(),
|
||||
Input::SplitCube => split_cube(),
|
||||
Input::Box => make_box([0.0; 3], [10.0; 3]).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Op {
|
||||
Stitch(Input, f64),
|
||||
Merge(Input, &'static [[f64; 3]], f64),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Expect {
|
||||
/// A solid: volume, counts, closed.
|
||||
Solid(f64, [usize; 3], bool),
|
||||
/// A sheet: area, counts, closed.
|
||||
Sheet(f64, [usize; 3], bool),
|
||||
Fails(Error),
|
||||
}
|
||||
|
||||
const ALL: &[usize] = &[0, 1, 2, 3, 4, 5];
|
||||
const OPEN: &[usize] = &[0, 1, 2, 3, 4];
|
||||
|
||||
fn case(name: &str) -> (Op, Expect) {
|
||||
match name {
|
||||
"stitch_six_faces_into_a_cube" => (Op::Stitch(Input::Loose(ALL, 0.0), TOL), Expect::Solid(1000.0, [6, 12, 8], true)),
|
||||
"stitch_five_faces_into_an_open_box" => (Op::Stitch(Input::Loose(OPEN, 0.0), TOL), Expect::Sheet(500.0, [5, 12, 8], false)),
|
||||
// The top 0.05 mm above the walls: farther than the tolerance, so it stays apart.
|
||||
"stitch_leaves_a_gap_wider_than_the_tolerance" => (Op::Stitch(Input::Loose(ALL, 0.05), TOL), Expect::Sheet(600.0, [6, 16, 12], false)),
|
||||
// Within a 0.1 mm tolerance, the same gap closes.
|
||||
// (the top stays on its own plane, so the solid reaches up to it)
|
||||
"stitch_closes_a_gap_within_the_tolerance" => (Op::Stitch(Input::Loose(ALL, 0.05), 0.1), Expect::Solid(1005.0, [6, 12, 8], true)),
|
||||
"stitch_a_cube_already_sewn" => (Op::Stitch(Input::Box, TOL), Expect::Solid(1000.0, [6, 12, 8], true)),
|
||||
"merge_two_boxes_into_one_block" => (Op::Merge(Input::TwoBoxes, &[], TOL), Expect::Solid(2000.0, [6, 12, 8], false)),
|
||||
"merge_a_split_cube_into_one" => (Op::Merge(Input::SplitCube, &[], TOL), Expect::Solid(1000.0, [6, 12, 8], false)),
|
||||
"merge_a_box_changes_nothing" => (Op::Merge(Input::Box, &[], TOL), Expect::Solid(1000.0, [6, 12, 8], false)),
|
||||
"merge_keeps_a_picked_edge" => (Op::Merge(Input::SplitCube, &[[5.0, 5.0, 10.0]], TOL), Expect::Solid(1000.0, [7, 15, 10], false)),
|
||||
"merge_picks_near_nothing" => {
|
||||
(Op::Merge(Input::SplitCube, &[[5.0, 5.0, 10.0], [50.0, 50.0, 50.0]], TOL), Expect::Fails(Error::NoMatch { point: 1 }))
|
||||
}
|
||||
_ => panic!("no case {name}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Measured {
|
||||
size: f64,
|
||||
counts: [usize; 3],
|
||||
closed: bool,
|
||||
bounds: ([f64; 3], [f64; 3]),
|
||||
kinds: BTreeMap<String, usize>,
|
||||
}
|
||||
|
||||
type Got = std::result::Result<Measured, Error>;
|
||||
|
||||
fn kinds(rec: &solidcraft_geom::brep::Brep) -> BTreeMap<String, usize> {
|
||||
let mut out = BTreeMap::new();
|
||||
for f in &rec.faces {
|
||||
let v = serde_json::to_value(&f.surface).unwrap();
|
||||
*out.entry(v["kind"].as_str().unwrap_or("?").to_owned()).or_default() += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs()
|
||||
}
|
||||
|
||||
fn judge(name: &str, who: &str, got: Got) -> Got {
|
||||
let (op, expect) = case(name);
|
||||
match (&expect, &got) {
|
||||
(Expect::Fails(e), Err(g)) => assert_eq!(g, e, "{name}: {who} failed otherwise"),
|
||||
(Expect::Fails(e), Ok(m)) => panic!("{name}: {who} gave {m:?}, expected {e:?}"),
|
||||
(_, Err(g)) => panic!("{name}: {who} failed: {g}"),
|
||||
(Expect::Solid(s, n, closed) | Expect::Sheet(s, n, closed), Ok(m)) => {
|
||||
assert!(rel(m.size, *s) < 1e-9, "{name}: {who} size {} vs analytic {s}", m.size);
|
||||
assert_eq!(m.counts, *n, "{name}: {who} counts");
|
||||
if matches!(op, Op::Stitch(..)) {
|
||||
assert_eq!(m.closed, *closed, "{name}: {who} closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
got
|
||||
}
|
||||
|
||||
fn is_sheet(name: &str) -> bool {
|
||||
matches!(case(name).1, Expect::Sheet(..))
|
||||
}
|
||||
|
||||
fn solidcore_case(name: &str) -> Got {
|
||||
let (op, _) = case(name);
|
||||
let r = match op {
|
||||
Op::Stitch(input, tol) => stitch(&input.core(), tol),
|
||||
Op::Merge(input, keep, tol) => merge_faces(&input.core(), keep, tol).map(|b| (b, false)),
|
||||
};
|
||||
let got = r.map(|(b, closed)| {
|
||||
let (rec, _) = native(&to_arrays(&b).unwrap()).unwrap_or_else(|e| panic!("{name}: record: {e:?}"));
|
||||
rec.validate().unwrap_or_else(|e| panic!("{name}: record: {e}"));
|
||||
let size = if is_sheet(name) { area(&b, 1e-8) } else { volume(&b, 1e-8) }.unwrap();
|
||||
Measured { size, counts: counts(&b).unwrap(), closed, bounds: bounds(&b).unwrap(), kinds: kinds(&rec) }
|
||||
});
|
||||
judge(name, "solidcore", got)
|
||||
}
|
||||
|
||||
macro_rules! cases {
|
||||
($($m:ident $(: $ign:meta)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
#[test]
|
||||
$(#[$ign])?
|
||||
fn solidcore_matches_the_closed_form() {
|
||||
let _ = super::solidcore_case(stringify!($m));
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
cases!(
|
||||
stitch_six_faces_into_a_cube,
|
||||
stitch_five_faces_into_an_open_box,
|
||||
stitch_leaves_a_gap_wider_than_the_tolerance,
|
||||
stitch_closes_a_gap_within_the_tolerance,
|
||||
stitch_a_cube_already_sewn,
|
||||
merge_two_boxes_into_one_block,
|
||||
merge_a_split_cube_into_one,
|
||||
merge_a_box_changes_nothing,
|
||||
merge_keeps_a_picked_edge,
|
||||
merge_picks_near_nothing,
|
||||
);
|
||||
|
||||
/// The capi's argument checks, before any kernel call.
|
||||
#[test]
|
||||
fn hostile_arguments_are_refused() {
|
||||
let b = make_box([0.0; 3], [10.0; 3]).unwrap();
|
||||
for t in [0.0, -1.0, f64::NAN, f64::INFINITY, 1e3 + 1.0] {
|
||||
assert_eq!(stitch(&b, t).err(), Some(Error::Argument), "stitch tolerance {t}");
|
||||
}
|
||||
for t in [0.0, -1.0, f64::NAN, f64::INFINITY, 1e6] {
|
||||
assert_eq!(merge_faces(&b, &[], t).err(), Some(Error::Argument), "merge tolerance {t}");
|
||||
}
|
||||
assert_eq!(merge_faces(&b, &[[f64::NAN, 0.0, 0.0]], TOL).err(), Some(Error::Argument));
|
||||
let many = vec![[0.0; 3]; crate::MAX_POINTS + 1];
|
||||
assert_eq!(merge_faces(&b, &many, TOL).err(), Some(Error::Argument));
|
||||
}
|
||||
|
||||
/// A 10 mm cube and, unsewn in the same body (`MergeBrep`), a sheet made on the kernel.
|
||||
fn cube_and_sheet(sheet: impl FnOnce(&mut Kernel) -> solidcore::Result<Brep>) -> Body {
|
||||
run("cube and sheet", |k| {
|
||||
let mut cube = create_box(k, Vec3::new(0.0, 0.0, 0.0), 10.0, 10.0, 10.0)?;
|
||||
let sheet = sheet(k)?;
|
||||
let mut state = ArenaNotifyState::new(&cube);
|
||||
let mut env = state.env(None);
|
||||
merge_brep(k, &mut env, &mut cube, &sheet, &solidcore::topology::brep::p2::copy_source_ref, None, None, true)?;
|
||||
cube.refresh_cached_topology();
|
||||
Ok(cube)
|
||||
})
|
||||
.map(Body::new)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The capi's `cube_and_cover` (capi/tests/sheets.cpp): the cube and a cover over its top, a
|
||||
/// height field z(x, y) over [0, 10]^2 of `degree` and uniform `spans`, its control points at
|
||||
/// their Greville abscissae (so its parameters are x and y), z = 10 but for the middle one,
|
||||
/// 10 + `rise`, trimmed to the square by lines (`SmApiTrimSurfaceWith3dCurves`), so its rim is the
|
||||
/// cube's top edges.
|
||||
fn cube_and_cover(degree: usize, spans: usize, rise: f64) -> Body {
|
||||
cube_and_sheet(|k| {
|
||||
let n = spans + degree;
|
||||
let step = 10.0 / spans as f64;
|
||||
let unique: Vec<f64> = (0..=spans).map(|k| step * k as f64).collect();
|
||||
let mult: Vec<usize> = (0..=spans).map(|k| if k == 0 || k == spans { degree + 1 } else { 1 }).collect();
|
||||
let knots: Vec<f64> = unique.iter().zip(&mult).flat_map(|(&u, &m)| std::iter::repeat_n(u, m)).collect();
|
||||
let greville: Vec<f64> = (0..n).map(|i| knots[i + 1..=i + degree].iter().sum::<f64>() / degree as f64).collect();
|
||||
let mut net = Vec::with_capacity(n * n);
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
let z = if 2 * i + 1 == n && 2 * j + 1 == n { 10.0 + rise } else { 10.0 };
|
||||
net.push(Vec3::new(greville[i], greville[j], z));
|
||||
}
|
||||
}
|
||||
let surface = BSplineSurface::create_canonical(&CanonicalSurfaceData {
|
||||
u_degree: degree,
|
||||
v_degree: degree,
|
||||
control_points: &net,
|
||||
form: BSplineSurfaceForm::Unspecified,
|
||||
u_multiplicities: &mult,
|
||||
v_multiplicities: &mult,
|
||||
u_knots: &unique,
|
||||
v_knots: &unique,
|
||||
knot_type: KnotType::Unspecified,
|
||||
weights: None,
|
||||
uv_domain: None,
|
||||
})?;
|
||||
let corner = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]].map(|[x, y]| Vec3::new(x, y, 10.0));
|
||||
let mut sides = Vec::with_capacity(4);
|
||||
for i in 0..4 {
|
||||
sides.push(Curve::from(Line::create_line_segment(3, corner[i], corner[(i + 1) % 4], None)?));
|
||||
}
|
||||
trim_surface_with_3d_curves(k, Surface::from(surface), &[4], sides)
|
||||
})
|
||||
}
|
||||
|
||||
/// A biquadratic Bezier cover rising 9 at its middle control point,
|
||||
/// z = 10 + 9 * 2s(1-s) * 2t(1-t) (s = x/10, t = y/10): the capi's cover.
|
||||
fn cube_and_biquadratic_cover() -> Body {
|
||||
cube_and_cover(2, 1, 9.0)
|
||||
}
|
||||
|
||||
/// A bicubic cover over 8 spans whose only raised control point is the middle one: the capi's
|
||||
/// cover lying on the cube's top for 2.5 mm in from every edge.
|
||||
fn cube_and_bicubic_cover() -> Body {
|
||||
cube_and_cover(3, 8, 9.0)
|
||||
}
|
||||
|
||||
/// The capi's flap: a 10 x 10 sheet on from the cube's top edge at x = 10, in the plane z = 10.
|
||||
fn cube_and_flap() -> Body {
|
||||
cube_and_sheet(|k| create_plane(k, Vec3::new(10.0, 0.0, 10.0), 10.0, 10.0))
|
||||
}
|
||||
|
||||
/// The space between the cover and the cube's top encloses 9 * (10/3)^2 = 100 mm^3: sewn, it is
|
||||
/// part of the solid and the top, now inside it, goes. A closed solid of the cube's five other
|
||||
/// faces and the cover, 1100 mm^3.
|
||||
#[test]
|
||||
fn a_cover_sews_onto_a_cube_into_a_closed_solid() {
|
||||
let (sewn, closed) = stitch(&cube_and_biquadratic_cover(), 1e-6).unwrap();
|
||||
assert!(closed);
|
||||
assert_eq!(counts(&sewn).unwrap()[0], 6);
|
||||
let v = volume(&sewn, 1e-8).unwrap();
|
||||
assert!((v - 1100.0).abs() < 1e-6, "volume {v}");
|
||||
}
|
||||
|
||||
/// The bicubic cover overlaps the top: no sector between the faces at the rim to sew it into, so
|
||||
/// it is refused, not sewn so that the cube's inside joins the outside.
|
||||
#[test]
|
||||
fn a_cover_lying_on_the_cube_is_refused() {
|
||||
assert_eq!(stitch(&cube_and_bicubic_cover(), 1e-6).err(), Some(Error::Unsupported));
|
||||
}
|
||||
|
||||
/// The flap closes nothing with the cube: it stays the sheet beside it the manifold stitch
|
||||
/// leaves, not glued into an edge three faces would share.
|
||||
#[test]
|
||||
fn a_flap_on_a_cube_edge_stays_beside_it() {
|
||||
let (sewn, closed) = stitch(&cube_and_flap(), 1e-6).unwrap();
|
||||
assert!(!closed);
|
||||
assert_eq!(counts(&sewn).unwrap()[0], 7);
|
||||
let (v, a) = (volume(&sewn, 1e-8).unwrap(), area(&sewn, 1e-8).unwrap());
|
||||
assert!((v - 1000.0).abs() < 1e-6 && (a - 700.0).abs() < 1e-6, "volume {v}, area {a}");
|
||||
}
|
||||
|
||||
/// The inputs are never changed.
|
||||
#[test]
|
||||
fn the_body_stays_as_it_was() {
|
||||
let loose = loose_faces(ALL, 0.0);
|
||||
let _ = stitch(&loose, TOL).unwrap();
|
||||
assert_eq!(counts(&loose).unwrap(), [6, 24, 24]);
|
||||
let block = two_boxes();
|
||||
let was = counts(&block).unwrap();
|
||||
let _ = merge_faces(&block, &[], TOL).unwrap();
|
||||
assert_eq!(counts(&block).unwrap(), was);
|
||||
}
|
||||
@@ -0,0 +1,660 @@
|
||||
//! Shells, face offsets, thickening, sheet offsets and drafts on solidcore: the C API's logic
|
||||
//! round SMLib's offset executive, ported from the fork's `capi/src/smlib_ops.cpp`
|
||||
//! (`smlib_shell`, `smlib_offset_faces`, `smlib_thicken`, `smlib_draft`, `leaves_cavity`,
|
||||
//! `FaceSet`, `outer_faceuse`, `move_faces`, `draft_faces`), `capi/src/smlib_sheets.cpp`
|
||||
//! (`smlib_offset_sheet`) and `capi/src/smlib_pick.h` (`pick_faces`).
|
||||
//!
|
||||
//! As there, every operation works on a copy of its body (`SmApiBrepCopy`) and picks faces by
|
||||
//! point; the shell and the offsets are `SmApiShellBrepFull` and `SmApiOffsetBrepFull`, with
|
||||
//! convex edges extended and the results merged. A sheet offset takes faces that close into a
|
||||
//! solid as the capi does since tangency-labs/smlib#17.
|
||||
//!
|
||||
//! A draft tilts planes and, since tangency-labs/smlib#27, turns a cylinder about the pull into
|
||||
//! a cone (solidcore's `Cone::lean_cylinder`, solidcore#1545), and since smlib#53 a cone about the
|
||||
//! pull into the cone its generators turned make. A draft's tilted faces go in through solidcore's
|
||||
//! `SmBrep::LocalOperation` ([`local_operation`]). The capi's check that it moved some face
|
||||
//! (`moved_faces`) marks the replacement surfaces; solidcore names a surface by its owning face, so
|
||||
//! the mark goes on the picked faces, which it stays with through the operation (solidcore#1875).
|
||||
|
||||
use solidcore::EntityRef;
|
||||
use solidcore::SolidError;
|
||||
use solidcore::geom::attribute::{Attribute, Behavior, ids};
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::object::Shape;
|
||||
use solidcore::geom::vector3d::{Point3, Vec3};
|
||||
use solidcore::intersect::point_class::PointClassType;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::math::Point2;
|
||||
use solidcore::nurbs::b_spline_curve::ControlPointForm;
|
||||
use solidcore::nurbs::b_spline_surface::p1::SurfParamType;
|
||||
use solidcore::nurbs::b_spline_surface_n_lib::p2::OrientType;
|
||||
use solidcore::nurbs::knots::Side;
|
||||
use solidcore::offset::offset_executive::p2::{OffsetOptions, ShellOptions, local_operation_simple, offset_brep_simple, shell_brep_simple};
|
||||
use solidcore::offset::offset_faces::{OffsetFacesExecutive, OffsetFacesRefusal};
|
||||
use solidcore::query::api_queries::{face_bounding_box, face_classify_uv, face_closest_point_in_shape, face_internal_point};
|
||||
use solidcore::query::brep::{BrepIdentity, BrepQuery, LiveBrepObjects};
|
||||
use solidcore::surface::cone::Cone;
|
||||
use solidcore::surface::surface::{Surface, SurfaceKind};
|
||||
use solidcore::topology::brep::{p1 as brep_p1, p2 as brep_p2};
|
||||
use solidcore::topology::face::p2 as face_p2;
|
||||
use solidcore::topology::{Brep, BrepId, FaceId, FaceuseId};
|
||||
|
||||
use super::blend::{Miss, on_copy, p3, pick, points_ok, status as blend_status, tolerance_ok};
|
||||
use super::{Body, answer};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// The capi's statuses as SolidCraft errors: an operation's own result.
|
||||
type Out<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// `SM_ERR_INVALID_INPUT`.
|
||||
const INVALID_INPUT: i32 = 1007;
|
||||
|
||||
/// `SM_ERR_OFFSET_WALLS_TOUCH` (fork a759622), which `status` (smlib_shim.h) reports as
|
||||
/// `SMLIB_ERR_WALLS_TOUCH`.
|
||||
const OFFSET_WALLS_TOUCH: i32 = 1042;
|
||||
|
||||
/// `status` (smlib_shim.h): an offset whose walls touch is [`Error::WallsTouch`]; a vertex moved
|
||||
/// past an edge's other end (`SM_ERR_OFFSET_EDGE_VANISHES`, fork 34318f4) is
|
||||
/// `SMLIB_ERR_UNSUPPORTED`; any other status as the blends pass it on.
|
||||
fn status(e: SolidError) -> Error {
|
||||
match e {
|
||||
SolidError::Upstream { code: OFFSET_WALLS_TOUCH, .. } => Error::WallsTouch,
|
||||
e if e.is_offset_edge_vanishes() => Error::Unsupported,
|
||||
e => blend_status(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// `std::fabs(x) > 1e6` and the rest of the capi's size checks: finite, not zero, at most 1e6.
|
||||
fn distance_ok(x: f64) -> bool {
|
||||
x.is_finite() && x != 0.0 && x.abs() <= 1e6
|
||||
}
|
||||
|
||||
/// The B-rep id every shape and face key in this module names the one body by.
|
||||
const ONE: BrepId = BrepId::from_index(0);
|
||||
|
||||
/// Faces with their loose bounding boxes, for nearest-face queries (`FaceSet`).
|
||||
struct FaceSet {
|
||||
faces: Vec<FaceId>,
|
||||
boxes: Vec<(Point3, Point3)>,
|
||||
/// Each face's shape (`SmApiFaceClosestPoint` makes one per query; it depends only on the
|
||||
/// face, so it is made once).
|
||||
shapes: Vec<Shape>,
|
||||
}
|
||||
|
||||
impl FaceSet {
|
||||
fn new() -> FaceSet {
|
||||
FaceSet { faces: Vec::new(), boxes: Vec::new(), shapes: Vec::new() }
|
||||
}
|
||||
|
||||
/// `add`: false when the face's box cannot be measured.
|
||||
fn add(&mut self, k: &mut Kernel, b: &Brep, f: FaceId) -> solidcore::Result<bool> {
|
||||
let Some(bx) = answer(face_bounding_box(k, b, f, false))? else { return Ok(false) };
|
||||
let Some(shape) = answer(Shape::new(k, &(ONE, b), &[], &[(ONE, f)], &[], &[]))? else { return Ok(false) };
|
||||
self.faces.push(f);
|
||||
self.boxes.push((bx.min(), bx.max()));
|
||||
self.shapes.push(shape);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// `nearest`: the distance from `p` to the nearest face, with the nearest point; infinite
|
||||
/// when none answers. A face whose box is farther than the nearest face so far is skipped.
|
||||
fn nearest(&self, k: &Kernel, live: &LiveBrepObjects<'_, '_>, p: Point3) -> solidcore::Result<(f64, Option<Point3>)> {
|
||||
let (mut best, mut at) = (f64::INFINITY, None);
|
||||
for ((&f, (lo, hi)), shape) in self.faces.iter().zip(&self.boxes).zip(&self.shapes) {
|
||||
let mut bx = 0.0;
|
||||
for (l, (h, x)) in [lo.x, lo.y, lo.z].into_iter().zip([hi.x, hi.y, hi.z].into_iter().zip([p.x, p.y, p.z])) {
|
||||
let out = (l - x).max((x - h).max(0.0));
|
||||
bx += out * out;
|
||||
}
|
||||
if f64::sqrt(bx) >= best {
|
||||
continue;
|
||||
}
|
||||
if let Some(c) = answer(face_closest_point_in_shape(k, shape, live, (ONE, f), p))?
|
||||
&& c.distance.is_finite()
|
||||
&& c.distance < best
|
||||
{
|
||||
best = c.distance;
|
||||
at = Some(c.point);
|
||||
}
|
||||
}
|
||||
Ok((best, at))
|
||||
}
|
||||
}
|
||||
|
||||
/// The live view of `b` the closest-point solves read, under `identity`: `query`'s effective
|
||||
/// identity ([`BrepQuery::effective_identity`]).
|
||||
fn live<'a, 'i>(b: &'a mut Brep, identity: &'a BrepIdentity<'i>) -> solidcore::Result<LiveBrepObjects<'a, 'i>> {
|
||||
LiveBrepObjects::new(b, identity, None)
|
||||
}
|
||||
|
||||
/// `pick_faces` (`face_distance`: `SmApiFaceClosestPoint`; a face whose query returns a status
|
||||
/// other than success is skipped, [`answer`]).
|
||||
pub(super) fn pick_faces(k: &mut Kernel, b: &mut Brep, points: &[[f64; 3]], tol: f64) -> solidcore::Result<std::result::Result<Vec<FaceId>, Miss>> {
|
||||
let all = brep_p1::faces(b, None)?;
|
||||
let model_tol = b.tolerance();
|
||||
let mut shapes = Vec::with_capacity(all.len());
|
||||
for &f in &all {
|
||||
shapes.push(answer(Shape::new(k, &(ONE, &*b), &[], &[(ONE, f)], &[], &[]))?);
|
||||
}
|
||||
let query = BrepQuery::of_brep(b);
|
||||
let identity = query.effective_identity();
|
||||
let objects = live(b, &identity)?;
|
||||
let k: &Kernel = k;
|
||||
let indices: Vec<usize> = (0..all.len()).collect();
|
||||
let picked = pick(&indices, points, tol, model_tol, |i, p| {
|
||||
let (Some(&f), Some(Some(shape))) = (all.get(i), shapes.get(i)) else { return Ok(None) };
|
||||
Ok(answer(face_closest_point_in_shape(k, shape, &objects, (ONE, f), p))?.map(|c| c.distance))
|
||||
})?;
|
||||
Ok(picked.map(|(p, _)| p.iter().filter_map(|&i| all.get(i).copied()).collect()))
|
||||
}
|
||||
|
||||
/// A face pick's failure as `pick_error` (ops.rs) reads it.
|
||||
pub(super) fn face_miss(m: Miss) -> Error {
|
||||
match m {
|
||||
Miss::None(point) => Error::NoMatch { point },
|
||||
Miss::Ambiguous(point) => Error::Ambiguous { point },
|
||||
}
|
||||
}
|
||||
|
||||
/// `outer_faceuse`: the faceuse of `f` on the outside of its body, the one bounding the infinite
|
||||
/// region (for a sheet, both do; then the upward one).
|
||||
fn outer_faceuse(b: &Brep, f: FaceId) -> solidcore::Result<Option<FaceuseId>> {
|
||||
let Some(fu) = face_p2::upward_faceuse(b, f)? else { return Ok(None) };
|
||||
let use_ = b.faceuse(fu)?;
|
||||
let outside = match use_.shell {
|
||||
Some(s) => b.shell(s)?.region.is_some() && b.shell(s)?.region == b.infinite_region(),
|
||||
None => false,
|
||||
};
|
||||
Ok(if outside { Some(fu) } else { use_.mate })
|
||||
}
|
||||
|
||||
/// `leaves_cavity`: whether an inward offset of `wall` (> 0) from every face of the solid `b`
|
||||
/// but `open` leaves a cavity: a point inside `b` farther than `wall`, and a margin, from every
|
||||
/// face that is offset. A point counts only as the centre of a ball inside `b`, reached from a
|
||||
/// face sample by stepping inward along the normal, then from ball to ball. The capi refuses a
|
||||
/// wall this finds no room for before SMLib sees it (C++ SMLib corrupts memory there,
|
||||
/// solidcore#380); the same refusal here keeps the C++ path's answer.
|
||||
fn leaves_cavity(k: &mut Kernel, b: &mut Brep, open: &[FaceId], wall: f64) -> solidcore::Result<bool> {
|
||||
let list = brep_p1::faces(b, None)?;
|
||||
let (mut all, mut walls) = (FaceSet::new(), FaceSet::new());
|
||||
for &f in &list {
|
||||
if !all.add(k, b, f)? {
|
||||
return Ok(false);
|
||||
}
|
||||
if !open.contains(&f) && !walls.add(k, b, f)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
if walls.faces.is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
let tol = b.tolerance();
|
||||
let margin = (10.0 * tol).max(1e-3 * wall);
|
||||
let need = wall + margin;
|
||||
// Face samples: a grid over each offset face's domain, the points inside its trims, each
|
||||
// with the inward unit normal there.
|
||||
const GRID: usize = 5;
|
||||
const MAX_SAMPLES: usize = 2048;
|
||||
let mut samples: Vec<(Point3, Vec3)> = Vec::new();
|
||||
for &f in &walls.faces {
|
||||
let Some(fu) = outer_faceuse(b, f)? else { continue };
|
||||
let inward = if b.faceuse(fu)?.orientation == OrientType::Same { -1.0 } else { 1.0 };
|
||||
let dom = b.face(f)?.uv_domain;
|
||||
for i in 0..GRID {
|
||||
for j in 0..GRID {
|
||||
if samples.len() >= MAX_SAMPLES {
|
||||
break;
|
||||
}
|
||||
let uv: Point2 = dom.evaluate((i as f64 + 0.5) / GRID as f64, (j as f64 + 0.5) / GRID as f64);
|
||||
if answer(face_classify_uv(k, b, f, uv))? != Some(PointClassType::Face) {
|
||||
continue;
|
||||
}
|
||||
let Some(surf) = b.face(f)?.surface.as_ref() else { break };
|
||||
let (Some(at), Some(n)) = (answer(surf.evaluate_point(k, uv))?, answer(surf.evaluate_normal(k, uv, Side::Left, Side::Left))?) else {
|
||||
continue;
|
||||
};
|
||||
let len = n.length();
|
||||
if !(len > 0.0) || !len.is_finite() {
|
||||
continue;
|
||||
}
|
||||
samples.push((at, n * (inward / len)));
|
||||
}
|
||||
}
|
||||
}
|
||||
let query = BrepQuery::of_brep(b);
|
||||
let identity = query.effective_identity();
|
||||
let objects = live(b, &identity)?;
|
||||
let k: &Kernel = k;
|
||||
// A step of `h` inward from a sample ends at a ball's centre when no face is nearer to it
|
||||
// than `h` (the step crossed none, and the ball touches only the sample's face).
|
||||
let ball = |s: Point3, n: Vec3, h: f64| -> solidcore::Result<(bool, Point3, f64)> {
|
||||
let c = s + n * h;
|
||||
let (r, _) = all.nearest(k, &objects, c)?;
|
||||
Ok((r.is_finite() && r >= h * (1.0 - 1e-9), c, r))
|
||||
};
|
||||
// First the direct step of `need` from each sample.
|
||||
for &(s, n) in &samples {
|
||||
let (inside, c, _) = ball(s, n, need)?;
|
||||
if inside && walls.nearest(k, &objects, c)?.0 >= need {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
// Then climbing, from a small ball at each sample: step away from the nearest offset face by
|
||||
// most of the ball's radius (staying inside the ball, so inside the body) while the distance
|
||||
// from the offset faces grows.
|
||||
const STEPS: usize = 48;
|
||||
for &(s, n) in &samples {
|
||||
let (inside, mut c, mut r) = ball(s, n, margin.max(1e3 * tol))?;
|
||||
if !inside {
|
||||
continue;
|
||||
}
|
||||
for _ in 0..STEPS {
|
||||
let (d, q) = walls.nearest(k, &objects, c)?;
|
||||
if d >= need {
|
||||
return Ok(true);
|
||||
}
|
||||
let Some(q) = q.filter(|_| d > 0.0 && d.is_finite()) else { break };
|
||||
let next = c + (c - q) * (0.9 * r / d);
|
||||
let (rn, _) = all.nearest(k, &objects, next)?;
|
||||
if !(rn > 0.0) || !rn.is_finite() || walls.nearest(k, &objects, next)?.0 <= d * (1.0 + 1e-9) {
|
||||
break;
|
||||
}
|
||||
c = next;
|
||||
r = rn;
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// `SmApiShellBrepFull(b, distance, TRUE, TRUE, TRUE, open, r)`: convex edges extended, results
|
||||
/// merged, side walls made at a sheet's edges, and (as the fork does, 5448b9a) a thickened
|
||||
/// sheet's coplanar side walls one face.
|
||||
fn shell_brep_full(k: &mut Kernel, b: &Brep, distance: f64, open: &[FaceId]) -> solidcore::Result<Brep> {
|
||||
let options = ShellOptions {
|
||||
offset: OffsetOptions { distance, extend_convex_edges: true, merge_results: true },
|
||||
create_offset_solid: true,
|
||||
merge_sheet_side_walls: true,
|
||||
};
|
||||
shell_brep_simple(k, b, options, open)
|
||||
}
|
||||
|
||||
/// `SmApiOffsetBrepFull(b, distance, TRUE, TRUE, r)`.
|
||||
fn offset_brep_full(k: &mut Kernel, b: &mut Brep, distance: f64) -> solidcore::Result<Brep> {
|
||||
offset_brep_simple(k, b, OffsetOptions { distance, extend_convex_edges: true, merge_results: true })
|
||||
}
|
||||
|
||||
/// `finish`: the result must have faces, and a solid input must give a closed manifold solid.
|
||||
fn finish(b: Brep, solid_in: bool) -> solidcore::Result<Out<Brep>> {
|
||||
if b.face_list().is_empty() {
|
||||
return Ok(Err(Error::Empty));
|
||||
}
|
||||
if solid_in && !brep_p2::is_manifold_solid(&b)? {
|
||||
return Ok(Err(Error::NotSolid));
|
||||
}
|
||||
Ok(Ok(b))
|
||||
}
|
||||
|
||||
/// An SMLib call's own status passed on (`status(...)` in the capi), else the value.
|
||||
fn called<T>(r: solidcore::Result<T>) -> solidcore::Result<Out<T>> {
|
||||
match r {
|
||||
Ok(v) => Ok(Ok(v)),
|
||||
Err(e) => Ok(Err(status(e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// `smlib_shell`: the faces nearest `points` (each within `tolerance`) opened, the rest offset
|
||||
/// by `thickness` (negative: inward) into a wall; none gives a closed hollow body. An inward
|
||||
/// wall of a solid that leaves no cavity is [`Error::NoRoom`], refused before the shell.
|
||||
pub(crate) fn shell(body: &Body, points: &[[f64; 3]], tolerance: f64, thickness: f64) -> Result<Body> {
|
||||
if !points_ok(points, true) || !tolerance_ok(tolerance) || !distance_ok(thickness) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
on_copy("smlib_shell", body, |k, mut w| {
|
||||
let picked = match pick_faces(k, &mut w, points, tolerance)? {
|
||||
Ok(p) => p,
|
||||
Err(m) => return Ok(Err(face_miss(m))),
|
||||
};
|
||||
let solid_in = brep_p2::is_manifold_solid(&w)?;
|
||||
if thickness < 0.0 && solid_in && !leaves_cavity(k, &mut w, &picked, -thickness)? {
|
||||
return Ok(Err(Error::NoRoom));
|
||||
}
|
||||
match called(shell_brep_full(k, &w, thickness, &picked))? {
|
||||
Ok(r) => finish(r, solid_in),
|
||||
Err(e) => Ok(Err(e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `smlib_offset_faces`: its checks, the pick, the solid check, (moving faces in) the cavity
|
||||
/// check, then `move_faces`.
|
||||
pub(crate) fn offset_faces(body: &Body, points: &[[f64; 3]], tolerance: f64, distance: f64) -> Result<Body> {
|
||||
if !points_ok(points, false) || !tolerance_ok(tolerance) || !distance_ok(distance) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
on_copy("smlib_offset_faces", body, |k, mut w| {
|
||||
let picked = match pick_faces(k, &mut w, points, tolerance)? {
|
||||
Ok(p) => p,
|
||||
Err(m) => return Ok(Err(face_miss(m))),
|
||||
};
|
||||
if !brep_p2::is_manifold_solid(&w)? {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
// Faces moved in so far that no material is left behind them: the body must reach
|
||||
// deeper than the move behind the moved faces.
|
||||
if distance < 0.0 {
|
||||
let others: Vec<FaceId> = brep_p1::faces(&w, None)?.into_iter().filter(|f| !picked.contains(f)).collect();
|
||||
if !leaves_cavity(k, &mut w, &others, -distance)? {
|
||||
return Ok(Err(Error::NoRoom));
|
||||
}
|
||||
}
|
||||
match move_faces(k, &mut w, &picked, distance) {
|
||||
Ok(()) => finish(w, true),
|
||||
Err(e) => Ok(Err(e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `move_faces`: moves the picked faces of `b` (in place) `distance` along their outward normals:
|
||||
/// each gets its surface's offset, and the local operation extends or trims the faces around them
|
||||
/// to meet it; the tangent blends on them follow (`SmOffsetFacesExecutive`: taken off, the faces
|
||||
/// moved, put back at their own sizes). A blend that no longer fits is [`Error::NoRoom`], one that
|
||||
/// cannot be put back as it was [`Error::Unsupported`], one that would need a continuation that
|
||||
/// cannot be made exactly [`Error::NotExact`], a move that moved nothing [`Error::NoEffect`].
|
||||
fn move_faces(k: &mut Kernel, b: &mut Brep, picked: &[FaceId], distance: f64) -> Out<()> {
|
||||
let mut exec = OffsetFacesExecutive::new();
|
||||
exec.offset_faces(k, b, picked, distance).map_err(|e| match exec.refusal() {
|
||||
OffsetFacesRefusal::NoRoom => Error::NoRoom,
|
||||
OffsetFacesRefusal::Unsupported => Error::Unsupported,
|
||||
OffsetFacesRefusal::NotExact => Error::NotExact,
|
||||
OffsetFacesRefusal::NoEffect => Error::NoEffect,
|
||||
// Not in the fork (solidcore#2020, #2027): the blends cannot be put back, the fillet's
|
||||
// status kept. SolidCraft has no such error: a move solidcore does not make.
|
||||
OffsetFacesRefusal::NotPutBack => Error::Unsupported,
|
||||
OffsetFacesRefusal::None => status(e),
|
||||
})
|
||||
}
|
||||
|
||||
/// `smlib_thicken`: a sheet thickened by `thickness` along its normals, or (symmetric) moved back
|
||||
/// by half first; a closed solid is [`Error::Argument`].
|
||||
pub(crate) fn thicken(body: &Body, thickness: f64, symmetric: bool) -> Result<Body> {
|
||||
if !distance_ok(thickness) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
on_copy("smlib_thicken", body, |k, mut w| {
|
||||
if brep_p2::is_manifold_solid(&w)? {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
if symmetric {
|
||||
w = match called(offset_brep_full(k, &mut w, -0.5 * thickness))? {
|
||||
Ok(moved) => moved,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
}
|
||||
match called(shell_brep_full(k, &w, thickness, &[]))? {
|
||||
Ok(r) => finish(r, true),
|
||||
Err(e) => Ok(Err(e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `smlib_offset_sheet`: every face of a sheet moved onto its surface's offset by `distance`
|
||||
/// along its normal; faces that close into a solid (a sphere's one face, a block's six) give the
|
||||
/// offset solid (tangency-labs/smlib#17, which dropped the capi's refusal of a closed solid).
|
||||
pub(crate) fn offset_sheet(body: &Body, distance: f64) -> Result<Body> {
|
||||
if !distance_ok(distance) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
on_copy("smlib_offset_sheet", body, |k, mut w| {
|
||||
// `take`: a result without faces (a sphere moved in to its centre) is SMLIB_ERR_EMPTY.
|
||||
match called(offset_brep_full(k, &mut w, distance))? {
|
||||
Ok(r) => finish(r, false),
|
||||
Err(e) => Ok(Err(e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A unit vector from `v`, as `smlib_draft`'s `unit`; `None` for a vector too short or not
|
||||
/// finite.
|
||||
fn unit(v: [f64; 3]) -> Option<Vec3> {
|
||||
if !v.iter().all(|x| x.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let u = p3(v);
|
||||
let len = u.length();
|
||||
(len > 1e-12 && len.is_finite()).then(|| u * (1.0 / len))
|
||||
}
|
||||
|
||||
/// `outward_normal`: the outward normal of face `f` (its outer faceuse `fu`) at `at`, a point on
|
||||
/// it: its surface's normal there, turned with the outer faceuse. `None` where SMLib cannot place
|
||||
/// the point on the surface (the capi's `STEPInversionAnswered` also takes a point outside the
|
||||
/// domain or off the surface by more than its tolerance; solidcore's inversion answers those with
|
||||
/// a status, so they are `None` here: the face's own internal point is neither).
|
||||
fn outward_normal(k: &Kernel, b: &Brep, f: FaceId, fu: FaceuseId, at: Point3) -> solidcore::Result<Option<Vec3>> {
|
||||
let Some(surf) = b.face(f)?.surface.as_ref() else { return Ok(None) };
|
||||
let Some((step, _)) = answer(surf.step_inversion(k, &surf.step_uv_domain(), at, 1e6, None))? else { return Ok(None) };
|
||||
let Some(nurb) = answer(surf.convert_uv_from_step_to_nurbs(k, step))? else { return Ok(None) };
|
||||
let Some(mut n) = answer(surf.evaluate_normal(k, nurb, Side::Left, Side::Left))? else { return Ok(None) };
|
||||
if b.faceuse(fu)?.orientation != OrientType::Same {
|
||||
n = -n;
|
||||
}
|
||||
Ok(Some(n))
|
||||
}
|
||||
|
||||
/// `drafted_cone` (tangency-labs/smlib#27, #53): the cone a face on the cone or cylinder `cyl` about
|
||||
/// the pull (its outward normal `n` at `inside`) becomes drafted by `angle` about its circle on the
|
||||
/// neutral plane (through `origin`, normal `normal`), so that it leans in going along `pull`: its
|
||||
/// generators turned by `angle` in their planes through the axis, about that circle. With R0 the
|
||||
/// radius on the neutral plane and t the slope of the radius going along the pull (0 for a
|
||||
/// cylinder), the radius h along the pull from that circle is R0 + h tan(atan(t) - s angle), s = 1
|
||||
/// where the face looks away from the axis (a pin), -1 where it looks towards it (a hole). It is
|
||||
/// the surface's own NURB with each control point moved radially by the new radius over the old at
|
||||
/// its height, so it keeps the surface's parameters and the face's UV trims stay valid: for a
|
||||
/// cylinder solidcore's `Cone::lean_cylinder` (solidcore#1545), for a cone the same edit on
|
||||
/// [`Cone::base_mut`]. [`Error::Unsupported`] when the pull does not run along the axis, the axis
|
||||
/// lies in the neutral plane, or the cone would close within the face's span.
|
||||
#[allow(clippy::too_many_arguments)] // the capi's drafted_cone
|
||||
fn drafted_cone(
|
||||
k: &Kernel,
|
||||
cyl: &Cone,
|
||||
n: Vec3,
|
||||
inside: Point3,
|
||||
origin: Point3,
|
||||
normal: Vec3,
|
||||
pull: Vec3,
|
||||
angle: f64,
|
||||
) -> solidcore::Result<Out<Cone>> {
|
||||
let frame = cyl.canonical().origin;
|
||||
let z = frame.x_axis().cross(frame.y_axis());
|
||||
let len = z.length();
|
||||
if !(len > 0.0 && len.is_finite()) {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
let z = z * (1.0 / len);
|
||||
let sense = z.dot(pull);
|
||||
if (sense.abs() - 1.0).abs() > 1e-9 {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
let axis = frame.origin();
|
||||
let toward = z.dot(normal);
|
||||
if toward.abs() < 1e-12 {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
// The hinge circle's centre: the axis on the neutral plane, at the cone's height v0.
|
||||
let v0 = (origin - axis).dot(normal) / toward;
|
||||
let centre = axis + z * v0;
|
||||
let radial = (inside - centre) - pull * (inside - centre).dot(pull);
|
||||
let s = if radial.dot(n) >= 0.0 { 1.0 } else { -1.0 };
|
||||
// The cone's radius is linear in its height along z; along the pull it changes by `slope` per
|
||||
// unit, and drafted by `turned`.
|
||||
let r0 = cyl.radius(v0);
|
||||
let slope = (cyl.radius(1.0) - cyl.radius(0.0)) * sense;
|
||||
let turned = (slope.atan() - s * angle).tan();
|
||||
if !(r0 > 0.0) || !turned.is_finite() {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
if cyl.is_cylinder() {
|
||||
return Ok(answer(cyl.lean_cylinder(k, turned * sense, v0))?.ok_or(Error::Unsupported));
|
||||
}
|
||||
let mut cone = cyl.clone();
|
||||
let net = cone.base_mut();
|
||||
let (nu, nv) = (net.number_control_points(SurfParamType::U), net.number_control_points(SurfParamType::V));
|
||||
for i in 0..nu {
|
||||
for j in 0..nv {
|
||||
let Some((p, w)) = answer(net.control_point(ControlPointForm::EuclideanRational, i, j))? else {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
};
|
||||
let h = (p - centre).dot(pull);
|
||||
let (was, now) = (cyl.radius(v0 + h * sense), r0 + h * turned);
|
||||
if !(was > 0.0 && now > 0.0) {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
let foot = centre + pull * h;
|
||||
if answer(net.set_control_point(ControlPointForm::EuclideanRational, i, j, foot + (p - foot) * (now / was), w))?.is_none() {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
}
|
||||
}
|
||||
// `CopyAndAddAnalytics`: the cone's analytic data from its edited NURB.
|
||||
if answer(cone.rebuild_step_from_nurb_parameters(k))?.is_none() {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
Ok(Ok(cone))
|
||||
}
|
||||
|
||||
/// The capi's `Before` mark on what `SmBrep::LocalOperation` is to move: above the history's tag.
|
||||
const MOVED_MARK: u64 = ids::USER_1 + 8;
|
||||
|
||||
/// `Before::mark` on each replacement surface. A surface has no identity apart from its owning
|
||||
/// face here, and `faces[i]`'s offset face takes `surfaces[i]`, so the mark goes on the picked face
|
||||
/// and travels as the surface's does (Copy behaviour; solidcore#1875). The one difference, recorded
|
||||
/// in solidcore's `topology.brep_local_operation--1875-face-mark-for-surface-mark` finding: a merge
|
||||
/// keeping an unpicked face over a picked one hands that survivor the mark, where SMLib's survivor
|
||||
/// keeps its own unmarked surface.
|
||||
pub(super) fn mark_moved(w: &mut Brep, picked: &[FaceId]) -> solidcore::Result<()> {
|
||||
for &f in picked {
|
||||
w.add_attribute(EntityRef::Face(f), Attribute::long(MOVED_MARK, 0, Behavior::Copy))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `moved_faces`'s reading (`read_marks`): whether any face of `w` carries the mark, every mark
|
||||
/// removed.
|
||||
pub(super) fn moved_faces(w: &mut Brep) -> solidcore::Result<bool> {
|
||||
let mut moved = false;
|
||||
for f in w.face_list().to_vec() {
|
||||
let e = EntityRef::Face(f);
|
||||
let marks: Vec<_> = w.entity_attributes(e).into_iter().filter(|(_, a)| a.attribute_id() == MOVED_MARK).map(|(h, _)| h).collect();
|
||||
moved = moved || !marks.is_empty();
|
||||
for h in marks {
|
||||
w.remove_attribute(e, h)?;
|
||||
}
|
||||
}
|
||||
Ok(moved)
|
||||
}
|
||||
|
||||
/// `smlib_draft`: its checks, the solid check, the pick, then `draft_faces`: each picked face (a
|
||||
/// plane parallel to the pull) gets a copy of its plane turned by `angle` about its line on the
|
||||
/// neutral plane, and a cylinder about the pull the cone `drafted_cone` gives
|
||||
/// (tangency-labs/smlib#27); `SmBrep::LocalOperation` extends or trims the faces around them to
|
||||
/// meet the tilted surfaces.
|
||||
pub(crate) fn draft(
|
||||
body: &Body,
|
||||
points: &[[f64; 3]],
|
||||
tolerance: f64,
|
||||
neutral_origin: [f64; 3],
|
||||
neutral_normal: [f64; 3],
|
||||
pull: [f64; 3],
|
||||
angle: f64,
|
||||
) -> Result<Body> {
|
||||
let (Some(normal), Some(along)) = (unit(neutral_normal), unit(pull)) else { return Err(Error::Argument) };
|
||||
if !points_ok(points, false)
|
||||
|| !tolerance_ok(tolerance)
|
||||
|| !neutral_origin.iter().all(|x| x.is_finite() && x.abs() <= 1e9)
|
||||
|| !angle.is_finite()
|
||||
|| angle == 0.0
|
||||
|| angle.abs() >= 1.5697963267948966
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
on_copy("smlib_draft", body, |k, mut w| {
|
||||
if !brep_p2::is_manifold_solid(&w)? {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
let picked = match pick_faces(k, &mut w, points, tolerance)? {
|
||||
Ok(p) => p,
|
||||
Err(m) => return Ok(Err(face_miss(m))),
|
||||
};
|
||||
// `draft_faces`.
|
||||
let mut surfaces = Vec::with_capacity(picked.len());
|
||||
for &f in &picked {
|
||||
let fu = outer_faceuse(&w, f)?;
|
||||
let face = w.face(f)?;
|
||||
let (Some(surf), Some(fu)) = (face.surface.as_ref(), fu) else {
|
||||
return Ok(Err(Error::Status(INVALID_INPUT)));
|
||||
};
|
||||
// A cylinder about the pull drafted becomes a cone (`drafted_cone`).
|
||||
if let SurfaceKind::Cone(c) = surf.kind_with_nurb(k)? {
|
||||
let cyl = (**c).clone();
|
||||
let Some(inside) = answer(face_internal_point(k, &mut w, f))? else {
|
||||
return Ok(Err(Error::Status(INVALID_INPUT)));
|
||||
};
|
||||
let Some(n) = outward_normal(k, &w, f, fu, inside)? else {
|
||||
return Ok(Err(Error::Status(INVALID_INPUT)));
|
||||
};
|
||||
match drafted_cone(k, &cyl, n, inside, p3(neutral_origin), normal, along, angle)? {
|
||||
Ok(cone) => surfaces.push(Surface::from(cone)),
|
||||
Err(e) => return Ok(Err(e)),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let SurfaceKind::Plane(plane) = surf.kind_with_nurb(k)? else {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
};
|
||||
// The face's outward normal: the plane's, turned with the outer faceuse.
|
||||
let mut n = plane.position().z_axis();
|
||||
if w.faceuse(fu)?.orientation != OrientType::Same {
|
||||
n = -n;
|
||||
}
|
||||
if n.dot(along).abs() > 1e-6 {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
let Some(inside) = answer(face_internal_point(k, &mut w, f))? else {
|
||||
return Ok(Err(Error::Status(INVALID_INPUT)));
|
||||
};
|
||||
let toward = along.dot(normal);
|
||||
if toward.abs() < 1e-12 {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
let hinge = inside + along * ((p3(neutral_origin) - inside).dot(normal) / toward);
|
||||
// Turning n about n x pull by `angle` gives n cos(angle) + pull sin(angle).
|
||||
let axis = n.cross(along);
|
||||
let len = axis.length();
|
||||
if !(len > 0.5) {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
let axis = axis * (1.0 / len);
|
||||
let Some(surf) = w.face(f)?.surface.as_ref() else {
|
||||
return Ok(Err(Error::Status(INVALID_INPUT)));
|
||||
};
|
||||
let Some(mut tilted) = answer(surf.copy(k))? else {
|
||||
return Ok(Err(Error::Status(INVALID_INPUT)));
|
||||
};
|
||||
let mut turn = Axis2Placement::IDENTITY;
|
||||
turn.rotate_about_axis_at_point(angle, hinge, axis);
|
||||
if answer(tilted.transform(k, &turn, None))?.is_none() {
|
||||
return Ok(Err(Error::Status(INVALID_INPUT)));
|
||||
}
|
||||
surfaces.push(tilted);
|
||||
}
|
||||
// `moved_faces(b, status(b->LocalOperation(faces, surfaces)))`, its marks on the picked
|
||||
// faces (`mark_moved`).
|
||||
mark_moved(&mut w, &picked)?;
|
||||
if let Err(e) = called(local_operation_simple(k, &mut w, &picked, surfaces))? {
|
||||
return Ok(Err(e));
|
||||
}
|
||||
if !moved_faces(&mut w)? {
|
||||
return Ok(Err(Error::NoEffect));
|
||||
}
|
||||
finish(w, true)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,642 @@
|
||||
//! Shells, face offsets, thickening, sheet offsets and drafts on solidcore, against closed forms:
|
||||
//! every result exports to a record that validates.
|
||||
//!
|
||||
//! The bodies are solidcore's own boxes, cylinders and linear sweeps (and booleans of them).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::f64::consts::PI;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::curve::ellipse::Ellipse;
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::extent1d::Extent1d;
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::primitive::api_curves::create_line_segment;
|
||||
use solidcore::sweep::api_brep::create_linear_sweep;
|
||||
|
||||
use super::offset::{draft, mark_moved, moved_faces, offset_faces, offset_sheet, shell, thicken};
|
||||
use super::{Body, area, bounds, counts, make_box, make_cylinder, make_sphere, run, to_arrays, volume};
|
||||
use crate::export::native;
|
||||
use crate::{BooleanOp, Error};
|
||||
|
||||
/// A case longer than this is a hang.
|
||||
const TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
/// How near a pick point must be.
|
||||
const TOL: f64 = 0.01;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Shape {
|
||||
Box([f64; 3], [f64; 3]),
|
||||
Cylinder([f64; 3], f64, f64),
|
||||
Sphere([f64; 3], f64),
|
||||
/// A frustum about +Z: base centre, base radius, top radius, height.
|
||||
Frustum([f64; 3], f64, f64, f64),
|
||||
/// A sheet: the line from the first point to the second swept along `dir` by `dist`.
|
||||
LineSheet([f64; 3], [f64; 3], [f64; 3], f64),
|
||||
/// A sheet: the arc about +Z through `center` of `radius` from 0 to `degrees`, swept along
|
||||
/// +Z by `height`.
|
||||
ArcSheet([f64; 3], f64, f64, f64),
|
||||
/// Rig case shell/4711: a 60 x 40 x 10 block less an r3 through hole at (22.5, 7) and a
|
||||
/// 10 x 10 notch on the front edge from x = 25 to 35.
|
||||
Notched,
|
||||
}
|
||||
|
||||
fn v3(p: [f64; 3]) -> Vec3 {
|
||||
Vec3::new(p[0], p[1], p[2])
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
fn core(self) -> Body {
|
||||
match self {
|
||||
Shape::Box(o, s) => make_box(o, s).unwrap(),
|
||||
Shape::Cylinder(o, r, h) => make_cylinder(o, r, h).unwrap(),
|
||||
Shape::Sphere(o, r) => make_sphere(o, r).unwrap(),
|
||||
Shape::Frustum(o, a, b, h) => super::make_cone(o, a, b, h).unwrap(),
|
||||
Shape::LineSheet(a, b, dir, dist) => run("sheet", |k| {
|
||||
let c = Curve::from(create_line_segment(v3(a), v3(b))?);
|
||||
create_linear_sweep(k, &[Some(&c)], v3(dir), dist, false)
|
||||
})
|
||||
.map(Body::new)
|
||||
.unwrap(),
|
||||
Shape::ArcSheet(o, r, degrees, h) => run("sheet", |k| {
|
||||
let frame = Axis2Placement::new(v3(o), Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0));
|
||||
let c = Curve::from(Ellipse::create_canonical_circle(k, &frame, r, Some(&Extent1d::from_min_max(0.0, degrees)), None)?);
|
||||
create_linear_sweep(k, &[Some(&c)], Vec3::new(0.0, 0.0, 1.0), h, false)
|
||||
})
|
||||
.map(Body::new)
|
||||
.unwrap(),
|
||||
Shape::Notched => {
|
||||
let block = make_box([0.0; 3], [60.0, 40.0, 10.0]).unwrap();
|
||||
let pin = make_cylinder([22.5, 7.0, -5.0], 3.0, 20.0).unwrap();
|
||||
let notch = make_box([25.0, -5.0, -5.0], [10.0, 10.0, 20.0]).unwrap();
|
||||
let a = super::boolean::boolean(&block, &pin, BooleanOp::Subtract).unwrap().unwrap();
|
||||
super::boolean::boolean(&a, ¬ch, BooleanOp::Subtract).unwrap().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Op {
|
||||
/// `Brep::shell`: open faces, thickness.
|
||||
Shell(Shape, Vec<[f64; 3]>, f64),
|
||||
/// `Brep::offset_faces`: faces, distance.
|
||||
OffsetFaces(Shape, Vec<[f64; 3]>, f64),
|
||||
/// `Brep::thicken`: thickness, symmetric.
|
||||
Thicken(Shape, f64, bool),
|
||||
/// `Brep::offset_sheet`: distance.
|
||||
OffsetSheet(Shape, f64),
|
||||
/// `Brep::draft`: faces, neutral origin, neutral normal, pull, angle.
|
||||
Draft(Shape, Vec<[f64; 3]>, [f64; 3], [f64; 3], [f64; 3], f64),
|
||||
}
|
||||
|
||||
/// What a case must give.
|
||||
#[derive(Clone, Debug)]
|
||||
enum Expect {
|
||||
/// A solid of this volume (relative 1e-9).
|
||||
Volume(f64),
|
||||
/// A solid of this volume whose bounds along z are these.
|
||||
Slab(f64, f64, f64),
|
||||
/// A solid of this volume (relative 1e-9) whose bounds are these.
|
||||
Solid(f64, [f64; 3], [f64; 3]),
|
||||
/// A sheet of this area (relative 1e-9) whose bounds are these.
|
||||
Sheet(f64, [f64; 3], [f64; 3]),
|
||||
/// This error.
|
||||
Fails(Error),
|
||||
}
|
||||
|
||||
struct Case {
|
||||
op: Op,
|
||||
expect: Expect,
|
||||
}
|
||||
|
||||
/// A 30 x 20 x 10 block from the origin.
|
||||
const BLOCK: Shape = Shape::Box([0.0; 3], [30.0, 20.0, 10.0]);
|
||||
const ABC: f64 = 6000.0;
|
||||
/// A point on its top face.
|
||||
const TOP: [f64; 3] = [15.0, 10.0, 10.0];
|
||||
/// A point on its front face (y = 0).
|
||||
const FRONT: [f64; 3] = [15.0, 0.0, 5.0];
|
||||
/// The z axis: a neutral plane's normal and a pull.
|
||||
const Z: [f64; 3] = [0.0, 0.0, 1.0];
|
||||
/// A 30 x 20 sheet in z = 0: the x axis from 0 to 30 swept 20 along y.
|
||||
const PLATE: Shape = Shape::LineSheet([0.0; 3], [30.0, 0.0, 0.0], [0.0, 1.0, 0.0], 20.0);
|
||||
/// A sphere of radius 10 about the origin.
|
||||
const BALL: Shape = Shape::Sphere([0.0; 3], 10.0);
|
||||
/// A quarter of a cylinder of radius 5 about the z axis, 10 high.
|
||||
const QUARTER: Shape = Shape::ArcSheet([0.0; 3], 5.0, 90.0, 10.0);
|
||||
|
||||
fn case(name: &str) -> Case {
|
||||
let c = |op, expect| Case { op, expect };
|
||||
match name {
|
||||
// abc - (a - 2t)(b - 2t)(c - t), t = 2.
|
||||
"shell_box_open_top" => c(Op::Shell(BLOCK, vec![TOP], -2.0), Expect::Volume(ABC - 26.0 * 16.0 * 8.0)),
|
||||
// A void: abc - (a - 2t)(b - 2t)(c - 2t).
|
||||
"shell_box_closed" => c(Op::Shell(BLOCK, vec![], -2.0), Expect::Volume(ABC - 26.0 * 16.0 * 6.0)),
|
||||
// Closed and outward, the convex edges extended: (a + 2t)(b + 2t)(c + 2t) - abc, t = 1.
|
||||
"shell_box_closed_outward" => c(Op::Shell(BLOCK, vec![], 1.0), Expect::Volume(32.0 * 22.0 * 12.0 - ABC)),
|
||||
// Outward, the convex edges extended: (a + 2t)(b + 2t)(c + t) - abc, t = 1.
|
||||
"shell_box_outward" => c(Op::Shell(BLOCK, vec![TOP], 1.0), Expect::Volume(32.0 * 22.0 * 11.0 - ABC)),
|
||||
// r = 5, h = 10, open at the top, t = 1: π r² h - π (r - t)² (h - t).
|
||||
"shell_cylinder_open_top" => {
|
||||
c(Op::Shell(Shape::Cylinder([0.0; 3], 5.0, 10.0), vec![[0.0, 2.0, 10.0]], -1.0), Expect::Volume(PI * 25.0 * 10.0 - PI * 16.0 * 9.0))
|
||||
}
|
||||
// A wall of 15 inside a block 20 wide leaves no cavity.
|
||||
"shell_too_thick" => c(Op::Shell(BLOCK, vec![TOP], -15.0), Expect::Fails(Error::NoRoom)),
|
||||
// The hole's offset (r 3 + 2) is tangent to the front's (y = 2): the walls meet along a
|
||||
// line, which bounds no solid.
|
||||
"shell_walls_touch" => c(Op::Shell(Shape::Notched, vec![[50.0, 30.0, 0.0]], -2.0), Expect::Fails(Error::WallsTouch)),
|
||||
"shell_pick_near_nothing" => c(Op::Shell(BLOCK, vec![TOP, [100.0; 3]], -2.0), Expect::Fails(Error::NoMatch { point: 1 })),
|
||||
// The top front edge's midpoint is as near the top as the front.
|
||||
"shell_pick_ambiguous" => c(Op::Shell(BLOCK, vec![[15.0, 0.0, 10.0]], -2.0), Expect::Fails(Error::Ambiguous { point: 0 })),
|
||||
// A plate of 30 x 20 thickened 2: 1200, 2 thick along z; symmetric, from -1 to 1.
|
||||
"thicken_plate" => c(Op::Thicken(PLATE, 2.0, false), Expect::Slab(1200.0, 0.0, 2.0)),
|
||||
"thicken_plate_symmetric" => c(Op::Thicken(PLATE, 2.0, true), Expect::Slab(1200.0, -1.0, 1.0)),
|
||||
"thicken_a_solid" => c(Op::Thicken(BLOCK, 2.0, false), Expect::Fails(Error::Argument)),
|
||||
// The plate moved 3 along its normal (+z: the x axis swept along y): still 600, at z = 3.
|
||||
"offset_sheet_plate" => c(Op::OffsetSheet(PLATE, 3.0), Expect::Sheet(600.0, [0.0, 0.0, 3.0], [30.0, 20.0, 3.0])),
|
||||
// A sphere's one face closes on itself (tangency-labs/smlib fix #17): r = 10 moved out 2 is
|
||||
// the ball of radius 12, (4/3) π 12³; moved in 10, to its centre, nothing is left.
|
||||
"offset_sheet_a_sphere" => c(Op::OffsetSheet(BALL, 2.0), Expect::Volume(4.0 / 3.0 * PI * 12.0f64.powi(3))),
|
||||
"offset_sheet_a_sphere_to_its_centre" => c(Op::OffsetSheet(BALL, -10.0), Expect::Fails(Error::Empty)),
|
||||
// The quarter cylinder moved 2 along its normal (outward: the arc's tangent swept along
|
||||
// z): radius 7, area (π/2) 7 · 10.
|
||||
"offset_sheet_quarter_cylinder" => c(Op::OffsetSheet(QUARTER, 2.0), Expect::Sheet(PI / 2.0 * 7.0 * 10.0, [0.0, 0.0, 0.0], [7.0, 7.0, 10.0])),
|
||||
// A closed sheet offsets as an open one (tangency-labs/smlib fix #17, develop 9d0fb564):
|
||||
// each face moved out 1 along its normal, the convex edges extended, gives the block
|
||||
// (a + 2d)(b + 2d)(c + 2d) of six planes, from -d to (a, b, c) + d.
|
||||
"offset_sheet_a_solid" => c(Op::OffsetSheet(BLOCK, 1.0), Expect::Solid(32.0 * 22.0 * 12.0, [-1.0, -1.0, -1.0], [31.0, 21.0, 11.0])),
|
||||
// The top moved out 2: abc + d·ab.
|
||||
"offset_faces_top_out" => c(Op::OffsetFaces(BLOCK, vec![TOP], 2.0), Expect::Volume(ABC + 2.0 * 600.0)),
|
||||
// The top moved in 10, as deep as the block: nothing left behind it.
|
||||
"offset_faces_through" => c(Op::OffsetFaces(BLOCK, vec![TOP], -10.0), Expect::Fails(Error::NoRoom)),
|
||||
// The four sides drafted θ = 5° about the bottom (z = 0), pulled along +z, leaning in:
|
||||
// the section at height z is (a - 2 z tanθ)(b - 2 z tanθ), so
|
||||
// V = abh - (a + b) h² tanθ + (4/3) h³ tan²θ.
|
||||
"draft_box_sides" => {
|
||||
let s = (5.0f64).to_radians().tan();
|
||||
let (a, b, h) = (30.0, 20.0, 10.0);
|
||||
let sides = vec![[15.0, 0.0, 5.0], [30.0, 10.0, 5.0], [15.0, 20.0, 5.0], [0.0, 10.0, 5.0]];
|
||||
c(
|
||||
Op::Draft(BLOCK, sides, [0.0; 3], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], 5f64.to_radians()),
|
||||
Expect::Volume(a * b * h - (a + b) * h * h * s + 4.0 / 3.0 * h * h * h * s * s),
|
||||
)
|
||||
}
|
||||
// The front (y = 0) alone drafted θ = 5° about the bottom: a wedge of a h² tanθ / 2
|
||||
// comes off along its length.
|
||||
"draft_one_side" => {
|
||||
let s = (5.0f64).to_radians().tan();
|
||||
c(Op::Draft(BLOCK, vec![FRONT], [0.0; 3], Z, Z, 5f64.to_radians()), Expect::Volume(ABC - 30.0 * 100.0 * s / 2.0))
|
||||
}
|
||||
// A negative angle leans it out: the same wedge goes on.
|
||||
"draft_one_side_out" => {
|
||||
let s = (5.0f64).to_radians().tan();
|
||||
c(Op::Draft(BLOCK, vec![FRONT], [0.0; 3], Z, Z, -5f64.to_radians()), Expect::Volume(ABC + 30.0 * 100.0 * s / 2.0))
|
||||
}
|
||||
// Hinged at the top (z = 10) and leaning in going up, the face leans out below the
|
||||
// hinge: the wedge goes on.
|
||||
"draft_one_side_hinged_at_the_top" => {
|
||||
let s = (5.0f64).to_radians().tan();
|
||||
c(Op::Draft(BLOCK, vec![FRONT], [0.0, 0.0, 10.0], Z, Z, 5f64.to_radians()), Expect::Volume(ABC + 30.0 * 100.0 * s / 2.0))
|
||||
}
|
||||
// A cylinder along the pull drafts to a cone (tangency-labs/smlib fix #27, develop
|
||||
// 9d0fb564): R = 5, h = 10, θ = 0.1 about the bottom, leaning in: a frustum of radii R
|
||||
// and r = R - h tanθ (a cone and its two caps): V = π h (R² + R r + r²) / 3.
|
||||
"draft_a_curved_face" => {
|
||||
let (big, h) = (5.0, 10.0);
|
||||
let small = big - h * 0.1f64.tan();
|
||||
c(
|
||||
Op::Draft(Shape::Cylinder([0.0; 3], big, h), vec![[5.0, 0.0, 5.0]], [0.0; 3], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], 0.1),
|
||||
Expect::Volume(PI * h * (big * big + big * small + small * small) / 3.0),
|
||||
)
|
||||
}
|
||||
// A cone r 10 at its base to 8 at its top, 20 tall, drafted 5° about its base turns its
|
||||
// generators 5° more (fork d51a723): r 10 + 20 tan(atan(-0.1) - 5°) at the top.
|
||||
"draft_a_cone_about_the_pull" => {
|
||||
let (big, h, a) = (10.0, 20.0, 5f64.to_radians());
|
||||
let small = big + h * ((-0.1f64).atan() - a).tan();
|
||||
c(
|
||||
Op::Draft(Shape::Frustum([0.0; 3], big, 8.0, h), vec![[0.0, 9.0, 10.0]], [0.0; 3], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], a),
|
||||
Expect::Volume(PI * h * (big * big + big * small + small * small) / 3.0),
|
||||
)
|
||||
}
|
||||
// The top is square to the pull, not along it.
|
||||
"draft_a_face_across_the_pull" => {
|
||||
c(Op::Draft(BLOCK, vec![TOP], [0.0; 3], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], 0.1), Expect::Fails(Error::Unsupported))
|
||||
}
|
||||
_ => panic!("{name}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The tolerance a case picks with: the ambiguous point lies on its two faces.
|
||||
fn tolerance(name: &str) -> f64 {
|
||||
if name == "shell_pick_ambiguous" { 1.0 } else { TOL }
|
||||
}
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Measured {
|
||||
/// The volume of a solid, the area of a sheet.
|
||||
size: f64,
|
||||
bounds: ([f64; 3], [f64; 3]),
|
||||
counts: [usize; 3],
|
||||
kinds: BTreeMap<String, usize>,
|
||||
}
|
||||
|
||||
fn kinds(rec: &solidcraft_geom::brep::Brep) -> BTreeMap<String, usize> {
|
||||
let mut out = BTreeMap::new();
|
||||
for f in &rec.faces {
|
||||
let v = serde_json::to_value(&f.surface).unwrap();
|
||||
*out.entry(v["kind"].as_str().unwrap_or("?").to_owned()).or_default() += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn is_sheet(e: &Expect) -> bool {
|
||||
matches!(e, Expect::Sheet(..))
|
||||
}
|
||||
|
||||
fn on_core(name: &str, c: &Case) -> crate::Result<Body> {
|
||||
let tol = tolerance(name);
|
||||
match &c.op {
|
||||
Op::Shell(s, points, t) => shell(&s.core(), points, tol, *t),
|
||||
Op::OffsetFaces(s, points, d) => offset_faces(&s.core(), points, tol, *d),
|
||||
Op::Thicken(s, t, sym) => thicken(&s.core(), *t, *sym),
|
||||
Op::OffsetSheet(s, d) => offset_sheet(&s.core(), *d),
|
||||
Op::Draft(s, points, o, n, pull, angle) => draft(&s.core(), points, tol, *o, *n, *pull, *angle),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `f` on its own thread: a hang fails the test instead of stopping the run.
|
||||
fn timed<T: Send + 'static>(name: &str, f: impl FnOnce() -> T + Send + 'static) -> T {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let worker = std::thread::spawn(move || {
|
||||
let _ = tx.send(f());
|
||||
});
|
||||
match rx.recv_timeout(TIMEOUT) {
|
||||
Ok(v) => v,
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => match worker.join() {
|
||||
Err(p) => std::panic::resume_unwind(p),
|
||||
Ok(()) => panic!("{name}: no result"),
|
||||
},
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => panic!("{name}: solidcore hung (no result after {TIMEOUT:?})"),
|
||||
}
|
||||
}
|
||||
|
||||
type Got = std::result::Result<Measured, Error>;
|
||||
|
||||
fn near(a: [f64; 3], b: [f64; 3]) -> bool {
|
||||
a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-6)
|
||||
}
|
||||
|
||||
/// Checks a kernel's result against the case's closed form; what it measured, or its error.
|
||||
fn judge(name: &str, who: &str, expect: &Expect, got: Got) -> Got {
|
||||
match (expect, &got) {
|
||||
(Expect::Fails(e), Err(g)) => assert_eq!(g, e, "{name}: {who} failed otherwise"),
|
||||
(Expect::Fails(e), Ok(m)) => panic!("{name}: {who} gave {m:?}, expected {e:?}"),
|
||||
(_, Err(g)) => panic!("{name}: {who} failed: {g}"),
|
||||
(Expect::Volume(v), Ok(m)) => assert!(rel(m.size, *v) < 1e-9, "{name}: {who} volume {} vs analytic {v}", m.size),
|
||||
(Expect::Slab(v, lo, hi), Ok(m)) => {
|
||||
assert!(rel(m.size, *v) < 1e-9, "{name}: {who} volume {} vs analytic {v}", m.size);
|
||||
let (a, b) = (m.bounds.0[2], m.bounds.1[2]);
|
||||
assert!((a - lo).abs() < 1e-6 && (b - hi).abs() < 1e-6, "{name}: {who} z from {a} to {b}, expected {lo} to {hi}");
|
||||
}
|
||||
(Expect::Solid(v, lo, hi), Ok(m)) => {
|
||||
assert!(rel(m.size, *v) < 1e-9, "{name}: {who} volume {} vs analytic {v}", m.size);
|
||||
assert!(near(m.bounds.0, *lo) && near(m.bounds.1, *hi), "{name}: {who} bounds {:?}, expected {lo:?} {hi:?}", m.bounds);
|
||||
}
|
||||
(Expect::Sheet(s, lo, hi), Ok(m)) => {
|
||||
assert!(rel(m.size, *s) < 1e-9, "{name}: {who} area {} vs analytic {s}", m.size);
|
||||
assert!(near(m.bounds.0, *lo) && near(m.bounds.1, *hi), "{name}: {who} bounds {:?}, expected {lo:?} {hi:?}", m.bounds);
|
||||
}
|
||||
}
|
||||
got
|
||||
}
|
||||
|
||||
fn solidcore_case(name: &'static str) -> Got {
|
||||
timed(name, move || {
|
||||
let c = case(name);
|
||||
let sheet = is_sheet(&c.expect);
|
||||
let got = on_core(name, &c).map(|body| {
|
||||
let size = if sheet { area(&body, 1e-8) } else { volume(&body, 1e-8) }.unwrap_or_else(|e| panic!("{name}: size: {e}"));
|
||||
let n = counts(&body).unwrap();
|
||||
let (rec, _) = native(&to_arrays(&body).unwrap()).unwrap_or_else(|e| panic!("{name}: record: {e:?}"));
|
||||
rec.validate().unwrap_or_else(|e| panic!("{name}: record: {e}"));
|
||||
let bx = bounds(&body).unwrap();
|
||||
Measured { size, bounds: bx, counts: n, kinds: kinds(&rec) }
|
||||
});
|
||||
judge(name, "solidcore", &c.expect, got)
|
||||
})
|
||||
}
|
||||
|
||||
macro_rules! cases {
|
||||
($($m:ident $(: $ign:meta)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
#[test]
|
||||
$(#[$ign])?
|
||||
fn solidcore_matches_the_closed_form() {
|
||||
let _ = super::solidcore_case(stringify!($m));
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
cases!(
|
||||
shell_box_open_top,
|
||||
shell_box_closed,
|
||||
shell_box_closed_outward,
|
||||
shell_box_outward,
|
||||
shell_cylinder_open_top,
|
||||
shell_too_thick,
|
||||
shell_walls_touch,
|
||||
shell_pick_near_nothing,
|
||||
shell_pick_ambiguous,
|
||||
thicken_plate,
|
||||
thicken_plate_symmetric,
|
||||
thicken_a_solid,
|
||||
offset_sheet_plate,
|
||||
offset_sheet_quarter_cylinder,
|
||||
offset_sheet_a_solid,
|
||||
offset_sheet_a_sphere,
|
||||
offset_sheet_a_sphere_to_its_centre,
|
||||
offset_faces_top_out,
|
||||
offset_faces_through,
|
||||
draft_box_sides,
|
||||
draft_one_side,
|
||||
draft_one_side_out,
|
||||
draft_one_side_hinged_at_the_top,
|
||||
draft_a_curved_face,
|
||||
draft_a_cone_about_the_pull,
|
||||
draft_a_face_across_the_pull,
|
||||
);
|
||||
|
||||
/// The capi's argument checks, before any kernel call.
|
||||
#[test]
|
||||
fn hostile_arguments_are_refused() {
|
||||
let b = BLOCK.core();
|
||||
for t in [0.0, f64::NAN, -1e7, f64::INFINITY] {
|
||||
assert_eq!(shell(&b, &[TOP], TOL, t).err(), Some(Error::Argument), "thickness {t}");
|
||||
assert_eq!(thicken(&b, t, false).err(), Some(Error::Argument), "thickness {t}");
|
||||
assert_eq!(offset_sheet(&b, t).err(), Some(Error::Argument), "distance {t}");
|
||||
assert_eq!(offset_faces(&b, &[TOP], TOL, t).err(), Some(Error::Argument), "distance {t}");
|
||||
}
|
||||
assert_eq!(shell(&b, &[[f64::NAN; 3]], TOL, -1.0).err(), Some(Error::Argument));
|
||||
assert_eq!(shell(&b, &[TOP], 0.0, -1.0).err(), Some(Error::Argument));
|
||||
assert_eq!(offset_faces(&b, &[], TOL, 1.0).err(), Some(Error::Argument));
|
||||
let z = [0.0, 0.0, 1.0];
|
||||
assert_eq!(draft(&b, &[TOP], TOL, [0.0; 3], [0.0; 3], z, 0.1).err(), Some(Error::Argument), "no neutral normal");
|
||||
assert_eq!(draft(&b, &[TOP], TOL, [0.0; 3], z, z, 0.0).err(), Some(Error::Argument), "no angle");
|
||||
assert_eq!(draft(&b, &[TOP], TOL, [0.0; 3], z, z, PI / 2.0).err(), Some(Error::Argument), "a right angle");
|
||||
assert_eq!(draft(&b, &[TOP], TOL, [f64::NAN; 3], z, z, 0.1).err(), Some(Error::Argument));
|
||||
}
|
||||
|
||||
/// The body offset is never changed.
|
||||
#[test]
|
||||
fn the_body_stays_as_it_was() {
|
||||
let r = timed("operand", || {
|
||||
let b = BLOCK.core();
|
||||
let _ = shell(&b, &[TOP], TOL, -2.0);
|
||||
(counts(&b).unwrap(), volume(&b, 1e-8).unwrap())
|
||||
});
|
||||
assert_eq!(r.0, [6, 12, 8]);
|
||||
assert!(rel(r.1, ABC) < 1e-12, "{}", r.1);
|
||||
}
|
||||
|
||||
/// The distance from `p` to each face of `b` (`SmApiFaceClosestPoint`), under `identity`.
|
||||
fn face_distances(
|
||||
k: &mut solidcore::kernel::Kernel,
|
||||
b: &mut solidcore::topology::Brep,
|
||||
identity: &solidcore::query::brep::BrepIdentity<'_>,
|
||||
p: Vec3,
|
||||
) -> solidcore::Result<Vec<(f64, Vec3)>> {
|
||||
use solidcore::geom::object::Shape as Sh;
|
||||
use solidcore::query::api_queries::face_closest_point_in_shape;
|
||||
use solidcore::query::brep::LiveBrepObjects;
|
||||
use solidcore::topology::BrepId;
|
||||
let one = BrepId::from_index(0);
|
||||
let mut out = Vec::new();
|
||||
for f in solidcore::topology::brep::p1::faces(b, None)? {
|
||||
let shape = Sh::new(k, &(one, &*b), &[], &[(one, f)], &[], &[])?;
|
||||
let objects = LiveBrepObjects::new(b, identity, None)?;
|
||||
let c = face_closest_point_in_shape(k, &shape, &objects, (one, f), p)?;
|
||||
out.push((c.distance, c.point));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The closed form for (15, 10, 20) above the 30 x 20 x 10 box at the origin, face by face of
|
||||
/// `create_box`: bottom 20, top 10, the x sides sqrt(15^2 + 10^2), the y sides sqrt(10^2 + 10^2).
|
||||
fn box_face_distances_are_exact(what: &str, d: &[(f64, Vec3)]) {
|
||||
let want = [20.0, 10.0, 325f64.sqrt(), 325f64.sqrt(), 200f64.sqrt(), 200f64.sqrt()];
|
||||
assert_eq!(d.len(), want.len(), "{what}: faces");
|
||||
for (i, (&(got, _), want)) in d.iter().zip(want).enumerate() {
|
||||
assert!((got - want).abs() < 1e-9, "{what}: face {i} at {got}, want {want}");
|
||||
}
|
||||
let top = d[1].1;
|
||||
assert!((top.x - 15.0).abs() < 1e-9 && (top.y - 10.0).abs() < 1e-9 && (top.z - 10.0).abs() < 1e-9, "{what}: top {top:?}");
|
||||
}
|
||||
|
||||
/// Picking faces on a solidcore box: on the body itself, on the working copy every operation
|
||||
/// makes (`brep_copy`), and by solidcore's recommended usage, each face's distance is the closed
|
||||
/// form and the top face is picked.
|
||||
#[test]
|
||||
fn faces_are_picked_on_the_box_and_on_its_working_copy() {
|
||||
use solidcore::query::brep::{BrepIdentity, BrepQuery};
|
||||
let p = Vec3::new(15.0, 10.0, 20.0);
|
||||
let on_top = [[15.0, 10.0, 10.0]];
|
||||
run("pick", |k| {
|
||||
let mut b = solidcore::primitive::api_primitives::create_box(k, Vec3::new(0.0, 0.0, 0.0), 30.0, 20.0, 10.0)?;
|
||||
let mut w = solidcore::topology::brep::p3::brep_copy(k, &b)?;
|
||||
let mut faces = Vec::new();
|
||||
for (what, x) in [("the body", &mut b), ("its copy", &mut w)] {
|
||||
let query = BrepQuery::of_brep(x);
|
||||
let identity = query.effective_identity();
|
||||
box_face_distances_are_exact(what, &face_distances(k, x, &identity, p)?);
|
||||
let picked = super::offset::pick_faces(k, x, &on_top, TOL)?;
|
||||
faces.push(picked.ok());
|
||||
}
|
||||
assert!(faces[0].as_ref().is_some_and(|f| f.len() == 1), "the body: {faces:?}");
|
||||
assert_eq!(faces[0], faces[1], "the body and its copy pick the same face");
|
||||
// `BrepIdentity::of_brep` honours a copy's owner keys since solidcore#1445: the body
|
||||
// and its copy both resolve.
|
||||
for (what, x) in [("of_brep on the body", &mut b), ("of_brep on its copy", &mut w)] {
|
||||
let proven = BrepIdentity::of_brep(x);
|
||||
box_face_distances_are_exact(what, &face_distances(k, x, &proven.identity(), p)?);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// A pick skips an entity whose query returns an SMLib status, as the capi skips any status
|
||||
/// other than `SM_SUCCESS`; any other solidcore error fails the pick.
|
||||
#[test]
|
||||
fn a_solidcore_error_in_a_pick_is_an_error() {
|
||||
use super::answer;
|
||||
use super::blend::pick;
|
||||
use solidcore::SolidError;
|
||||
let all = [0usize, 1];
|
||||
let points = [[0.0; 3]];
|
||||
let status = pick(&all, &points, 1.0, 1e-6, |i, _| answer(if i == 0 { Err(SolidError::sm_err("q")) } else { Ok(0.5) }));
|
||||
assert_eq!(status.unwrap().unwrap().0, vec![1], "a status skips the entity");
|
||||
let other = pick(&all, &points, 1.0, 1e-6, |i, _| answer(if i == 0 { Err(SolidError::invalid_input("q", "owner")) } else { Ok(0.5) }));
|
||||
assert!(matches!(other, Err(SolidError::InvalidInput { .. })), "{other:?}");
|
||||
let unported = pick(&all, &points, 1.0, 1e-6, |i, _| answer(if i == 0 { Err(SolidError::unported("Sm::X", "u")) } else { Ok(0.5) }));
|
||||
assert!(matches!(unported, Err(SolidError::Unported { .. })), "{unported:?}");
|
||||
}
|
||||
|
||||
/// The capi's `moved_faces` reading: no mark, no move (SMLIB_ERR_NO_EFFECT); a mark is read once,
|
||||
/// and gone.
|
||||
#[test]
|
||||
fn a_draft_with_no_marked_face_left_has_no_effect() {
|
||||
let body = make_box([0.0; 3], [10.0; 3]).unwrap();
|
||||
super::with_body("marks", &body, |_, w| {
|
||||
assert!(!moved_faces(w)?, "no face marked");
|
||||
let f = w.face_list()[0];
|
||||
mark_moved(w, &[f])?;
|
||||
assert!(moved_faces(w)?, "a face marked");
|
||||
assert!(!moved_faces(w)?, "the mark removed");
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// A draft leaves no mark of its own on what it returns, on a plane or on a cylinder turned cone.
|
||||
#[test]
|
||||
fn a_draft_leaves_no_mark() {
|
||||
let z = [0.0, 0.0, 1.0];
|
||||
let bx = make_box([0.0; 3], [20.0, 30.0, 10.0]).unwrap();
|
||||
let plane = draft(&bx, &[[10.0, 0.0, 5.0]], TOL, [0.0; 3], z, z, 0.1).unwrap();
|
||||
let cyl = make_cylinder([0.0; 3], 5.0, 10.0).unwrap();
|
||||
let cone = draft(&cyl, &[[5.0, 0.0, 5.0]], TOL, [0.0; 3], z, z, 0.1).unwrap();
|
||||
for b in [plane, cone] {
|
||||
super::with_body("marks", &b, |_, w| {
|
||||
assert!(!moved_faces(w)?);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// are one face of a 6-faced box.
|
||||
#[test]
|
||||
fn offset_faces_close_a_step_wall() {
|
||||
use super::boolean::boolean;
|
||||
let stepped = boolean(&make_box([0.0; 3], [40.0, 30.0, 10.0]).unwrap(), &make_box([0.0; 3], [20.0, 30.0, 20.0]).unwrap(), BooleanOp::Union)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(counts(&stepped).unwrap()[0], 8);
|
||||
for (at, d, v, a) in [([30.0, 15.0, 10.0], 10.0, 24000.0, 5200.0), ([10.0, 15.0, 20.0], -10.0, 12000.0, 3800.0)] {
|
||||
let r = offset_faces(&stepped, &[at], 0.1, d).unwrap();
|
||||
let (got, n, area) = (volume(&r, 1e-8).unwrap(), counts(&r).unwrap(), super::area(&r, 1e-8).unwrap());
|
||||
assert_eq!(n[0], 6, "d {d}");
|
||||
assert!(rel(got, v) < 1e-9 && rel(area, a) < 1e-9, "d {d}: V {got} A {area}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Rig case shell/4492's rounded box: four mitred rounds, r 3, come off a 40 x 30 x 10 box's top
|
||||
/// together and follow it. From height 0 the outline is the 34 x 24 top moved out by the round's
|
||||
/// w(z) = sqrt(9 - (z - axis)^2), or 3 below the axis. With the fork's values at +19, and bodies
|
||||
/// whose rounds go all the way down from -7 and are cut short from -7.5 (solidcore#2027). At -9
|
||||
/// solidcore refuses to put the rounds back (solidcore#2020, open): SolidCraft says it does not
|
||||
/// make that move.
|
||||
#[test]
|
||||
fn mitred_rounds_follow_a_moved_top() {
|
||||
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]];
|
||||
let top = [20.0, 15.0, 10.0];
|
||||
let rounded = blend(&make_box([0.0; 3], [40.0, 30.0, 10.0]).unwrap(), Blend::Round, &edges, &[3.0; 4], TOL, &[], &[], Corners::Filled).unwrap();
|
||||
let analytic = |d: f64| {
|
||||
let (height, axis, n) = (10.0 + d, 7.0 + d, 20000);
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let z = height * (f64::from(i) + 0.5) / f64::from(n);
|
||||
let w = if z < axis { 3.0 } else { (9.0 - (z - axis) * (z - axis)).max(0.0).sqrt() };
|
||||
(34.0 + 2.0 * w) * (24.0 + 2.0 * w) * height / f64::from(n)
|
||||
})
|
||||
.sum::<f64>()
|
||||
};
|
||||
let moves = [
|
||||
(19.0, [10, 20, 12]),
|
||||
(1.0, [10, 20, 12]),
|
||||
(-1.0, [10, 20, 12]),
|
||||
(-6.9, [10, 20, 12]),
|
||||
(-7.0, [6, 12, 8]),
|
||||
(-7.5, [6, 12, 8]),
|
||||
(-8.0, [6, 12, 8]),
|
||||
(-9.5, [6, 12, 8]),
|
||||
];
|
||||
for (d, n) in moves {
|
||||
let moved = offset_faces(&rounded, &[top], 0.1, d).unwrap();
|
||||
let v = volume(&moved, 1e-8).unwrap();
|
||||
assert_eq!(counts(&moved).unwrap(), n, "d {d}");
|
||||
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)));
|
||||
}
|
||||
|
||||
/// `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);
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
//! Split Face and Split Body on solidcore: the C API's composition, ported from the fork's
|
||||
//! `capi/src/smlib_split.cpp` (`smlib_split_faces`, `smlib_split_body`, `cut_face`,
|
||||
//! `plane_covers`, `slice`, `both_sides`, `smlib_split_body_by_sheet`) and the tool surfaces of `capi/src/smlib_ssi.cpp`
|
||||
//! (`smlib_surface_plane`, `_cylinder`, `_cone`, `_sphere`, `_torus`, `_nurbs`).
|
||||
//!
|
||||
//! As there, every operation works on copies of its body (`SmApiBrepCopy`). A face is cut where
|
||||
//! a tool meets it the way SMLib's plane cutter cuts a subset of faces: the face's surface is
|
||||
//! intersected with the tool over the face's UV domain (`CreatePlanarSectionCurves` for a plane
|
||||
//! reaching over the face, `GlobalSurfaceIntersect` otherwise) and the untrimmed curves go to
|
||||
//! `SmBrep::MergeCurvesOnSurface` with the tool as the intersecting surface. Unlike the plane
|
||||
//! cutter, a failed merge fails the call. A body is split by slicing (`SM_BO_SLICE`) copies of it
|
||||
//! with a one-face sheet on the tool surface, once with the surface turned over; or with a sheet
|
||||
//! of several faces as it is, its surfaces first turned to agree.
|
||||
//!
|
||||
//! A tool is a [`crate::ssi::Surface`]; solidcore's own surface is made from its description
|
||||
//! ([`Spec`]), as `smlib_ssi.cpp` makes SMLib's, over the surface's natural UV domain.
|
||||
|
||||
use std::sync::PoisonError;
|
||||
|
||||
use solidcore::EntityRef;
|
||||
use solidcore::boolean::api_brep::boolean_with_options;
|
||||
use solidcore::boolean::merge::MergeOptions;
|
||||
use solidcore::geom::a_object::BooleanOperation;
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::extent2d::Extent2d;
|
||||
use solidcore::geom::object::ObjectRef;
|
||||
use solidcore::geom::tol::{USE_DEFAULT, approx_tol3d};
|
||||
use solidcore::geom::vector2d::Vec2;
|
||||
use solidcore::geom::vector3d::{Point3, Vec3};
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::nurbs::b_spline_curve::p1::KnotType;
|
||||
use solidcore::nurbs::b_spline_surface::p1::{BSplineSurface, BSplineSurfaceForm, CanonicalSurfaceData};
|
||||
use solidcore::nurbs::math::CPoint;
|
||||
use solidcore::query::brep::BrepFaceServices;
|
||||
use solidcore::query::face::p1::calculate_bounding_box;
|
||||
use solidcore::surface::cone::p1::Cone;
|
||||
use solidcore::surface::plane::p1::Plane;
|
||||
use solidcore::surface::sphere::{Sphere, SphereOptions};
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::surface::surface::p2::TsectCurveType;
|
||||
use solidcore::surface::torus::{Torus, TorusOptions};
|
||||
use solidcore::topology::brep::p1 as brep_p1;
|
||||
use solidcore::topology::brep::p2::{is_manifold_solid, is_sheet};
|
||||
use solidcore::topology::brep::p3::{new_brep, with_kernel_callbacks};
|
||||
use solidcore::topology::brep::p8::create_face_from_surface;
|
||||
use solidcore::topology::brep::p9::orient_sheet_surfaces;
|
||||
use solidcore::topology::brep::p10::merge_curves_on_surface_through_surface;
|
||||
use solidcore::topology::brep::planar_section::face_planar_section_curves;
|
||||
use solidcore::topology::brep_cache::CacheObject;
|
||||
use solidcore::topology::edge::NotifyEnv;
|
||||
use solidcore::topology::face::p2 as face_p2;
|
||||
use solidcore::topology::{Brep, EdgeId, FaceId};
|
||||
|
||||
use super::blend::status;
|
||||
use super::sweep::build_faces;
|
||||
use super::{Body, run};
|
||||
use crate::ssi::Spec;
|
||||
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 = 100_000;
|
||||
pub(crate) const MAX_TOOLS: usize = 4096;
|
||||
|
||||
/// `SM_ERR`.
|
||||
const SM_ERR: i32 = 1001;
|
||||
|
||||
/// The capi's statuses as SolidCraft errors: an operation's own result.
|
||||
type Out<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// A tool surface as solidcore holds it: the surface over its natural UV domain (`smlib_surface`'s
|
||||
/// `s` and `domain`), and for a plane its point and unit normal (`IsPlanar`).
|
||||
pub(crate) struct Tool {
|
||||
pub(super) surface: Surface,
|
||||
pub(super) domain: Extent2d,
|
||||
plane: Option<(Point3, Vec3)>,
|
||||
}
|
||||
|
||||
fn v3(p: [f64; 3]) -> Vec3 {
|
||||
Vec3::new(p[0], p[1], p[2])
|
||||
}
|
||||
|
||||
/// The runner's `unit_cross`.
|
||||
fn unit_cross(a: Vec3, b: Vec3) -> Vec3 {
|
||||
let c = a.cross(b);
|
||||
let l = c.length();
|
||||
Vec3::new(c.x / l, c.y / l, c.z / l)
|
||||
}
|
||||
|
||||
/// `[k0, k0, k1, ...]` as unique values and their multiplicities (`unique_knots`).
|
||||
fn unique_knots(k: &[f64]) -> (Vec<f64>, Vec<usize>) {
|
||||
let (mut values, mut mults): (Vec<f64>, Vec<usize>) = (Vec::new(), Vec::new());
|
||||
for &x in k {
|
||||
match (values.last(), mults.last_mut()) {
|
||||
(Some(&last), Some(m)) if last == x => *m += 1,
|
||||
_ => {
|
||||
values.push(x);
|
||||
mults.push(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
(values, mults)
|
||||
}
|
||||
|
||||
/// `clamped`: the first and the last knot each p + 1 times.
|
||||
fn clamped(k: &[f64], p: usize) -> bool {
|
||||
match (k.first(), k.get(p), k.last(), k.len().checked_sub(1 + p).and_then(|i| k.get(i))) {
|
||||
(Some(a), Some(b), Some(c), Some(d)) => a == b && c == d,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The tool's surface as `smlib_ssi.cpp`'s constructors make it, then its natural UV domain
|
||||
/// (`construct`: finite and not empty, else `SM_ERR_INVALID_INPUT`).
|
||||
pub(super) fn tool(k: &Kernel, spec: &Spec) -> solidcore::Result<Out<Tool>> {
|
||||
let mut plane = None;
|
||||
let surface = match spec {
|
||||
Spec::Plane { origin, xaxis, yaxis, u, v } => {
|
||||
let dom = Extent2d::from_bounds(u[0], v[0], u[1], v[1]);
|
||||
plane = Some((v3(*origin), unit_cross(v3(*xaxis), v3(*yaxis))));
|
||||
Surface::from(Plane::new(v3(*origin), v3(*xaxis), v3(*yaxis), Vec2::new(1.0, 1.0), &dom))
|
||||
}
|
||||
Spec::Cylinder { origin, xaxis, yaxis, radius, angle_deg, height } => {
|
||||
let (x, y) = (v3(*xaxis), v3(*yaxis));
|
||||
let o = v3(*origin) + unit_cross(x, y) * height[0];
|
||||
Surface::from(Cone::new_cylinder_bounded(o, x, y, *radius, angle_deg[0], angle_deg[1], height[1] - height[0], false, false, false))
|
||||
}
|
||||
Spec::Cone { apex, xaxis, yaxis, half_angle_deg, height } => {
|
||||
let (x, y) = (v3(*xaxis), v3(*yaxis));
|
||||
let (h0, h1) = (height[0], height[1]);
|
||||
let t = half_angle_deg.to_radians().tan();
|
||||
let o = v3(*apex) + unit_cross(x, y) * h0;
|
||||
let pl = Axis2Placement::new(o, x, y);
|
||||
Surface::from(Cone::create_canonical(k, &pl, h0 * t, h1 * t, h1 - h0, false, false)?)
|
||||
}
|
||||
Spec::Sphere { center, xaxis, yaxis, radius, lon_deg, lat_deg } => {
|
||||
let anal = Extent2d::from_bounds(lon_deg[0], lat_deg[0], lon_deg[1], lat_deg[1]);
|
||||
let opts = SphereOptions { swap_uv: false, inside_out: false, circle_nurb: None };
|
||||
Surface::from(Sphere::new(k, v3(*center), v3(*xaxis), v3(*yaxis), anal, *radius, opts)?)
|
||||
}
|
||||
Spec::Torus { center, xaxis, yaxis, major_radius, minor_radius, major_deg, minor_deg } => {
|
||||
let anal = Extent2d::from_bounds(major_deg[0], minor_deg[0], major_deg[1], minor_deg[1]);
|
||||
Surface::from(Torus::new(k, v3(*center), v3(*xaxis), v3(*yaxis), anal, *major_radius, *minor_radius, TorusOptions::default())?)
|
||||
}
|
||||
Spec::Nurbs { degree, nu, nv, knots_u, knots_v, points, weights } => {
|
||||
let [p, q] = *degree;
|
||||
let w = |i: usize| weights.as_ref().and_then(|w| w.get(i).copied()).unwrap_or(1.0);
|
||||
if !clamped(knots_u, p) || !clamped(knots_v, q) {
|
||||
// SMLib's component constructor clamps the surface to its domain.
|
||||
let (ku, mu) = unique_knots(knots_u);
|
||||
let (kv, mv) = unique_knots(knots_v);
|
||||
let wts: Vec<f64> = (0..points.len()).map(w).collect();
|
||||
let pts: Vec<Point3> = points.iter().map(|p| v3(*p)).collect();
|
||||
let data = CanonicalSurfaceData {
|
||||
u_degree: p,
|
||||
v_degree: q,
|
||||
control_points: &pts,
|
||||
form: BSplineSurfaceForm::Unspecified,
|
||||
u_multiplicities: &mu,
|
||||
v_multiplicities: &mv,
|
||||
u_knots: &ku,
|
||||
v_knots: &kv,
|
||||
knot_type: KnotType::Unspecified,
|
||||
weights: Some(&wts),
|
||||
uv_domain: None,
|
||||
};
|
||||
Surface::from(BSplineSurface::create_canonical(&data)?)
|
||||
} else {
|
||||
// The runner's way: the homogeneous net filled directly.
|
||||
let mut pw = Vec::with_capacity(*nu);
|
||||
for i in 0..*nu {
|
||||
let mut row = Vec::with_capacity(*nv);
|
||||
for j in 0..*nv {
|
||||
let at = i * nv + j;
|
||||
let (Some(pt), wt) = (points.get(at), w(at)) else {
|
||||
return Ok(Err(Error::Argument));
|
||||
};
|
||||
row.push(CPoint::new(pt[0] * wt, pt[1] * wt, pt[2] * wt, wt));
|
||||
}
|
||||
pw.push(row);
|
||||
}
|
||||
let (n, m) = (nu.saturating_sub(1), nv.saturating_sub(1));
|
||||
let (r, s) = (knots_u.len().saturating_sub(1), knots_v.len().saturating_sub(1));
|
||||
Surface::from(BSplineSurface::from_data(n, m, p, q, r, s, &pw, knots_u, knots_v)?)
|
||||
}
|
||||
}
|
||||
};
|
||||
let domain = surface.natural_uv_domain_with_nurb(k)?;
|
||||
let (lo, hi) = (domain.min(), domain.max());
|
||||
if !(lo.x.is_finite() && lo.y.is_finite() && hi.x.is_finite() && hi.y.is_finite() && lo.x < hi.x && lo.y < hi.y) {
|
||||
return Ok(Err(Error::Status(1007)));
|
||||
}
|
||||
Ok(Ok(Tool { surface, domain, plane }))
|
||||
}
|
||||
|
||||
/// `plane_covers`: whether the plane `tool` reaches over all of `bx` (each corner's foot on the
|
||||
/// plane lies in the tool's parameter box): then its section with a face in the box is the
|
||||
/// infinite plane's.
|
||||
fn plane_covers(k: &Kernel, tool: &Tool, bx: &solidcore::geom::extent3d::Extent3d, tol: f64) -> bool {
|
||||
let (lo, hi) = (tool.domain.min(), tool.domain.max());
|
||||
let at = |u: f64, v: f64| tool.surface.evaluate_point(k, Vec2::new(u, v)).ok();
|
||||
let (Some(o), Some(pu), Some(pv)) = (at(lo.x, lo.y), at(hi.x, lo.y), at(lo.x, hi.y)) else {
|
||||
return false;
|
||||
};
|
||||
// The plane is affine in (u, v): P = o + (u - u0) X + (v - v0) Y.
|
||||
let x = (pu - o) / (hi.x - lo.x);
|
||||
let y = (pv - o) / (hi.y - lo.y);
|
||||
let (a, b, c) = (x.dot(x), x.dot(y), y.dot(y));
|
||||
let det = a * c - b * b;
|
||||
if !(det.abs() > 0.0) {
|
||||
return false;
|
||||
}
|
||||
let (m, mm) = (bx.min(), bx.max());
|
||||
(0..8).all(|kk| {
|
||||
let q = Vec3::new(if kk & 1 != 0 { mm.x } else { m.x }, if kk & 2 != 0 { mm.y } else { m.y }, if kk & 4 != 0 { mm.z } else { m.z });
|
||||
let r = q - o;
|
||||
let (g0, g1) = (x.dot(r), y.dot(r));
|
||||
let u = lo.x + (c * g0 - b * g1) / det;
|
||||
let v = lo.y + (a * g1 - b * g0) / det;
|
||||
let (su, sv) = (tol / a.sqrt(), tol / c.sqrt());
|
||||
u >= lo.x - su && u <= hi.x + su && v >= lo.y - sv && v <= hi.y + sv
|
||||
})
|
||||
}
|
||||
|
||||
/// The name of each edge curve a merge makes (`SmApiCut`'s).
|
||||
fn curve_ref(e: EdgeId, _type_code: i64) -> ObjectRef {
|
||||
solidcore::topology::loopuse::notify_ref(EntityRef::Edge(e))
|
||||
}
|
||||
|
||||
/// `cut_face`: cuts `face` where `tool` meets it; the faces it was split into besides itself.
|
||||
fn cut_face(k: &mut Kernel, b: &mut Brep, env: &mut NotifyEnv<'_>, face: FaceId, tool: &Tool, tol: f64) -> solidcore::Result<Out<Vec<FaceId>>> {
|
||||
let Some(surface) = face_p2::surface(b, face)? else {
|
||||
return Ok(Err(Error::Status(SM_ERR)));
|
||||
};
|
||||
let surface = surface.clone_for_evaluation()?;
|
||||
let domain = face_p2::uv_domain(b, face)?;
|
||||
let approx = approx_tol3d(k, approx_tol3d(k, tol) / 4.0);
|
||||
let bx = match calculate_bounding_box(k, &mut *b, face, false) {
|
||||
Ok(bx) => bx,
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
};
|
||||
let merge =
|
||||
|k: &mut Kernel, b: &mut Brep, env: &mut NotifyEnv<'_>, mut c3: Vec<_>, uv: Option<Vec<_>>| match merge_curves_on_surface_through_surface(
|
||||
k,
|
||||
b,
|
||||
env,
|
||||
&curve_ref,
|
||||
face,
|
||||
tol,
|
||||
&mut c3,
|
||||
uv.as_deref(),
|
||||
Some(tool.surface.clone()),
|
||||
) {
|
||||
Ok((made, _)) => Ok(Ok(made)),
|
||||
Err(e) => Ok(Err(status(e))),
|
||||
};
|
||||
// A plane reaching over the face sections it exactly (SmPlaneCutter's way); a plane strip,
|
||||
// like any other tool, is intersected over its own domain.
|
||||
if let Some((at, normal)) = tool.plane
|
||||
&& plane_covers(k, tool, &bx, tol)
|
||||
{
|
||||
// As SmPlaneCutter: a face far from the plane is not sectioned.
|
||||
if bx.distance_to_plane(at, normal).abs() > tol {
|
||||
return Ok(Ok(Vec::new()));
|
||||
}
|
||||
let section = match face_planar_section_curves(k, b, face, &surface, &domain, at, normal, Some(approx), None) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
};
|
||||
if section.curves_3d.is_empty() {
|
||||
return Ok(Ok(Vec::new()));
|
||||
}
|
||||
let uv = (section.uv_curves.len() == section.curves_3d.len()).then_some(section.uv_curves);
|
||||
return merge(k, b, env, section.curves_3d, uv);
|
||||
}
|
||||
// `face->GetSurface()` reaches its face's trimmed cache: the solve runs through the B-rep's
|
||||
// face services, as solidcore's own face intersections do (a face-owned surface has no cache
|
||||
// of its own).
|
||||
let mut services = BrepFaceServices::new(&mut *b);
|
||||
let found = match surface.global_surface_intersect_with_context(
|
||||
k,
|
||||
[Some(CacheObject::Surface(face)), None],
|
||||
&mut services,
|
||||
&domain,
|
||||
&tool.surface,
|
||||
&tool.domain,
|
||||
[true, true],
|
||||
Some(approx),
|
||||
None,
|
||||
) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
};
|
||||
// A single point of contact cuts nothing.
|
||||
let (mut c3, mut uv1) = (Vec::new(), Vec::new());
|
||||
for (i, c) in found.curves_3d.into_iter().enumerate() {
|
||||
if found.curve_types.get(i) == Some(&TsectCurveType::Touching) {
|
||||
continue;
|
||||
}
|
||||
c3.push(c);
|
||||
uv1.push(found.surface1_uv_curves.get(i).cloned().flatten());
|
||||
}
|
||||
if c3.is_empty() {
|
||||
return Ok(Ok(Vec::new()));
|
||||
}
|
||||
let all_uv = uv1.iter().all(Option::is_some);
|
||||
merge(k, b, env, c3, all_uv.then_some(uv1))
|
||||
}
|
||||
|
||||
/// `smlib_split_faces`: a copy of `body` with faces `faces` (indices in its face order, distinct)
|
||||
/// cut where each tool meets them, the tools in turn, each cutting the pieces the earlier ones
|
||||
/// left; and for each face how many faces it became besides itself. `None` when no face is split
|
||||
/// (`SMLIB_ERR_NO_CUT`).
|
||||
pub(crate) fn split_faces(body: &Body, faces: &[usize], tools: &[&Spec]) -> Result<Option<(Body, Vec<usize>)>> {
|
||||
if faces.is_empty() || faces.len() > MAX_FACES || tools.is_empty() || tools.len() > MAX_TOOLS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if body.0.is_poisoned() {
|
||||
return Err(Error::Internal("smlib_split_faces: the body was left unfinished by an earlier panic".to_owned()));
|
||||
}
|
||||
let r = run("smlib_split_faces", |k| {
|
||||
let mut a = {
|
||||
let g = body.0.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
super::dump::input(k, "brep", &g);
|
||||
let count = brep_p1::faces(&g, None)?.len();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
if faces.iter().any(|&f| f >= count || !seen.insert(f)) {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
brep_copy(k, &g)?
|
||||
};
|
||||
let all = brep_p1::faces(&a, None)?;
|
||||
// Each chosen face and, as tools cut it, its pieces.
|
||||
let mut pieces: Vec<Vec<FaceId>> = Vec::with_capacity(faces.len());
|
||||
for &f in faces {
|
||||
let Some(&face) = all.get(f) else { return Ok(Err(Error::Status(SM_ERR))) };
|
||||
pieces.push(vec![face]);
|
||||
}
|
||||
let mut made = vec![0usize; faces.len()];
|
||||
let mut built = Vec::with_capacity(tools.len());
|
||||
for spec in tools {
|
||||
match tool(k, spec)? {
|
||||
Ok(t) => built.push(t),
|
||||
Err(e) => return Ok(Err(e)),
|
||||
}
|
||||
}
|
||||
let editing = a.editing_enabled();
|
||||
let doing = k.context.doing_boolean();
|
||||
a.set_editing_enabled(true);
|
||||
k.context.set_doing_boolean(true);
|
||||
let tol = a.tolerance();
|
||||
// The edits notify through the kernel's callbacks with the body's own attributes, so
|
||||
// the history tags follow each cut (as SMLib's objects notify their attributes).
|
||||
let cut = with_kernel_callbacks(k, &mut a, |k, a, env| {
|
||||
let mut total = 0usize;
|
||||
for t in &built {
|
||||
for (i, list) in pieces.iter_mut().enumerate() {
|
||||
let now = list.clone();
|
||||
for f in now {
|
||||
let split = match cut_face(k, a, env, f, t, tol)? {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
if let Some(m) = made.get_mut(i) {
|
||||
*m += split.len();
|
||||
}
|
||||
total += split.len();
|
||||
list.extend(split);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Ok(total))
|
||||
});
|
||||
a.set_editing_enabled(editing);
|
||||
k.context.set_doing_boolean(doing);
|
||||
a.refresh_cached_topology();
|
||||
// The cut's new edges own their curves under the kernel's identities (it records them),
|
||||
// not the copy's Notify ones, and the check reads the owners by the body's naming.
|
||||
a.set_naming(solidcore::topology::NamingScheme::Caller);
|
||||
Ok(match cut? {
|
||||
Err(e) => Err(e),
|
||||
Ok(0) => Ok(None),
|
||||
Ok(_) => Ok(Some((a, made))),
|
||||
})
|
||||
})??;
|
||||
Ok(r.map(|(b, made)| (Body::new(b), made)))
|
||||
}
|
||||
|
||||
/// `slice`: a copy of `body` sliced by `sheet` (SMLib's slice reads the side from the surfaces, not
|
||||
/// the faces' uses). `None` when nothing is left.
|
||||
fn slice(k: &mut Kernel, body: &Body, sheet: Brep) -> solidcore::Result<Out<Option<Brep>>> {
|
||||
let a = {
|
||||
let g = body.0.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
brep_copy(k, &g)?
|
||||
};
|
||||
let options = MergeOptions::new(false, false, false, false, false);
|
||||
let r = match boolean_with_options(k, a, sheet, BooleanOperation::Slice, options) {
|
||||
Ok(r) => r.result,
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
};
|
||||
Ok(Ok((!r.face_list().is_empty()).then_some(r)))
|
||||
}
|
||||
|
||||
/// `both_sides`: both parts, `side` making each side's sheet (`true`: turned over, for the back).
|
||||
/// A body that is no manifold solid is [`Error::NotSolid`]; a side left empty is `None`
|
||||
/// (`SMLIB_ERR_NO_CUT`), and with `whole` so is a part that is no manifold solid: `SM_BO_SLICE`
|
||||
/// with a sheet that only nicks a region behaves as a difference, leaving the sheet's piece as a
|
||||
/// slit in the body and a lamina on the other side, which is no cut.
|
||||
fn both_sides(
|
||||
k: &mut Kernel,
|
||||
body: &Body,
|
||||
whole: bool,
|
||||
mut side: impl FnMut(&mut Kernel, bool) -> solidcore::Result<Out<Brep>>,
|
||||
) -> solidcore::Result<Out<Option<(Brep, Brep)>>> {
|
||||
let solid = {
|
||||
let g = body.0.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
is_manifold_solid(&g)?
|
||||
};
|
||||
if !solid {
|
||||
return Ok(Err(Error::NotSolid));
|
||||
}
|
||||
let mut parts = [None, None];
|
||||
for (part, flip) in parts.iter_mut().zip([true, false]) {
|
||||
let sheet = match side(k, flip)? {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
*part = match slice(k, body, sheet)? {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
}
|
||||
let [Some(back), Some(front)] = parts else { return Ok(Ok(None)) };
|
||||
if whole && !(is_manifold_solid(&back)? && is_manifold_solid(&front)?) {
|
||||
return Ok(Ok(None));
|
||||
}
|
||||
Ok(Ok(Some((back, front))))
|
||||
}
|
||||
|
||||
/// One side of `smlib_split_body`: a one-face sheet on the tool's surface, turned over with `flip`.
|
||||
fn side(k: &mut Kernel, body: &Body, spec: &Spec, flip: bool) -> solidcore::Result<Out<Brep>> {
|
||||
let tolerance = body.0.lock().unwrap_or_else(PoisonError::into_inner).tolerance();
|
||||
let t = match tool(k, spec)? {
|
||||
Ok(t) => t,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
let mut surface = t.surface.copy(k)?;
|
||||
if flip && let Err(e) = surface.swap_uv(k) {
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
let mut sheet = new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
sheet.set_tolerance(tolerance);
|
||||
let natural = surface.natural_uv_domain_with_nurb(k)?;
|
||||
if let Err(e) = build_faces(&mut sheet, |s, fenv| create_face_from_surface(k, s, Some(surface), &natural, fenv)) {
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
sheet.refresh_cached_topology();
|
||||
Ok(Ok(sheet))
|
||||
}
|
||||
|
||||
/// `smlib_split_body`: the manifold solid `body` cut in two by the tool: the part behind the
|
||||
/// surface (its normal points away) and the part in front. A side left empty is `None`
|
||||
/// (`SMLIB_ERR_NO_CUT`); a body that is no manifold solid is [`Error::NotSolid`].
|
||||
pub(crate) fn split_body(body: &Body, spec: &Spec) -> Result<Option<(Body, Body)>> {
|
||||
if body.0.is_poisoned() {
|
||||
return Err(Error::Internal("smlib_split_body: the body was left unfinished by an earlier panic".to_owned()));
|
||||
}
|
||||
let r = run("smlib_split_body", |k| {
|
||||
super::dump::input(k, "brep", &body.0.lock().unwrap_or_else(PoisonError::into_inner));
|
||||
both_sides(k, body, false, |k, flip| side(k, body, spec, flip))
|
||||
})??;
|
||||
Ok(r.map(|(b, f)| (Body::new(b), Body::new(f))))
|
||||
}
|
||||
|
||||
/// `smlib_split_body_by_sheet`: the manifold solid `body` cut in two by `sheet` as it is (one or
|
||||
/// more faces joined edge to edge), its surfaces turned to agree with face `face`'s
|
||||
/// (`SmBrep::OrientSheetSurfaces`), for the back all turned over: the part behind that normal and
|
||||
/// the part in front. A sheet that does not divide the body is `None` (`SMLIB_ERR_NO_CUT`). A
|
||||
/// `sheet` that is no sheet, or a `face` out of range, is [`Error::Argument`].
|
||||
pub(crate) fn split_body_by_sheet(body: &Body, sheet: &Body, face: usize) -> Result<Option<(Body, Body)>> {
|
||||
if std::ptr::eq(body, sheet) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if body.0.is_poisoned() || sheet.0.is_poisoned() {
|
||||
return Err(Error::Internal("smlib_split_body_by_sheet: a body was left unfinished by an earlier panic".to_owned()));
|
||||
}
|
||||
let r = run("smlib_split_body_by_sheet", |k| {
|
||||
super::dump::input(k, "brep", &body.0.lock().unwrap_or_else(PoisonError::into_inner));
|
||||
{
|
||||
let g = sheet.0.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
super::dump::input(k, "sheet", &g);
|
||||
if !is_sheet(k, &g)? || face >= g.face_list().len() {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
}
|
||||
both_sides(k, body, true, |k, flip| {
|
||||
let mut copy = {
|
||||
let g = sheet.0.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
brep_copy(k, &g)?
|
||||
};
|
||||
let Some(&f) = brep_p1::faces(©, None)?.get(face) else { return Ok(Err(Error::Argument)) };
|
||||
// The UV swaps notify as SMLib's objects do: through the kernel's callbacks, with
|
||||
// the sheet's own attributes, so its faces' tags see each swap.
|
||||
let turned = with_kernel_callbacks(k, &mut copy, |k, b, env| orient_sheet_surfaces(k, b, Some(f), !flip, env));
|
||||
copy.refresh_cached_topology();
|
||||
Ok(match turned {
|
||||
Ok(()) => Ok(copy),
|
||||
Err(e) => Err(status(e)),
|
||||
})
|
||||
})
|
||||
})??;
|
||||
Ok(r.map(|(b, f)| (Body::new(b), Body::new(f))))
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! The body is a 10 mm cube [0, 10]^3 on solidcore; the tools are described as
|
||||
//! [`crate::ssi::Surface`] describes them.
|
||||
|
||||
use super::split::{split_body, split_body_by_sheet, split_faces};
|
||||
use super::{Body, area, counts, make_box, to_arrays, volume};
|
||||
use crate::Error;
|
||||
use crate::export::native;
|
||||
use crate::ssi::Spec;
|
||||
|
||||
const X: [f64; 3] = [1.0, 0.0, 0.0];
|
||||
const Y: [f64; 3] = [0.0, 1.0, 0.0];
|
||||
const Z: [f64; 3] = [0.0, 0.0, 1.0];
|
||||
|
||||
fn cube() -> Body {
|
||||
make_box([0.0; 3], [10.0; 3]).unwrap()
|
||||
}
|
||||
|
||||
/// The plane x = `at` over u (along y) and v (along z) in `[-1, 11]`: it reaches over the cube.
|
||||
fn plane_x(at: f64) -> Spec {
|
||||
Spec::Plane { origin: [at, 0.0, 0.0], xaxis: Y, yaxis: Z, u: [-1.0, 11.0], v: [-1.0, 11.0] }
|
||||
}
|
||||
|
||||
/// The cylinder of radius 3 about the vertical line through (5, 5), from z = -1 to 11.
|
||||
fn cylinder() -> Spec {
|
||||
Spec::Cylinder { origin: [5.0, 5.0, 0.0], xaxis: X, yaxis: Y, radius: 3.0, angle_deg: [0.0, 360.0], height: [-1.0, 11.0] }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Op {
|
||||
/// Split the faces (all six when empty) by the tools.
|
||||
Faces(Vec<usize>, Vec<Spec>),
|
||||
Body(Spec),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Expect {
|
||||
/// Volume, counts, and the total of faces made.
|
||||
Split(f64, [usize; 3], usize),
|
||||
/// The two sides' volumes and counts.
|
||||
Halves(f64, [usize; 3], f64, [usize; 3]),
|
||||
NoCut,
|
||||
}
|
||||
|
||||
fn case(name: &str) -> (Op, Expect) {
|
||||
let all: Vec<usize> = (0..6).collect();
|
||||
match name {
|
||||
// Four faces cross x = 5, each cut in two; the four edges along x are cut at it.
|
||||
"split_faces_by_a_plane" => (Op::Faces(all, vec![plane_x(5.0)]), Expect::Split(1000.0, [10, 20, 12], 4)),
|
||||
// The cylinder meets the top and the bottom in a circle each: a disc and a holed face.
|
||||
"split_faces_by_a_cylinder" => (Op::Faces(all, vec![cylinder()]), Expect::Split(1000.0, [8, 14, 10], 2)),
|
||||
// Two planes in turn, the second cutting the first's pieces.
|
||||
"split_faces_by_two_planes" => (Op::Faces(all, vec![plane_x(3.0), plane_x(7.0)]), Expect::Split(1000.0, [14, 28, 16], 8)),
|
||||
"split_faces_missed_by_the_tool" => (Op::Faces(all, vec![plane_x(20.0)]), Expect::NoCut),
|
||||
"split_body_by_a_plane" => (Op::Body(plane_x(3.0)), Expect::Halves(300.0, [6, 12, 8], 700.0, [6, 12, 8])),
|
||||
// A tool that misses leaves both parts the whole body.
|
||||
"split_body_missed_by_the_tool" => (Op::Body(plane_x(20.0)), Expect::Halves(1000.0, [6, 12, 8], 1000.0, [6, 12, 8])),
|
||||
_ => panic!("no case {name}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Measured {
|
||||
Split { volume: f64, counts: [usize; 3], made: Vec<usize> },
|
||||
Halves { volumes: [f64; 2], counts: [[usize; 3]; 2] },
|
||||
NoCut,
|
||||
}
|
||||
|
||||
type Got = std::result::Result<Measured, Error>;
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs()
|
||||
}
|
||||
|
||||
fn valid(name: &str, b: &Body) {
|
||||
let (rec, _) = native(&to_arrays(b).unwrap()).unwrap_or_else(|e| panic!("{name}: record: {e:?}"));
|
||||
rec.validate().unwrap_or_else(|e| panic!("{name}: record: {e}"));
|
||||
}
|
||||
|
||||
fn judge(name: &str, who: &str, got: Got) -> Got {
|
||||
let (_, expect) = case(name);
|
||||
match (&expect, &got) {
|
||||
(_, Err(g)) => panic!("{name}: {who} failed: {g}"),
|
||||
(Expect::NoCut, Ok(m)) => assert_eq!(*m, Measured::NoCut, "{name}: {who}"),
|
||||
(Expect::Split(v, n, total), Ok(Measured::Split { volume, counts, made })) => {
|
||||
assert!(rel(*volume, *v) < 1e-9, "{name}: {who} volume {volume} vs analytic {v}");
|
||||
assert_eq!(counts, n, "{name}: {who} counts");
|
||||
assert_eq!(made.iter().sum::<usize>(), *total, "{name}: {who} faces made {made:?}");
|
||||
}
|
||||
(Expect::Halves(v0, n0, v1, n1), Ok(Measured::Halves { volumes, counts })) => {
|
||||
assert!(rel(volumes[0], *v0) < 1e-9 && rel(volumes[1], *v1) < 1e-9, "{name}: {who} volumes {volumes:?} vs {v0} {v1}");
|
||||
assert_eq!(counts, &[*n0, *n1], "{name}: {who} counts");
|
||||
}
|
||||
(e, Ok(m)) => panic!("{name}: {who} gave {m:?}, expected {e:?}"),
|
||||
}
|
||||
got
|
||||
}
|
||||
|
||||
fn solidcore_case(name: &str) -> Got {
|
||||
let (op, _) = case(name);
|
||||
let got = match &op {
|
||||
Op::Faces(faces, tools) => {
|
||||
let tools: Vec<&Spec> = tools.iter().collect();
|
||||
split_faces(&cube(), faces, &tools).map(|r| match r {
|
||||
None => Measured::NoCut,
|
||||
Some((b, made)) => {
|
||||
valid(name, &b);
|
||||
Measured::Split { volume: volume(&b, 1e-8).unwrap(), counts: counts(&b).unwrap(), made }
|
||||
}
|
||||
})
|
||||
}
|
||||
Op::Body(tool) => split_body(&cube(), tool).map(|r| match r {
|
||||
None => Measured::NoCut,
|
||||
Some((b, f)) => {
|
||||
valid(name, &b);
|
||||
valid(name, &f);
|
||||
Measured::Halves {
|
||||
volumes: [volume(&b, 1e-8).unwrap(), volume(&f, 1e-8).unwrap()],
|
||||
counts: [counts(&b).unwrap(), counts(&f).unwrap()],
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
judge(name, "solidcore", got)
|
||||
}
|
||||
|
||||
macro_rules! cases {
|
||||
($($m:ident $(: $ign:meta)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
#[test]
|
||||
$(#[$ign])?
|
||||
fn solidcore_matches_the_closed_form() {
|
||||
let _ = super::solidcore_case(stringify!($m));
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
cases!(
|
||||
split_faces_by_a_plane,
|
||||
split_faces_by_a_cylinder,
|
||||
split_faces_by_two_planes,
|
||||
split_faces_missed_by_the_tool,
|
||||
split_body_by_a_plane,
|
||||
split_body_missed_by_the_tool,
|
||||
);
|
||||
|
||||
/// The capi's argument checks, before any kernel call.
|
||||
#[test]
|
||||
fn hostile_arguments_are_refused() {
|
||||
let b = cube();
|
||||
let p = plane_x(5.0);
|
||||
assert_eq!(split_faces(&b, &[], &[&p]).err(), Some(Error::Argument), "no face");
|
||||
assert_eq!(split_faces(&b, &[0], &[]).err(), Some(Error::Argument), "no tool");
|
||||
assert_eq!(split_faces(&b, &[0, 0], &[&p]).err(), Some(Error::Argument), "a face twice");
|
||||
assert_eq!(split_faces(&b, &[6], &[&p]).err(), Some(Error::Argument), "no face 6");
|
||||
assert_eq!(split_faces(&b, &[usize::MAX], &[&p]).err(), Some(Error::Argument), "no face usize::MAX");
|
||||
let many = vec![&p; super::split::MAX_TOOLS + 1];
|
||||
assert_eq!(split_faces(&b, &[0], &many).err(), Some(Error::Argument), "too many tools");
|
||||
let faces: Vec<usize> = (0..=super::split::MAX_FACES).collect();
|
||||
assert_eq!(split_faces(&b, &faces, &[&p]).err(), Some(Error::Argument), "too many faces");
|
||||
}
|
||||
|
||||
/// A sheet is no solid to split in two (`SMLIB_ERR_NOT_SOLID`).
|
||||
#[test]
|
||||
fn a_sheet_is_not_split_into_two_bodies() {
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::primitive::api_curves::create_line_segment;
|
||||
use solidcore::sweep::api_brep::create_linear_sweep;
|
||||
let sheet = super::run("sheet", |k| {
|
||||
let c = Curve::from(create_line_segment(Vec3::new(0.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0))?);
|
||||
create_linear_sweep(k, &[Some(&c)], Vec3::new(0.0, 1.0, 0.0), 10.0, false)
|
||||
})
|
||||
.map(Body::new)
|
||||
.unwrap();
|
||||
assert!(rel(area(&sheet, 1e-8).unwrap(), 100.0) < 1e-9);
|
||||
assert_eq!(split_body(&sheet, &plane_x(5.0)).err(), Some(Error::NotSolid));
|
||||
}
|
||||
|
||||
/// Faces of the box -20..5 x -20..20 x -20..5 copied off it into a sheet (`CopyFaces` with merge,
|
||||
/// as the capi's test): the top z = 5 and, with `side`, the side x = 5 too.
|
||||
fn lid(side: bool) -> Body {
|
||||
use solidcore::EntityRef;
|
||||
use solidcore::geom::tol::USE_DEFAULT;
|
||||
use solidcore::query::api_queries::face_internal_point;
|
||||
use solidcore::topology::brep::p1::faces;
|
||||
use solidcore::topology::brep::p2::ArenaNotifyState;
|
||||
use solidcore::topology::brep::p3::new_brep;
|
||||
use solidcore::topology::brep::p7::copy_faces;
|
||||
let slab = make_box([-20.0; 3], [25.0, 40.0, 25.0]).unwrap();
|
||||
super::run("sheet", |k| {
|
||||
let mut whole = solidcore::topology::brep::p3::brep_copy(k, &slab.0.lock().unwrap())?;
|
||||
let mut picked = Vec::new();
|
||||
for f in faces(&whole, None)? {
|
||||
let p = face_internal_point(k, &mut whole, f)?;
|
||||
if (p.z - 5.0).abs() < 1e-9 || (side && (p.x - 5.0).abs() < 1e-9) {
|
||||
picked.push(f);
|
||||
}
|
||||
}
|
||||
assert_eq!(picked.len(), if side { 2 } else { 1 });
|
||||
let mut w = new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
w.set_tolerance(whole.tolerance());
|
||||
{
|
||||
let mut state = ArenaNotifyState::new(&w);
|
||||
let mut env = state.env(None);
|
||||
let key_of = env.key_of;
|
||||
let this_face = |f| key_of(EntityRef::Face(f));
|
||||
copy_faces(k, &mut whole, &picked, &mut w, true, &this_face, &mut env)?;
|
||||
}
|
||||
w.refresh_cached_topology();
|
||||
Ok(w)
|
||||
})
|
||||
.map(Body::new)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The capi's test of `smlib_split_body_by_sheet` (fork 2fc375f): a cube 0..10 cut by an L of two
|
||||
/// faces: the corner x < 5, z < 5 (250) on one side, the rest (750) on the other, whichever face
|
||||
/// the sides are read from; on C++ too.
|
||||
#[test]
|
||||
fn split_body_by_an_l_sheet() {
|
||||
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");
|
||||
valid("L sheet", &b);
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// What `smlib_split_body_by_sheet` refuses: a body that is no sheet, a face out of range, the
|
||||
/// sheet as its own body; a face that ends inside the cube only nicks it, which is no cut.
|
||||
#[test]
|
||||
fn split_by_a_sheet_refuses() {
|
||||
let (c, sheet) = (cube(), lid(true));
|
||||
assert_eq!(split_body_by_sheet(&c, &make_box([-20.0; 3], [25.0, 40.0, 25.0]).unwrap(), 0).err(), Some(Error::Argument));
|
||||
assert_eq!(split_body_by_sheet(&c, &sheet, 2).err(), Some(Error::Argument));
|
||||
assert_eq!(split_body_by_sheet(&c, &c, 0).err(), Some(Error::Argument));
|
||||
assert!(split_body_by_sheet(&c, &lid(false), 0).unwrap().is_none());
|
||||
assert_eq!(split_body_by_sheet(&sheet, &lid(false), 0).err(), Some(Error::NotSolid));
|
||||
}
|
||||
|
||||
/// The body split is never changed.
|
||||
#[test]
|
||||
fn the_body_stays_as_it_was() {
|
||||
let b = cube();
|
||||
let _ = split_faces(&b, &[0, 1, 2, 3, 4, 5], &[&plane_x(5.0)]).unwrap();
|
||||
let _ = split_body(&b, &plane_x(5.0)).unwrap();
|
||||
assert_eq!(counts(&b).unwrap(), [6, 12, 8]);
|
||||
assert!(rel(volume(&b, 1e-8).unwrap(), 1000.0) < 1e-12);
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// face inside its bounds, so the face's surface is intersected through its face's trimmed
|
||||
/// cache. The square comes out as a face of its own, the sheet holed by it.
|
||||
#[test]
|
||||
fn a_spline_sheet_is_cut_by_strips_inside_it() {
|
||||
use solidcore::geom::tol::USE_DEFAULT;
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::nurbs::b_spline_curve::p1::KnotType;
|
||||
use solidcore::nurbs::b_spline_surface::p1::{BSplineSurface, BSplineSurfaceForm, CanonicalSurfaceData};
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::topology::brep::p3::new_brep;
|
||||
use solidcore::topology::brep::p8::create_face_from_surface;
|
||||
// Control points u-major: the curve's four, each at y = 0 and y = 40.
|
||||
let curve = [[0.0, 0.0], [13.0, -8.0], [27.0, 8.0], [40.0, 0.0]];
|
||||
let pts: Vec<Vec3> = curve.iter().flat_map(|[x, z]| [Vec3::new(*x, 0.0, *z), Vec3::new(*x, 40.0, *z)]).collect();
|
||||
let sheet = super::run("sheet", |k| {
|
||||
let surface = Surface::from(BSplineSurface::create_canonical(&CanonicalSurfaceData {
|
||||
u_degree: 3,
|
||||
v_degree: 1,
|
||||
control_points: &pts,
|
||||
form: BSplineSurfaceForm::Unspecified,
|
||||
u_multiplicities: &[4, 4],
|
||||
v_multiplicities: &[2, 2],
|
||||
u_knots: &[0.0, 1.0],
|
||||
v_knots: &[0.0, 40.0],
|
||||
knot_type: KnotType::Unspecified,
|
||||
weights: None,
|
||||
uv_domain: None,
|
||||
})?);
|
||||
let mut b = new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
let natural = surface.natural_uv_domain_with_nurb(k)?;
|
||||
super::sweep::build_faces(&mut b, |s, fenv| create_face_from_surface(k, s, Some(surface), &natural, fenv))?;
|
||||
b.refresh_cached_topology();
|
||||
Ok(b)
|
||||
})
|
||||
.map(Body::new)
|
||||
.unwrap();
|
||||
assert_eq!(counts(&sheet).unwrap(), [1, 4, 4]);
|
||||
let whole = area(&sheet, 1e-8).unwrap();
|
||||
// The strip standing on the line from `o` along `d` (16 long), reaching past the sheet in z.
|
||||
let strip = |o: [f64; 3], d: [f64; 3]| Spec::Plane { origin: o, xaxis: Z, yaxis: d, u: [-60.0, 60.0], v: [0.0, 16.0] };
|
||||
let tools = [
|
||||
strip([12.0, 12.0, 0.0], X),
|
||||
strip([28.0, 12.0, 0.0], Y),
|
||||
strip([28.0, 28.0, 0.0], [-1.0, 0.0, 0.0]),
|
||||
strip([12.0, 28.0, 0.0], [0.0, -1.0, 0.0]),
|
||||
];
|
||||
let tools: Vec<&Spec> = tools.iter().collect();
|
||||
let (cut, made) = split_faces(&sheet, &[0], &tools).unwrap().expect("the strips cut the sheet");
|
||||
valid("spline sheet", &cut);
|
||||
// The square's face and the holed sheet: four edges and vertices round each.
|
||||
assert_eq!(counts(&cut).unwrap(), [2, 8, 8]);
|
||||
assert_eq!(made, [1]);
|
||||
// Cutting moves no surface: the pieces cover the sheet.
|
||||
assert!(rel(area(&cut, 1e-8).unwrap(), whole) < 1e-9);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Surface/surface intersection on solidcore (`smlib_ssi.cpp`'s `smlib_ssi`, `smlib_surface_domain`,
|
||||
//! `smlib_surface_evaluate`), on the surfaces [`crate::core::split`] makes from a [`Spec`]:
|
||||
//! `SmSurface::GlobalSurfaceIntersect` and `SmAdvSurfaceIntersector`, whose results are read as the
|
||||
//! capi reads them.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::geom::vector2d::Point2;
|
||||
use solidcore::intersect::surface_intersector::p5::do_intersection;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::nurbs::b_spline_curve::BSplineCurve;
|
||||
use solidcore::surface::surface::p2::{SurfaceIntersection, TsectCurveType};
|
||||
|
||||
use super::convert::bspline_curve_part;
|
||||
use super::run;
|
||||
use super::split::{Tool, tool};
|
||||
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`).
|
||||
pub(crate) fn surface(spec: &Spec) -> Result<Tool> {
|
||||
run("smlib_surface", |k| tool(k, spec))?
|
||||
}
|
||||
|
||||
/// `[u0, u1, v0, v1]`.
|
||||
pub(crate) fn domain(s: &Tool) -> [f64; 4] {
|
||||
let (lo, hi) = (s.domain.min(), s.domain.max());
|
||||
[lo.x, hi.x, lo.y, hi.y]
|
||||
}
|
||||
|
||||
/// The surface's point at (u, v): `smlib_surface_evaluate`, a non-finite point being `SM_ERR`.
|
||||
pub(crate) fn evaluate(s: &Tool, u: f64, v: f64) -> Result<[f64; 3]> {
|
||||
if !u.is_finite() || !v.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let p = run("smlib_surface_evaluate", |k| s.surface.evaluate_point(k, Point2::new(u, v)))?;
|
||||
let r = [p.x, p.y, p.z];
|
||||
if r.iter().all(|x| x.is_finite()) { Ok(r) } else { Err(Error::Solidcore("smlib_surface_evaluate: not a finite point".into())) }
|
||||
}
|
||||
|
||||
fn curve_type(t: TsectCurveType) -> CurveType {
|
||||
match t {
|
||||
TsectCurveType::Unknown => CurveType::Unknown,
|
||||
TsectCurveType::Touching => CurveType::Touching,
|
||||
TsectCurveType::Crossing => CurveType::Crossing,
|
||||
TsectCurveType::Tangent => CurveType::Tangent,
|
||||
TsectCurveType::Coincident => CurveType::Coincident,
|
||||
TsectCurveType::NearTangent => CurveType::NearTangent,
|
||||
TsectCurveType::RegionBoundary => CurveType::RegionBoundary,
|
||||
}
|
||||
}
|
||||
|
||||
/// The capi's `nurbs`: `c` as its B-spline, control points made cartesian in `dim` coordinates;
|
||||
/// `None` for a curve that is not one, which is never approximated here.
|
||||
fn nurbs(k: &Kernel, c: &Curve, dim: usize) -> Result<Option<Nurbs>> {
|
||||
let Some(part): Option<Cow<'_, BSplineCurve>> = bspline_curve_part(k, c)? else { return Ok(None) };
|
||||
let Some(n) = part.gw_nurb() else { return Ok(None) };
|
||||
let mut out = Nurbs { points: Vec::with_capacity(n.cpts().len() * dim), weights: Vec::with_capacity(n.cpts().len()), knots: n.knots().to_vec() };
|
||||
for p in n.cpts() {
|
||||
let w = if p.w != solidcore::nurbs::math::NOW { p.w } else { 1.0 };
|
||||
let q = [p.x / w, p.y / w, p.z / w];
|
||||
out.points.extend_from_slice(q.get(..dim).unwrap_or(&q));
|
||||
out.weights.push(w);
|
||||
}
|
||||
Ok(Some(out))
|
||||
}
|
||||
|
||||
/// The capi's `start`: where `c` starts, NaN where there is no curve or it can't be evaluated.
|
||||
fn start(k: &Kernel, c: Option<&Curve>) -> [f64; 3] {
|
||||
c.and_then(|c| c.evaluate_point(k, c.natural_interval().min()).ok()).map_or([f64::NAN; 3], |p| [p.x, p.y, p.z])
|
||||
}
|
||||
|
||||
fn read(k: &Kernel, r: &SurfaceIntersection) -> Result<SsiResult> {
|
||||
let mut out = SsiResult::default();
|
||||
for (i, c3) in r.curves_3d.iter().enumerate() {
|
||||
let (ua, ub) = (r.surface1_uv_curves.get(i).and_then(Option::as_ref), r.surface2_uv_curves.get(i).and_then(Option::as_ref));
|
||||
let ty = r.curve_types.get(i).copied();
|
||||
let ivl = c3.natural_interval();
|
||||
let uv = |c: Option<&Curve>| -> Result<_> { Ok(c.map(|c| nurbs(k, c, 2)).transpose()?.flatten().map(curve2)) };
|
||||
out.curves.push(SsiCurve {
|
||||
curve_type: ty.map_or(CurveType::None, curve_type),
|
||||
deviation: r.deviations.get(i).copied().unwrap_or(f64::NAN),
|
||||
interval: [ivl.min(), ivl.max()],
|
||||
// A curve that is no B-spline is the one thing the result can't hold.
|
||||
curve: curve3(nurbs(k, c3, 3)?.ok_or(Error::Unsupported)?),
|
||||
uv_a: uv(ua)?,
|
||||
uv_b: uv(ub)?,
|
||||
});
|
||||
if ty == Some(TsectCurveType::Touching) {
|
||||
let (a, b) = (start(k, ua), start(k, ub));
|
||||
out.points.push(SsiPoint { xyz: start(k, Some(c3)), uv_a: [a[0], a[1]], uv_b: [b[0], b[1]] });
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `smlib_ssi`: `a` and `b` within their domains, at approximation tolerance `approx_tol` (<= 0:
|
||||
/// the kernel's default).
|
||||
pub(crate) fn intersect(a: &Tool, b: &Tool, method: Method, approx_tol: f64) -> Result<SsiResult> {
|
||||
if !approx_tol.is_finite() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let tol = (approx_tol > 0.0).then_some(approx_tol);
|
||||
let edges = [true, true];
|
||||
run("smlib_ssi", |k| {
|
||||
let found = match method {
|
||||
Method::Global => a.surface.global_surface_intersect(k, &a.domain, &b.surface, &b.domain, edges, tol, None),
|
||||
Method::Advanced => do_intersection(k, &a.surface, &a.domain, &b.surface, &b.domain, edges, tol, None),
|
||||
};
|
||||
// An SMLib status is an answer, as the C API returns it.
|
||||
match found {
|
||||
Ok(found) => Ok(read(k, &found)),
|
||||
Err(e) => match e.sm_status() {
|
||||
Some(status) => Ok(Err(Error::Status(status))),
|
||||
None => Err(e),
|
||||
},
|
||||
}
|
||||
})?
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,613 @@
|
||||
//! Extrusions, revolutions, sweeps, lofts and planar faces on solidcore, against closed forms:
|
||||
//! every result exports to a record that validates.
|
||||
//!
|
||||
//! The profiles are built straight into solidcore curves from the curve lists.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::sweep::{extrude, face_section, loft, loft_to_point, planar_faces, project_curves, revolve, screw_sweep, sweep_placed, sweep_stations};
|
||||
use super::{Body, area, bounds, counts, make_box, make_cylinder, to_arrays, volume};
|
||||
use crate::Error;
|
||||
use crate::export::native;
|
||||
use crate::sweeps::{Affine, CornerRefusal, Curve, DraftCorner, Ends, SweepFrame};
|
||||
|
||||
/// A case longer than this is a hang.
|
||||
const TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
const Z: [f64; 3] = [0.0, 0.0, 1.0];
|
||||
|
||||
fn line(a: [f64; 3], b: [f64; 3]) -> Curve {
|
||||
Curve::Line { start: a, end: b }
|
||||
}
|
||||
|
||||
/// The closed polygon through `pts`.
|
||||
fn polygon(pts: &[[f64; 3]]) -> Vec<Curve> {
|
||||
(0..pts.len()).map(|i| line(pts[i], pts[(i + 1) % pts.len()])).collect()
|
||||
}
|
||||
|
||||
/// The square of side `s` from `(x, y)` in the plane z = `z`.
|
||||
fn square(x: f64, y: f64, s: f64, z: f64) -> Vec<Curve> {
|
||||
polygon(&[[x, y, z], [x + s, y, z], [x + s, y + s, z], [x, y + s, z]])
|
||||
}
|
||||
|
||||
/// The circle of `radius` about `axis` through `center`, from `reference`.
|
||||
fn circle(center: [f64; 3], axis: [f64; 3], reference: [f64; 3], radius: f64) -> Curve {
|
||||
Curve::Arc { center, axis, reference, radius, start: 0.0, end: TAU }
|
||||
}
|
||||
|
||||
/// An arc of radius 5 about the z axis from `reference`, from `start` to `end`.
|
||||
fn arc5(reference: [f64; 3], start: f64, end: f64) -> Curve {
|
||||
Curve::Arc { center: [0.0; 3], axis: Z, reference, radius: 5.0, start, end }
|
||||
}
|
||||
|
||||
/// The circle of radius 5 about the z axis as two half arcs in one frame.
|
||||
fn halves() -> Vec<Curve> {
|
||||
vec![arc5([1.0, 0.0, 0.0], 0.0, PI), arc5([1.0, 0.0, 0.0], PI, TAU)]
|
||||
}
|
||||
|
||||
/// The circle of radius 5 about the z axis as a quarter from +x and three quarters from +y.
|
||||
fn two_frames() -> Vec<Curve> {
|
||||
vec![arc5([1.0, 0.0, 0.0], 0.0, FRAC_PI_2), arc5([0.0, 1.0, 0.0], 0.0, 3.0 * FRAC_PI_2)]
|
||||
}
|
||||
|
||||
/// The upper half disc of radius 5 about the z axis, its arc in two quarters.
|
||||
fn half_disc() -> Vec<Curve> {
|
||||
vec![arc5([1.0, 0.0, 0.0], 0.0, FRAC_PI_2), arc5([1.0, 0.0, 0.0], FRAC_PI_2, PI), line([-5.0, 0.0, 0.0], [5.0, 0.0, 0.0])]
|
||||
}
|
||||
|
||||
/// The rectangle from x = 5 to 8 and z = 0 to 4 in the plane y = 0: area 12, centroid at x 6.5.
|
||||
fn ring_section() -> Vec<Curve> {
|
||||
polygon(&[[5.0, 0.0, 0.0], [8.0, 0.0, 0.0], [8.0, 0.0, 4.0], [5.0, 0.0, 4.0]])
|
||||
}
|
||||
|
||||
fn translate(t: [f64; 3]) -> Affine {
|
||||
Affine { m: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], t }
|
||||
}
|
||||
|
||||
fn scale_up(s: f64, z: f64) -> Affine {
|
||||
Affine { m: [[s, 0.0, 0.0], [0.0, s, 0.0], [0.0, 0.0, 1.0]], t: [0.0, 0.0, z] }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Op {
|
||||
PlanarFaces(Vec<Curve>),
|
||||
Extrude(Vec<Curve>, [f64; 3], f64, f64, DraftCorner, bool),
|
||||
Revolve(Vec<Curve>, [f64; 3], [f64; 3], f64, bool),
|
||||
Sweep(Vec<Curve>, Vec<Curve>, SweepFrame, bool),
|
||||
Loft(Vec<Vec<Curve>>, u32, bool),
|
||||
LoftToPoint(Vec<Curve>, [f64; 3]),
|
||||
Stations(Vec<Curve>, Vec<Affine>, Ends),
|
||||
Screw(Vec<Curve>, [f64; 3], [f64; 3], f64, f64, f64),
|
||||
/// A screw sweep with a radial law.
|
||||
ScrewRadial(Vec<Curve>, [f64; 3], [f64; 3], f64, f64, Vec<(f64, f64)>, f64),
|
||||
}
|
||||
|
||||
/// What a case must give.
|
||||
#[derive(Clone, Debug)]
|
||||
enum Expect {
|
||||
/// A solid of this volume (relative 1e-9).
|
||||
Volume(f64),
|
||||
/// A solid of this volume within this relative error (walls approximated within a tolerance).
|
||||
About(f64, f64),
|
||||
/// A sheet of this area (relative 1e-9).
|
||||
Sheet(f64),
|
||||
/// A drafted corner refused for this reason, wherever.
|
||||
Corner(CornerRefusal),
|
||||
}
|
||||
|
||||
struct Case {
|
||||
op: Op,
|
||||
expect: Expect,
|
||||
}
|
||||
|
||||
/// The frustum of a pyramid or cone: h/3 (A1 + A2 + sqrt(A1 A2)).
|
||||
fn frustum(a1: f64, a2: f64, h: f64) -> f64 {
|
||||
h / 3.0 * (a1 + a2 + (a1 * a2).sqrt())
|
||||
}
|
||||
|
||||
fn case(name: &str) -> Case {
|
||||
let c = |op, expect| Case { op, expect };
|
||||
let rect = polygon(&[[0.0, 0.0, 0.0], [30.0, 0.0, 0.0], [30.0, 20.0, 0.0], [0.0, 20.0, 0.0]]);
|
||||
let theta = 5f64.to_radians();
|
||||
let disc = || vec![circle([0.0; 3], Z, [1.0, 0.0, 0.0], 5.0)];
|
||||
match name {
|
||||
// A square of 10 with a hole of radius 2: 100 - 4π.
|
||||
"planar_faces_with_a_hole" => {
|
||||
let mut loops = square(0.0, 0.0, 10.0, 0.0);
|
||||
loops.push(circle([5.0, 5.0, 0.0], Z, [1.0, 0.0, 0.0], 2.0));
|
||||
c(Op::PlanarFaces(loops), Expect::Sheet(100.0 - 4.0 * PI))
|
||||
}
|
||||
// V = A h.
|
||||
"extrude_rectangle" => c(Op::Extrude(rect, Z, 10.0, 0.0, DraftCorner::default(), true), Expect::Volume(6000.0)),
|
||||
// V = π r² h.
|
||||
"extrude_circle" => c(Op::Extrude(disc(), Z, 10.0, 0.0, DraftCorner::default(), true), Expect::Volume(250.0 * PI)),
|
||||
// A circle drawn in two halves (split where another sketch curve ends on it), the
|
||||
// pieces in the circle's own frame: one wall, as the whole circle sweeps (smlib 7dee48f).
|
||||
"extrude_circle_in_two_halves" => c(Op::Extrude(halves(), Z, 10.0, 0.0, DraftCorner::default(), true), Expect::Volume(250.0 * PI)),
|
||||
// The same circle as a quarter and three quarters each in its own frame: one wall still,
|
||||
// its seam in the middle of the longer piece.
|
||||
"extrude_circle_in_two_frames" => c(Op::Extrude(two_frames(), Z, 10.0, 0.0, DraftCorner::default(), true), Expect::Volume(250.0 * PI)),
|
||||
// A half disc whose arc is drawn in two quarters, closed by its diameter: the quarters
|
||||
// sweep one wall. V = π r² h / 2.
|
||||
"extrude_half_disc_split_arc" => c(Op::Extrude(half_disc(), Z, 10.0, 0.0, DraftCorner::default(), true), Expect::Volume(125.0 * PI)),
|
||||
// Leaning in by θ, the walls' offsets overlap at the corners and are trimmed where they
|
||||
// cross, under every rule: the frustum from side 20 to 20 - 2 h tanθ.
|
||||
"extrude_square_drafted_round" => {
|
||||
let top = 20.0 - 2.0 * 10.0 * theta.tan();
|
||||
c(Op::Extrude(square(0.0, 0.0, 20.0, 0.0), Z, 10.0, theta, DraftCorner::Round, true), Expect::Volume(frustum(400.0, top * top, 10.0)))
|
||||
}
|
||||
"extrude_square_drafted_curvature" => {
|
||||
let top = 20.0 - 2.0 * 10.0 * theta.tan();
|
||||
c(Op::Extrude(square(0.0, 0.0, 20.0, 0.0), Z, 10.0, theta, DraftCorner::Curvature, true), Expect::Volume(frustum(400.0, top * top, 10.0)))
|
||||
}
|
||||
// No corner: the cone frustum from radius 5 to 5 - h tanθ.
|
||||
"extrude_circle_drafted" => {
|
||||
let r2 = 5.0 - 10.0 * theta.tan();
|
||||
c(Op::Extrude(disc(), Z, 10.0, theta, DraftCorner::default(), true), Expect::Volume(frustum(PI * 25.0, PI * r2 * r2, 10.0)))
|
||||
}
|
||||
// A strip 2 wide leaning in 10° over 10: the long walls meet across the short ones
|
||||
// (10 tan 10° > 1) before the far end.
|
||||
"extrude_draft_wall_closes" => c(
|
||||
Op::Extrude(
|
||||
polygon(&[[0.0, 0.0, 0.0], [20.0, 0.0, 0.0], [20.0, 2.0, 0.0], [0.0, 2.0, 0.0]]),
|
||||
Z,
|
||||
10.0,
|
||||
10f64.to_radians(),
|
||||
DraftCorner::Curvature,
|
||||
true,
|
||||
),
|
||||
Expect::Corner(CornerRefusal::WallCloses),
|
||||
),
|
||||
// Pappus: A · θ x̄ = 12 · θ · 6.5.
|
||||
"revolve_rectangle_full" => c(Op::Revolve(ring_section(), [0.0; 3], Z, TAU, true), Expect::Volume(12.0 * TAU * 6.5)),
|
||||
"revolve_rectangle_quarter" => c(Op::Revolve(ring_section(), [0.0; 3], Z, FRAC_PI_2, true), Expect::Volume(12.0 * FRAC_PI_2 * 6.5)),
|
||||
// π r² L, along two lines end to end.
|
||||
"sweep_circle_straight" => c(
|
||||
Op::Sweep(
|
||||
vec![circle([0.0; 3], Z, [1.0, 0.0, 0.0], 2.0)],
|
||||
vec![line([0.0; 3], [0.0, 0.0, 6.0]), line([0.0, 0.0, 6.0], [0.0, 0.0, 10.0])],
|
||||
SweepFrame::Follow,
|
||||
true,
|
||||
),
|
||||
Expect::Volume(PI * 4.0 * 10.0),
|
||||
),
|
||||
// A quarter of a torus: π r² · R θ.
|
||||
"sweep_circle_along_an_arc" => c(
|
||||
Op::Sweep(
|
||||
vec![circle([10.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], 1.0)],
|
||||
vec![Curve::Arc { center: [0.0; 3], axis: Z, reference: [1.0, 0.0, 0.0], radius: 10.0, start: 0.0, end: FRAC_PI_2 }],
|
||||
SweepFrame::Follow,
|
||||
true,
|
||||
),
|
||||
Expect::Volume(PI * 10.0 * FRAC_PI_2),
|
||||
),
|
||||
// A bent path (a line, then a quarter arc) needs the curve sweep: π r² times the path's
|
||||
// length, 10 + 10 π/2, within the walls' fit.
|
||||
"sweep_along_a_bent_path" => c(
|
||||
Op::Sweep(
|
||||
vec![circle([0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], 1.0)],
|
||||
vec![
|
||||
line([0.0; 3], [10.0, 0.0, 0.0]),
|
||||
Curve::Arc { center: [10.0, 10.0, 0.0], axis: Z, reference: [0.0, -1.0, 0.0], radius: 10.0, start: 0.0, end: FRAC_PI_2 },
|
||||
],
|
||||
SweepFrame::Follow,
|
||||
true,
|
||||
),
|
||||
Expect::About(PI * (10.0 + 10.0 * FRAC_PI_2), 1e-4),
|
||||
),
|
||||
// A 4 x 3 rectangle round a closed 60 x 40 rounded rectangle (corner radius 8), from the
|
||||
// middle of its bottom line: no caps, the walls closing on each other. Pappus: A L, with
|
||||
// A = 12 and L = 2 (44 + 24) + 4 (π/2) 8.
|
||||
"sweep_round_a_closed_path" => {
|
||||
let arc = |c: [f64; 3], r: [f64; 3]| Curve::Arc { center: c, axis: Z, reference: r, radius: 8.0, start: 0.0, end: FRAC_PI_2 };
|
||||
let path = vec![
|
||||
line([0.0, -20.0, 0.0], [22.0, -20.0, 0.0]),
|
||||
arc([22.0, -12.0, 0.0], [0.0, -1.0, 0.0]),
|
||||
line([30.0, -12.0, 0.0], [30.0, 12.0, 0.0]),
|
||||
arc([22.0, 12.0, 0.0], [1.0, 0.0, 0.0]),
|
||||
line([22.0, 20.0, 0.0], [-22.0, 20.0, 0.0]),
|
||||
arc([-22.0, 12.0, 0.0], [0.0, 1.0, 0.0]),
|
||||
line([-30.0, 12.0, 0.0], [-30.0, -12.0, 0.0]),
|
||||
arc([-22.0, -12.0, 0.0], [-1.0, 0.0, 0.0]),
|
||||
line([-22.0, -20.0, 0.0], [0.0, -20.0, 0.0]),
|
||||
];
|
||||
c(
|
||||
Op::Sweep(polygon(&[[0.0, -22.0, -1.5], [0.0, -18.0, -1.5], [0.0, -18.0, 1.5], [0.0, -22.0, 1.5]]), path, SweepFrame::Follow, true),
|
||||
Expect::About(12.0 * (2.0 * (44.0 + 24.0) + 16.0 * PI), 1e-4),
|
||||
)
|
||||
}
|
||||
// The frustum of a square pyramid: side 20 to 10 over 10.
|
||||
"loft_two_squares" => {
|
||||
c(Op::Loft(vec![square(0.0, 0.0, 20.0, 0.0), square(5.0, 5.0, 10.0, 10.0)], 1, true), Expect::Volume(frustum(400.0, 100.0, 10.0)))
|
||||
}
|
||||
// The cone frustum: radius 5 to 3 over 10.
|
||||
"loft_two_circles" => c(
|
||||
Op::Loft(vec![disc(), vec![circle([0.0, 0.0, 10.0], Z, [1.0, 0.0, 0.0], 3.0)]], 1, true),
|
||||
Expect::Volume(frustum(25.0 * PI, 9.0 * PI, 10.0)),
|
||||
),
|
||||
// A h / 3.
|
||||
"loft_to_point_pyramid" => c(Op::LoftToPoint(square(0.0, 0.0, 10.0, 0.0), [5.0, 5.0, 6.0]), Expect::Volume(200.0)),
|
||||
// A square of 2 carried up 10: 40.
|
||||
"sweep_stations_prism" => c(
|
||||
Op::Stations(
|
||||
square(-1.0, -1.0, 2.0, 0.0),
|
||||
vec![translate([0.0; 3]), translate([0.0, 0.0, 5.0]), translate([0.0, 0.0, 10.0])],
|
||||
Ends::Capped,
|
||||
),
|
||||
Expect::Volume(40.0),
|
||||
),
|
||||
// Scaled by s = 1 + z/10 on the way: ∫ (2 s)² dz = 4 · 10 · (2³ - 1) / 3.
|
||||
"sweep_stations_scaled" => c(
|
||||
Op::Stations(square(-1.0, -1.0, 2.0, 0.0), vec![scale_up(1.0, 0.0), scale_up(1.5, 5.0), scale_up(2.0, 10.0)], Ends::Capped),
|
||||
Expect::Volume(4.0 * 10.0 * 7.0 / 3.0),
|
||||
),
|
||||
// A square thread 1 x 1 from radius 5 to 6, pitch 2, two turns: Pappus along the screw
|
||||
// motion (its rise lies in the profile's plane), A · 2π x̄ · turns, the walls within the
|
||||
// tolerance of the screw surfaces.
|
||||
"screw_sweep_square_thread" => c(
|
||||
Op::Screw(polygon(&[[5.0, 0.0, 0.0], [6.0, 0.0, 0.0], [6.0, 0.0, 1.0], [5.0, 0.0, 1.0]]), [0.0; 3], Z, 2.0, 2.0, 1e-5),
|
||||
Expect::About(TAU * 5.5 * 2.0, 1e-5),
|
||||
),
|
||||
// The same thread moved out by 0 to 1 over its two turns, linearly: Pappus with the
|
||||
// centroid at 5.5 + t/2, A · 2π · turns · (5.5 + 1/2).
|
||||
"screw_sweep_radial_taper" => c(
|
||||
Op::ScrewRadial(
|
||||
polygon(&[[5.0, 0.0, 0.0], [6.0, 0.0, 0.0], [6.0, 0.0, 1.0], [5.0, 0.0, 1.0]]),
|
||||
[0.0; 3],
|
||||
Z,
|
||||
2.0,
|
||||
2.0,
|
||||
vec![(0.0, 0.0), (2.0, 1.0)],
|
||||
1e-5,
|
||||
),
|
||||
Expect::About(TAU * 2.0 * 6.0, 1e-5),
|
||||
),
|
||||
_ => panic!("{name}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Measured {
|
||||
/// The volume of a solid, the area of a sheet.
|
||||
size: f64,
|
||||
counts: [usize; 3],
|
||||
kinds: BTreeMap<String, usize>,
|
||||
}
|
||||
|
||||
fn kinds(rec: &solidcraft_geom::brep::Brep) -> BTreeMap<String, usize> {
|
||||
let mut out = BTreeMap::new();
|
||||
for f in &rec.faces {
|
||||
let v = serde_json::to_value(&f.surface).unwrap();
|
||||
*out.entry(v["kind"].as_str().unwrap_or("?").to_owned()).or_default() += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn on_core(op: &Op) -> crate::Result<Body> {
|
||||
match op {
|
||||
Op::PlanarFaces(l) => planar_faces(l),
|
||||
Op::Extrude(p, d, dist, draft, corner, cap) => extrude(p, *d, *dist, *draft, *corner, *cap),
|
||||
Op::Revolve(p, o, a, angle, cap) => revolve(p, *o, *a, *angle, *cap),
|
||||
Op::Sweep(p, path, frame, cap) => sweep_placed(p, path, *frame, *cap).map(|(b, _)| b),
|
||||
Op::Loft(s, d, cap) => loft(s, *d, *cap),
|
||||
Op::LoftToPoint(s, apex) => loft_to_point(s, *apex),
|
||||
Op::Stations(p, s, e) => sweep_stations(p, s, *e),
|
||||
Op::Screw(p, o, a, pitch, turns, tol) => screw_sweep(p, *o, *a, *pitch, *turns, None, *tol),
|
||||
Op::ScrewRadial(p, o, a, pitch, turns, law, tol) => screw_sweep(p, *o, *a, *pitch, *turns, Some(law), *tol),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `f` on its own thread: a hang fails the test instead of stopping the run.
|
||||
fn timed<T: Send + 'static>(name: &str, f: impl FnOnce() -> T + Send + 'static) -> T {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let worker = std::thread::spawn(move || {
|
||||
let _ = tx.send(f());
|
||||
});
|
||||
match rx.recv_timeout(TIMEOUT) {
|
||||
Ok(v) => v,
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => match worker.join() {
|
||||
Err(p) => std::panic::resume_unwind(p),
|
||||
Ok(()) => panic!("{name}: no result"),
|
||||
},
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => panic!("{name}: solidcore hung (no result after {TIMEOUT:?})"),
|
||||
}
|
||||
}
|
||||
|
||||
type Got = std::result::Result<Measured, Error>;
|
||||
|
||||
/// Checks a kernel's result against the case's closed form; what it measured, or its error.
|
||||
fn judge(name: &str, who: &str, expect: &Expect, got: Got) -> Got {
|
||||
match (expect, &got) {
|
||||
(Expect::Corner(why), Err(g)) => {
|
||||
assert!(
|
||||
matches!(g, Error::Corner { why: w, at } if w == why && at.iter().all(|x| x.is_finite())),
|
||||
"{name}: {who} failed otherwise: {g:?}"
|
||||
)
|
||||
}
|
||||
(Expect::Corner(_), Ok(m)) => panic!("{name}: {who} gave {m:?}, expected {expect:?}"),
|
||||
(_, Err(g)) => panic!("{name}: {who} failed: {g}"),
|
||||
(Expect::Volume(v) | Expect::Sheet(v), Ok(m)) => assert!(rel(m.size, *v) < 1e-9, "{name}: {who} {} vs analytic {v}", m.size),
|
||||
(Expect::About(v, within), Ok(m)) => assert!(rel(m.size, *v) < *within, "{name}: {who} {} vs analytic {v}", m.size),
|
||||
}
|
||||
got
|
||||
}
|
||||
|
||||
fn solidcore_case(name: &'static str) -> Got {
|
||||
timed(name, move || {
|
||||
let c = case(name);
|
||||
let sheet = matches!(c.expect, Expect::Sheet(_));
|
||||
let got = on_core(&c.op).map(|body| {
|
||||
let size = if sheet { area(&body, 1e-8) } else { volume(&body, 1e-8) }.unwrap_or_else(|e| panic!("{name}: size: {e}"));
|
||||
let n = counts(&body).unwrap();
|
||||
let (rec, _) = native(&to_arrays(&body).unwrap()).unwrap_or_else(|e| panic!("{name}: record: {e:?}"));
|
||||
rec.validate().unwrap_or_else(|e| panic!("{name}: record: {e}"));
|
||||
bounds(&body).unwrap();
|
||||
Measured { size, counts: n, kinds: kinds(&rec) }
|
||||
});
|
||||
judge(name, "solidcore", &c.expect, got)
|
||||
})
|
||||
}
|
||||
|
||||
macro_rules! cases {
|
||||
($($m:ident $([$pign:meta])? $(: $ign:meta)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
#[test]
|
||||
$(#[$ign])?
|
||||
fn solidcore_matches_the_closed_form() {
|
||||
let _ = super::solidcore_case(stringify!($m));
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
// `: ignore = …` cases fail on solidcore.
|
||||
cases!(
|
||||
planar_faces_with_a_hole,
|
||||
extrude_rectangle,
|
||||
extrude_circle,
|
||||
extrude_circle_in_two_halves,
|
||||
extrude_circle_in_two_frames,
|
||||
extrude_half_disc_split_arc,
|
||||
extrude_square_drafted_round,
|
||||
extrude_square_drafted_curvature,
|
||||
extrude_circle_drafted,
|
||||
extrude_draft_wall_closes,
|
||||
revolve_rectangle_full,
|
||||
revolve_rectangle_quarter,
|
||||
sweep_circle_straight,
|
||||
sweep_circle_along_an_arc,
|
||||
sweep_along_a_bent_path,
|
||||
sweep_round_a_closed_path,
|
||||
loft_two_squares,
|
||||
loft_two_circles,
|
||||
loft_to_point_pyramid,
|
||||
sweep_stations_prism,
|
||||
sweep_stations_scaled,
|
||||
screw_sweep_square_thread,
|
||||
screw_sweep_radial_taper,
|
||||
);
|
||||
|
||||
/// Arcs of a profile on one circle sweep one wall face (tangency-labs/smlib 7dee48f): a circle
|
||||
/// 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.
|
||||
#[test]
|
||||
fn arcs_on_one_circle_sweep_one_wall() {
|
||||
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 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>();
|
||||
for (name, want_counts, want_area, cylinder_on_core) in cases {
|
||||
let (counts_got, kinds_got, area_got) = timed(name, move || {
|
||||
let body = on_core(&case(name).op).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
let (rec, _) = native(&to_arrays(&body).unwrap()).unwrap();
|
||||
(counts(&body).unwrap(), kinds(&rec), area(&body, 1e-8).unwrap())
|
||||
});
|
||||
assert_eq!(counts_got, want_counts, "{name}: solidcore faces, edges, vertices");
|
||||
assert_eq!(walls(&kinds_got), 1, "{name}: solidcore curved walls {kinds_got:?}");
|
||||
if cylinder_on_core {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
fn near(a: [f64; 3], b: [f64; 3]) -> bool {
|
||||
a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-9)
|
||||
}
|
||||
|
||||
fn same_motion(a: &Affine, b: &Affine) -> bool {
|
||||
a.m.iter().zip(&b.m).all(|(x, y)| near(*x, *y)) && near(a.t, b.t)
|
||||
}
|
||||
|
||||
/// The motions of a quarter turn about the z axis: by 0, π/4 and π/2.
|
||||
fn quarter_turn() -> [Affine; 3] {
|
||||
[0.0, FRAC_PI_2 / 2.0, FRAC_PI_2].map(|a: f64| {
|
||||
let (c, s) = (a.cos(), a.sin());
|
||||
Affine { m: [[c, s, 0.0], [-s, c, 0.0], [0.0, 0.0, 1.0]], t: [0.0; 3] }
|
||||
})
|
||||
}
|
||||
|
||||
fn straight_up() -> (Vec<Curve>, Vec<Curve>) {
|
||||
(vec![circle([0.0; 3], Z, [1.0, 0.0, 0.0], 2.0)], vec![line([0.0; 3], [0.0, 0.0, 10.0])])
|
||||
}
|
||||
|
||||
fn round_z() -> (Vec<Curve>, Vec<Curve>) {
|
||||
(
|
||||
vec![circle([10.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], 1.0)],
|
||||
vec![Curve::Arc { center: [0.0; 3], axis: Z, reference: [1.0, 0.0, 0.0], radius: 10.0, start: 0.0, end: FRAC_PI_2 }],
|
||||
)
|
||||
}
|
||||
|
||||
fn check_motions(placed: &[Affine; 3], want: &[Affine; 3]) {
|
||||
assert!(placed.iter().zip(want).all(|(a, b)| same_motion(a, b)), "{placed:?}, expected {want:?}");
|
||||
}
|
||||
|
||||
/// `sweep_placed`: along a straight path the profile only moves, by none, half and all of the
|
||||
/// path; along an arc about z it turns with it about the arc's centre, by none, half and all of
|
||||
/// the arc's angle.
|
||||
#[test]
|
||||
fn sweep_placed_reports_the_motions() {
|
||||
let (placed, turned) = timed("placed", || {
|
||||
let (p, up) = straight_up();
|
||||
let (q, arc) = round_z();
|
||||
(sweep_placed(&p, &up, SweepFrame::Follow, true).unwrap().1, sweep_placed(&q, &arc, SweepFrame::Follow, true).unwrap().1)
|
||||
});
|
||||
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]
|
||||
fn project_a_line_onto_a_face() {
|
||||
let got = timed("project", || {
|
||||
let b = make_box([0.0; 3], [10.0; 3]).unwrap();
|
||||
project_curves(&b, [5.0, 5.0, 10.0], 0.01, &[line([1.0, 1.0, 20.0], [9.0, 9.0, 20.0])], [0.0, 0.0, -1.0], 1e-6)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(got.len(), 1, "{got:?}");
|
||||
let Some((Curve::Nurbs { points, .. }, _)) = got.first() else { panic!("{got:?}") };
|
||||
let (a, b) = (points[0], points[points.len() - 1]);
|
||||
let ends = [[a[0], a[1], a[2]], [b[0], b[1], b[2]]];
|
||||
let want = [[1.0, 1.0, 10.0], [9.0, 9.0, 10.0]];
|
||||
assert!((near(ends[0], want[0]) && near(ends[1], want[1])) || (near(ends[0], want[1]) && near(ends[1], want[0])), "{ends:?}");
|
||||
}
|
||||
|
||||
/// A point of a section curve: the rational B-spline at `u` (de Boor on the weighted points).
|
||||
fn at(c: &Curve, u: f64) -> [f64; 3] {
|
||||
let Curve::Nurbs { degree, points, knots } = c else { panic!("{c:?}") };
|
||||
let p = *degree;
|
||||
let span = (p..points.len()).rev().find(|&i| knots[i] <= u).unwrap_or(p);
|
||||
let mut d: Vec<[f64; 4]> = (0..=p).map(|j| points[span - p + j]).map(|[x, y, z, w]| [x * w, y * w, z * w, w]).collect();
|
||||
for r in 1..=p {
|
||||
for j in (r..=p).rev() {
|
||||
let i = span - p + j;
|
||||
let den = knots[i + p + 1 - r] - knots[i];
|
||||
let a = if den > 0.0 { (u - knots[i]) / den } else { 0.0 };
|
||||
let prev = d[j - 1];
|
||||
for (x, q) in d[j].iter_mut().zip(prev) {
|
||||
*x = (1.0 - a) * q + a * *x;
|
||||
}
|
||||
}
|
||||
}
|
||||
let [x, y, z, w] = d[p];
|
||||
[x / w, y / w, z / w]
|
||||
}
|
||||
|
||||
/// `n + 1` points along each section curve over its interval.
|
||||
fn samples(got: &[(Curve, [f64; 2])], n: usize) -> Vec<Vec<[f64; 3]>> {
|
||||
got.iter().map(|(c, [a, b])| (0..=n).map(|i| at(c, a + (b - a) * i as f64 / n as f64)).collect()).collect()
|
||||
}
|
||||
|
||||
/// The length of the section curves, from chords.
|
||||
fn length(pts: &[Vec<[f64; 3]>]) -> f64 {
|
||||
pts.iter().flat_map(|p| p.windows(2)).map(|w| (0..3).map(|i| (w[1][i] - w[0][i]).powi(2)).sum::<f64>().sqrt()).sum()
|
||||
}
|
||||
|
||||
/// A loft from a 10 x 10 square at z = 0 to a 6 x 6 one at z = 10 (degree 1): its front wall
|
||||
/// runs from y = 0 up to y = 2, so the plane z = 5 cuts it in the line from (9, 1, 5) to
|
||||
/// (1, 1, 5).
|
||||
fn square_frustum() -> Vec<Vec<Curve>> {
|
||||
vec![square(0.0, 0.0, 10.0, 0.0), square(2.0, 2.0, 6.0, 10.0)]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lofted_wall_section_is_the_line_across_it() {
|
||||
let got = timed("section", || {
|
||||
let b = loft(&square_frustum(), 1, true).unwrap();
|
||||
face_section(&b, [5.0, 1.0, 5.0], 0.01, [0.0, 0.0, 5.0], [0.0, 0.0, 1.0], 1e-6)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(got.len(), 1, "{got:?}");
|
||||
let p = samples(&got, 8);
|
||||
let ends = [p[0][0], p[0][8]];
|
||||
let want = [[9.0, 1.0, 5.0], [1.0, 1.0, 5.0]];
|
||||
assert!((near(ends[0], want[0]) && near(ends[1], want[1])) || (near(ends[0], want[1]) && near(ends[1], want[0])), "{ends:?}");
|
||||
assert!(p[0].iter().all(|q| (q[1] - 1.0).abs() < 1e-9 && (q[2] - 5.0).abs() < 1e-9), "{p:?}");
|
||||
}
|
||||
|
||||
/// The plane z = 3 through the side of a cylinder of radius 5 and height 10 on the xy plane: the
|
||||
/// circle of radius 5 at z = 3, 2π·5 long.
|
||||
#[test]
|
||||
fn a_cylinder_side_section_is_its_circle() {
|
||||
let got = timed("section", || {
|
||||
let b = make_cylinder([0.0; 3], 5.0, 10.0).unwrap();
|
||||
face_section(&b, [5.0, 0.0, 5.0], 0.01, [0.0, 0.0, 3.0], [0.0, 0.0, 1.0], 1e-7)
|
||||
})
|
||||
.unwrap();
|
||||
let p = samples(&got, 2000);
|
||||
for q in p.iter().flatten() {
|
||||
assert!(((q[0] * q[0] + q[1] * q[1]).sqrt() - 5.0).abs() < 1e-6 && (q[2] - 3.0).abs() < 1e-9, "{q:?}");
|
||||
}
|
||||
let len = length(&p);
|
||||
assert!((len - TAU * 5.0).abs() < 1e-4, "{len} against {}", TAU * 5.0);
|
||||
}
|
||||
|
||||
/// The plane x = 5 through the top of a 10 mm box: the line (5, 0, 10)-(5, 10, 10) across it.
|
||||
#[test]
|
||||
fn a_box_top_section_is_the_line_across_it() {
|
||||
let got = timed("section", || {
|
||||
let b = make_box([0.0; 3], [10.0; 3]).unwrap();
|
||||
face_section(&b, [5.0, 5.0, 10.0], 0.01, [5.0, 5.0, 5.0], [1.0, 0.0, 0.0], 1e-7)
|
||||
})
|
||||
.unwrap();
|
||||
let p = samples(&got, 8);
|
||||
assert!(p.iter().flatten().all(|q| (q[0] - 5.0).abs() < 1e-9 && (q[2] - 10.0).abs() < 1e-9), "{p:?}");
|
||||
let len = length(&p);
|
||||
assert!((len - 10.0).abs() < 1e-6, "{len} against 10");
|
||||
}
|
||||
|
||||
/// The capi's argument checks, before any kernel call.
|
||||
#[test]
|
||||
fn hostile_arguments_are_refused() {
|
||||
let sq = square(0.0, 0.0, 1.0, 0.0);
|
||||
let d = DraftCorner::default();
|
||||
for dist in [0.0, -1.0, f64::NAN, 1e10] {
|
||||
assert_eq!(extrude(&sq, Z, dist, 0.0, d, true).err(), Some(Error::Argument), "distance {dist}");
|
||||
}
|
||||
assert_eq!(extrude(&sq, Z, 1.0, FRAC_PI_2, d, true).err(), Some(Error::Argument), "a right-angled draft");
|
||||
assert_eq!(extrude(&sq, [0.0; 3], 1.0, 0.0, d, true).err(), Some(Error::Argument), "no direction");
|
||||
assert_eq!(extrude(&[], Z, 1.0, 0.0, d, true).err(), Some(Error::Argument), "no profile");
|
||||
assert_eq!(extrude(&[line([0.0; 3], [0.0; 3])], Z, 1.0, 0.0, d, true).err(), Some(Error::Argument), "a point");
|
||||
assert_eq!(extrude(&[line([f64::NAN; 3], [1.0; 3])], Z, 1.0, 0.0, d, true).err(), Some(Error::Argument));
|
||||
// A drafted profile is one loop, square to the direction.
|
||||
assert_eq!(extrude(&sq[..3], Z, 1.0, 0.1, d, true).err(), Some(Error::Argument), "an open profile");
|
||||
assert_eq!(extrude(&sq, [1.0, 0.0, 0.0], 1.0, 0.1, d, true).err(), Some(Error::Argument), "along the profile");
|
||||
let bad_arc = Curve::Arc { center: [0.0; 3], axis: Z, reference: Z, radius: 1.0, start: 0.0, end: 1.0 };
|
||||
assert_eq!(extrude(&[bad_arc], Z, 1.0, 0.0, d, false).err(), Some(Error::Argument), "reference along the axis");
|
||||
let bad_nurbs = Curve::Nurbs { degree: 1, points: vec![[0.0; 4], [1.0, 0.0, 0.0, 1.0]], knots: vec![0.0, 0.0, 1.0, 1.0] };
|
||||
assert_eq!(extrude(&[bad_nurbs], Z, 1.0, 0.0, d, false).err(), Some(Error::Argument), "a zero weight");
|
||||
let unclamped = Curve::Nurbs { degree: 1, points: vec![[0.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 1.0]], knots: vec![0.0, 0.5, 1.0, 1.0] };
|
||||
assert_eq!(extrude(&[unclamped], Z, 1.0, 0.0, d, false).err(), Some(Error::Argument), "unclamped knots");
|
||||
assert_eq!(revolve(&sq, [0.0; 3], Z, 0.0, true).err(), Some(Error::Argument), "no angle");
|
||||
assert_eq!(revolve(&sq, [0.0; 3], Z, 7.0, true).err(), Some(Error::Argument), "more than a turn");
|
||||
assert_eq!(revolve(&sq, [f64::INFINITY; 3], Z, 1.0, true).err(), Some(Error::Argument));
|
||||
assert_eq!(loft(std::slice::from_ref(&sq), 1, true).err(), Some(Error::Argument), "one section");
|
||||
assert_eq!(loft(&[sq.clone(), sq.clone()], 4, true).err(), Some(Error::Argument), "degree 4");
|
||||
assert_eq!(loft_to_point(&sq, [0.5, 0.5, 0.0]).err(), Some(Error::Argument), "apex in the base's plane");
|
||||
assert_eq!(loft_to_point(&[circle([0.0; 3], Z, [1.0, 0.0, 0.0], 1.0)], Z).err(), Some(Error::Argument), "not lines");
|
||||
let flat = Affine { m: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]], t: [0.0; 3] };
|
||||
assert_eq!(sweep_stations(&sq, &[translate([0.0; 3]), flat], Ends::Capped).err(), Some(Error::Argument), "a singular map");
|
||||
assert_eq!(sweep_stations(&sq, &[translate([0.0; 3])], Ends::Capped).err(), Some(Error::Argument), "one station");
|
||||
assert_eq!(screw_sweep(&sq, [0.0; 3], Z, 0.0, 1.0, None, 1e-3).err(), Some(Error::Argument), "no pitch");
|
||||
assert_eq!(screw_sweep(&sq, [0.0; 3], Z, 1.0, 300.0, None, 1e-3).err(), Some(Error::Argument), "too many turns");
|
||||
assert_eq!(screw_sweep(&sq, [0.0; 3], Z, 1.0, 1.0, Some(&[(0.5, 0.0), (0.2, 0.0)]), 1e-3).err(), Some(Error::Argument), "a law going back");
|
||||
assert_eq!(screw_sweep(&sq, [0.0; 3], Z, 1.0, 1.0, Some(&[]), 1e-3).err(), Some(Error::Argument), "an empty law");
|
||||
let b = make_box([0.0; 3], [1.0; 3]).unwrap();
|
||||
assert_eq!(project_curves(&b, [0.5; 3], 0.01, &sq, [0.0; 3], 1e-6).err(), Some(Error::Argument), "no direction");
|
||||
assert_eq!(project_curves(&b, [0.5; 3], 0.0, &sq, Z, 1e-6).err(), Some(Error::Argument), "no pick tolerance");
|
||||
assert_eq!(face_section(&b, [0.5; 3], 0.01, [0.5; 3], [0.0; 3], 1e-6).err(), Some(Error::Argument), "no plane normal");
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//! Copies and affine transforms on solidcore: the C API's `smlib_brep_copy` and
|
||||
//! `smlib_brep_transform`, ported from the fork's `capi/src/smlib_c.cpp`.
|
||||
//!
|
||||
//! As there, a transform works on a copy of its body (`SmApiBrepCopy`), which
|
||||
//! `SmBrep::Transform(double[4][4], bScaleTol)` (solidcore's `transform_matrix`, solidcore#1570)
|
||||
//! then moves in place, tolerances scaling with the model. The matrix is the capi's: rows 0-2 the
|
||||
//! images of x, y and z, row 3 the translation (row-vector convention, `p' = p·A + t`); the
|
||||
//! fourth column is not read.
|
||||
|
||||
use super::Body;
|
||||
use super::blend::{on_copy, status};
|
||||
use crate::{Error, Result};
|
||||
use solidcore::topology::brep::mirror::transform_matrix;
|
||||
|
||||
/// `smlib_brep_copy`: `SmApiBrepCopy`.
|
||||
pub(crate) fn copy(body: &Body) -> Result<Body> {
|
||||
on_copy("SmApiBrepCopy", body, |_, b| Ok(Ok(b)))
|
||||
}
|
||||
|
||||
/// `smlib_brep_transform`: `SmBrep::Transform(matrix, TRUE)` on a copy of `body`. Every entry of
|
||||
/// the matrix must be finite (`SMLIB_ERR_ARG`).
|
||||
pub(crate) fn transform(body: &Body, matrix: &[[f64; 4]; 4]) -> Result<Body> {
|
||||
if !matrix.iter().flatten().all(|x| x.is_finite()) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
on_copy("SmBrep::Transform", body, |k, mut b| {
|
||||
Ok(match transform_matrix(k, &mut b, matrix, true) {
|
||||
Ok(()) => Ok(b),
|
||||
Err(e) => Err(status(e)),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
//! 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.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use super::transform::{copy, transform};
|
||||
use super::{Body, bounds, counts, make_box, make_cylinder, to_arrays, volume};
|
||||
use crate::Error;
|
||||
use crate::export::native;
|
||||
|
||||
/// The identity, as the capi reads a matrix: rows 0-2 the images of x, y and z, row 3 the move.
|
||||
const ID: [[f64; 4]; 4] = [[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, 0.0, 0.0, 1.0]];
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Shape {
|
||||
/// A box from its lower corner.
|
||||
Box([f64; 3], [f64; 3]),
|
||||
/// A cylinder about +Z from its base centre.
|
||||
Cylinder([f64; 3], f64, f64),
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
fn core(self) -> Body {
|
||||
match self {
|
||||
Shape::Box(o, s) => make_box(o, s).unwrap(),
|
||||
Shape::Cylinder(o, r, h) => make_cylinder(o, r, h).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn volume(self) -> f64 {
|
||||
match self {
|
||||
Shape::Box(_, s) => s[0] * s[1] * s[2],
|
||||
Shape::Cylinder(_, r, h) => PI * r * r * h,
|
||||
}
|
||||
}
|
||||
|
||||
fn counts(self) -> [usize; 3] {
|
||||
match self {
|
||||
Shape::Box(..) => [6, 12, 8],
|
||||
Shape::Cylinder(..) => [3, 3, 2],
|
||||
}
|
||||
}
|
||||
|
||||
/// The corners of its bounding box.
|
||||
fn corners(self) -> Vec<[f64; 3]> {
|
||||
let (lo, hi) = match self {
|
||||
Shape::Box(o, s) => (o, [o[0] + s[0], o[1] + s[1], o[2] + s[2]]),
|
||||
Shape::Cylinder(o, r, h) => ([o[0] - r, o[1] - r, o[2]], [o[0] + r, o[1] + r, o[2] + h]),
|
||||
};
|
||||
(0..8)
|
||||
.map(|k| [if k & 1 == 0 { lo[0] } else { hi[0] }, if k & 2 == 0 { lo[1] } else { hi[1] }, if k & 4 == 0 { lo[2] } else { hi[2] }])
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// `p·A + t`.
|
||||
fn apply(m: &[[f64; 4]; 4], p: [f64; 3]) -> [f64; 3] {
|
||||
[0, 1, 2].map(|j| p[0] * m[0][j] + p[1] * m[1][j] + p[2] * m[2][j] + m[3][j])
|
||||
}
|
||||
|
||||
fn det(m: &[[f64; 4]; 4]) -> f64 {
|
||||
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
|
||||
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
|
||||
}
|
||||
|
||||
fn rotation_z(a: f64) -> [[f64; 4]; 4] {
|
||||
let (s, c) = a.sin_cos();
|
||||
[[c, s, 0.0, 0.0], [-s, c, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]
|
||||
}
|
||||
|
||||
struct Case {
|
||||
shape: Shape,
|
||||
matrix: [[f64; 4]; 4],
|
||||
/// `None`: the closed form (volume times |det|, the moved box, the same counts).
|
||||
fails: Option<Error>,
|
||||
}
|
||||
|
||||
const BOX: Shape = Shape::Box([1.0, 2.0, 3.0], [10.0, 20.0, 30.0]);
|
||||
const CYL: Shape = Shape::Cylinder([0.0, 0.0, 0.0], 5.0, 12.0);
|
||||
|
||||
fn case(name: &str) -> Case {
|
||||
let ok = |shape, matrix| Case { shape, matrix, fails: None };
|
||||
let mut moved = ID;
|
||||
moved[3] = [5.0, -7.0, 11.0, 1.0];
|
||||
match name {
|
||||
"identity" => ok(BOX, ID),
|
||||
"translation" => ok(BOX, moved),
|
||||
"rotation_about_z" => ok(BOX, rotation_z(PI / 2.0)),
|
||||
"uniform_scale" => ok(BOX, [[2.0, 0.0, 0.0, 0.0], [0.0, 2.0, 0.0, 0.0], [0.0, 0.0, 2.0, 0.0], [1.0, 1.0, 1.0, 1.0]]),
|
||||
"mirror_in_x" => ok(BOX, [[-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]]),
|
||||
"non_uniform_scale" => ok(BOX, [[2.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.5, 0.0], [0.0; 4]]),
|
||||
"shear" => ok(BOX, [[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]]),
|
||||
"cylinder_moved" => ok(CYL, moved),
|
||||
"cylinder_rotated" => ok(CYL, rotation_z(0.3)),
|
||||
"cylinder_mirrored" => ok(CYL, [[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]]),
|
||||
"cylinder_scaled" => ok(CYL, [[3.0, 0.0, 0.0, 0.0], [0.0, 3.0, 0.0, 0.0], [0.0, 0.0, 3.0, 0.0], [0.0; 4]]),
|
||||
_ => panic!("no case {name}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Measured {
|
||||
volume: f64,
|
||||
bounds: ([f64; 3], [f64; 3]),
|
||||
counts: [usize; 3],
|
||||
kinds: BTreeMap<String, usize>,
|
||||
}
|
||||
|
||||
type Got = std::result::Result<Measured, Error>;
|
||||
|
||||
fn kinds(rec: &solidcraft_geom::brep::Brep) -> BTreeMap<String, usize> {
|
||||
let mut out = BTreeMap::new();
|
||||
for f in &rec.faces {
|
||||
let v = serde_json::to_value(&f.surface).unwrap();
|
||||
*out.entry(v["kind"].as_str().unwrap_or("?").to_owned()).or_default() += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs()
|
||||
}
|
||||
|
||||
/// The moved shape's bounding box: a box's moved corners bound it exactly; a cylinder's only for
|
||||
/// the matrices here (a rotation about its axis, a move, a mirror in z, a uniform scale), whose
|
||||
/// moved circle's box is the moved square's turned back to the axes.
|
||||
fn expected_bounds(c: &Case) -> ([f64; 3], [f64; 3]) {
|
||||
let pts: Vec<[f64; 3]> = match c.shape {
|
||||
Shape::Cylinder(o, r, h) => {
|
||||
let s = (c.matrix[0][0] * c.matrix[0][0] + c.matrix[0][1] * c.matrix[0][1]).sqrt();
|
||||
let centre = [apply(&c.matrix, o), apply(&c.matrix, [o[0], o[1], o[2] + h])];
|
||||
centre.iter().flat_map(|p| [[p[0] - r * s, p[1] - r * s, p[2]], [p[0] + r * s, p[1] + r * s, p[2]]]).collect()
|
||||
}
|
||||
s => s.corners().into_iter().map(|p| apply(&c.matrix, p)).collect(),
|
||||
};
|
||||
let mut lo = [f64::INFINITY; 3];
|
||||
let mut hi = [f64::NEG_INFINITY; 3];
|
||||
for p in pts {
|
||||
for k in 0..3 {
|
||||
lo[k] = lo[k].min(p[k]);
|
||||
hi[k] = hi[k].max(p[k]);
|
||||
}
|
||||
}
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
fn judge(name: &str, who: &str, got: Got) -> Got {
|
||||
let c = case(name);
|
||||
match (&c.fails, &got) {
|
||||
(Some(e), Err(g)) => assert_eq!(g, e, "{name}: {who} failed otherwise"),
|
||||
(Some(e), Ok(m)) => panic!("{name}: {who} gave {m:?}, expected {e:?}"),
|
||||
(None, Err(g)) => panic!("{name}: {who} failed: {g}"),
|
||||
(None, Ok(m)) => {
|
||||
let v = c.shape.volume() * det(&c.matrix).abs();
|
||||
assert!(rel(m.volume, v) < 1e-9, "{name}: {who} volume {} vs analytic {v}", m.volume);
|
||||
assert_eq!(m.counts, c.shape.counts(), "{name}: {who} counts");
|
||||
let (lo, hi) = expected_bounds(&c);
|
||||
let near = |a: [f64; 3], b: [f64; 3]| a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-6);
|
||||
assert!(near(m.bounds.0, lo) && near(m.bounds.1, hi), "{name}: {who} bounds {:?}, expected {lo:?} {hi:?}", m.bounds);
|
||||
}
|
||||
}
|
||||
got
|
||||
}
|
||||
|
||||
fn solidcore_case(name: &str) -> Got {
|
||||
let c = case(name);
|
||||
let got = transform(&c.shape.core(), &c.matrix).map(|b| {
|
||||
let (rec, _) = native(&to_arrays(&b).unwrap()).unwrap_or_else(|e| panic!("{name}: record: {e:?}"));
|
||||
rec.validate().unwrap_or_else(|e| panic!("{name}: record: {e}"));
|
||||
Measured { volume: volume(&b, 1e-8).unwrap(), bounds: bounds(&b).unwrap(), counts: counts(&b).unwrap(), kinds: kinds(&rec) }
|
||||
});
|
||||
judge(name, "solidcore", got)
|
||||
}
|
||||
|
||||
macro_rules! cases {
|
||||
($($m:ident $(: $ign:meta)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
#[test]
|
||||
$(#[$ign])?
|
||||
fn solidcore_matches_the_closed_form() {
|
||||
let _ = super::solidcore_case(stringify!($m));
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
cases!(
|
||||
identity,
|
||||
translation,
|
||||
rotation_about_z,
|
||||
uniform_scale,
|
||||
mirror_in_x,
|
||||
non_uniform_scale,
|
||||
shear,
|
||||
cylinder_moved,
|
||||
cylinder_rotated,
|
||||
cylinder_mirrored,
|
||||
cylinder_scaled,
|
||||
);
|
||||
|
||||
/// The capi's argument check, before any kernel call: every entry finite.
|
||||
#[test]
|
||||
fn hostile_matrices_are_refused() {
|
||||
let b = BOX.core();
|
||||
for (i, j) in [(0, 0), (1, 2), (3, 1), (2, 3)] {
|
||||
for x in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
|
||||
let mut m = ID;
|
||||
m[i][j] = x;
|
||||
assert_eq!(transform(&b, &m).err(), Some(Error::Argument), "m[{i}][{j}] = {x}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A singular or huge matrix never panics: it gives a body or an error.
|
||||
#[test]
|
||||
fn degenerate_matrices_do_not_crash() {
|
||||
let b = BOX.core();
|
||||
let zero = [[0.0; 4]; 4];
|
||||
let flat = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0; 4]];
|
||||
let huge = [[1e300, 0.0, 0.0, 0.0], [0.0, 1e300, 0.0, 0.0], [0.0, 0.0, 1e300, 0.0], [0.0; 4]];
|
||||
for m in [zero, flat, huge] {
|
||||
let _ = transform(&b, &m);
|
||||
}
|
||||
}
|
||||
|
||||
/// The body moved, and the body copied, are never changed.
|
||||
#[test]
|
||||
fn the_body_stays_as_it_was() {
|
||||
let b = BOX.core();
|
||||
let moved = transform(&b, &rotation_z(1.0)).unwrap();
|
||||
let c = copy(&b).unwrap();
|
||||
drop(moved);
|
||||
for x in [&b, &c] {
|
||||
assert_eq!(counts(x).unwrap(), [6, 12, 8]);
|
||||
assert!(rel(volume(x, 1e-8).unwrap(), 6000.0) < 1e-12);
|
||||
assert_eq!(bounds(x).unwrap(), ([1.0, 2.0, 3.0], [11.0, 22.0, 33.0]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,862 @@
|
||||
//! Untrim and extend on solidcore: the C API's `smlib_untrim_face` and `smlib_extend_sheet`,
|
||||
//! ported from the fork's `capi/src/smlib_untrim.cpp` (`face_at`, `loop_distance`,
|
||||
//! `copy_attributes`, `analytic`, `span_of`, `pieces_of`, `is_disc`, `rebuild_hole`) and
|
||||
//! `capi/src/smlib_extend.cpp` (`on_side`, `eval`, `nearest_edge`, `is_boundary`, `side_edge`,
|
||||
//! `across`, `cone_continued`, `same_patch`).
|
||||
//!
|
||||
//! A face restored towards its surface. Picked inner loops (holes) are taken away with
|
||||
//! `SmFace::DeleteInnerLoop` on a copy where they are lamina; the outer boundary goes back to a
|
||||
//! parameter box of the surface (`SmBrep::CreateFaceFromSurface`), and the holes kept are rebuilt
|
||||
//! in that face from their own edges' curves and intervals (`MakeVertexLoop`, `MakeEdgeInFace`:
|
||||
//! closing a loop splits off the face inside it, which is deleted). The new face and the rebuilt
|
||||
//! edges take the attributes of the face and edges they stand for, so the history tags carry
|
||||
//! through.
|
||||
//!
|
||||
//! A sheet is extended by SMLib's extension of its surface (`CreateExtendedSurface`: its end
|
||||
//! spans continued, or a G1 or G2 run-off), the distance asked of it corrected until the
|
||||
//! extension measures the distance across the side's middle; `CopyAndAddAnalytics` gives the
|
||||
//! result back its analytic kind, and a cone or cylinder continued naturally stays the analytic
|
||||
//! widened. The new face and each of its sides take the attributes of the face and side they
|
||||
//! continue.
|
||||
//!
|
||||
//! An untrim of a face out of its body (`in_place` false) copies the face into a body of its own
|
||||
//! through `SmBrep::CopyFaces` with the merge (solidcore#1872), which keeps its attributes.
|
||||
|
||||
use std::sync::PoisonError;
|
||||
|
||||
use solidcore::EntityRef;
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::geom::extent2d::Extent2d;
|
||||
use solidcore::geom::geom_utility::p2::ContinuityType;
|
||||
use solidcore::geom::object::Shape;
|
||||
use solidcore::geom::tol::USE_DEFAULT;
|
||||
use solidcore::geom::vector2d::Point2;
|
||||
use solidcore::geom::vector3d::{Point3, Vec3};
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::nurbs::b_spline_surface::p1::SurfParamType;
|
||||
use solidcore::nurbs::b_spline_surface_n_lib::p2::EvaluateFlags;
|
||||
use solidcore::nurbs::b_spline_surface_n_lib::p2::OrientType;
|
||||
use solidcore::query::api_queries::{edge_closest_point, face_closest_point_in_shape, face_internal_point};
|
||||
use solidcore::query::brep::{BrepQuery, LiveBrepObjects};
|
||||
use solidcore::surface::cone::p1::Cone;
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::surface::surface::SurfaceKind;
|
||||
use solidcore::topology::brep::p1 as brep_p1;
|
||||
use solidcore::topology::brep::p2::{CopyNotifyState, copy_source_ref};
|
||||
use solidcore::topology::brep::p3::{DeleteFaceOptions, delete_face, new_brep, with_kernel_callbacks};
|
||||
use solidcore::topology::brep::p5::{EdgeInFace, make_edge_in_face};
|
||||
use solidcore::topology::brep::p7::copy_faces_from;
|
||||
use solidcore::topology::brep::p8::create_face_from_surface;
|
||||
use solidcore::topology::brep::p9::make_vertex_loop;
|
||||
use solidcore::topology::edgeuse::p1 as eu_p1;
|
||||
use solidcore::topology::edgeuse::p3::get_or_create_uv_trim_curve;
|
||||
use solidcore::topology::face::{p1 as face_p1, p2 as face_p2, p4 as face_p4};
|
||||
use solidcore::topology::loopuse::{loop_edges, loop_loopuses, notify_ref};
|
||||
use solidcore::topology::topology_traverser::Marks;
|
||||
use solidcore::topology::{Brep, BrepId, EdgeId, FaceId, LoopId, edge};
|
||||
|
||||
use super::blend::{p3, status};
|
||||
use super::sweep::build_faces;
|
||||
use super::{Body, answer, run};
|
||||
use crate::trim::Continuation;
|
||||
use crate::{Error, Result};
|
||||
use solidcore::topology::brep::p3::brep_copy;
|
||||
|
||||
/// `MAX_HOLE_POINTS`.
|
||||
const MAX_HOLE_POINTS: usize = 10_000;
|
||||
|
||||
/// `SMLIB_ERR_EXCEPTION`'s stand-in: a rebuilt hole that did not close as the capi expects.
|
||||
fn hole_refused() -> Error {
|
||||
Error::Exception
|
||||
}
|
||||
|
||||
/// The capi's statuses as SolidCraft errors: an operation's own result.
|
||||
type Out<T> = std::result::Result<T, Error>;
|
||||
|
||||
const ONE: BrepId = BrepId::from_index(0);
|
||||
|
||||
/// `face_at`: the face of `b` nearest `p`, within `tol` (`SmApiFaceClosestPoint`; a face whose
|
||||
/// query fails is skipped).
|
||||
fn face_at(k: &mut Kernel, b: &mut Brep, p: Point3, tol: f64) -> solidcore::Result<Option<FaceId>> {
|
||||
let all = brep_p1::faces(b, None)?;
|
||||
let mut shapes = Vec::with_capacity(all.len());
|
||||
for &f in &all {
|
||||
shapes.push(answer(Shape::new(k, &(ONE, &*b), &[], &[(ONE, f)], &[], &[]))?);
|
||||
}
|
||||
let query = BrepQuery::of_brep(b);
|
||||
let identity = query.effective_identity();
|
||||
let objects = LiveBrepObjects::new(b, &identity, None)?;
|
||||
let (mut best, mut d0) = (None, f64::INFINITY);
|
||||
for (&f, shape) in all.iter().zip(&shapes) {
|
||||
let Some(shape) = shape else { continue };
|
||||
if let Some(c) = answer(face_closest_point_in_shape(k, shape, &objects, (ONE, f), p))?
|
||||
&& c.distance.is_finite()
|
||||
&& c.distance < d0
|
||||
{
|
||||
d0 = c.distance;
|
||||
best = Some(f);
|
||||
}
|
||||
}
|
||||
Ok(best.filter(|_| d0 <= tol))
|
||||
}
|
||||
|
||||
/// `loop_distance`: the distance from `p` to the nearest edge of loop `l`.
|
||||
fn loop_distance(k: &Kernel, b: &Brep, l: LoopId, p: Point3) -> solidcore::Result<f64> {
|
||||
let mut best = f64::INFINITY;
|
||||
for e in loop_edges(b, l)?.into_iter().flatten() {
|
||||
if let Some(c) = answer(edge_closest_point(k, b, e, p))?
|
||||
&& c.closest.distance.is_finite()
|
||||
{
|
||||
best = best.min(c.closest.distance);
|
||||
}
|
||||
}
|
||||
Ok(best)
|
||||
}
|
||||
|
||||
/// `copy_attributes`: `to` (in `into`) takes a copy of each attribute of `from` (in `src`).
|
||||
fn copy_attributes(src: &Brep, from: EntityRef, into: &mut Brep, to: EntityRef) -> solidcore::Result<()> {
|
||||
let copies: Vec<_> = src.entity_attributes(from).into_iter().map(|(_, a)| a.make_copy()).collect();
|
||||
for a in copies {
|
||||
into.add_attribute(to, a)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// [`copy_attributes`] within one body.
|
||||
fn copy_attributes_within(b: &mut Brep, from: EntityRef, to: EntityRef) -> solidcore::Result<()> {
|
||||
let copies: Vec<_> = b.entity_attributes(from).into_iter().map(|(_, a)| a.make_copy()).collect();
|
||||
for a in copies {
|
||||
b.add_attribute(to, a)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `span_of`: the box of the face's outer loop in the surface's parameters, the union of the
|
||||
/// tight boxes of its edgeuses' UV trim curves (made where the face has none attached yet).
|
||||
fn span_of(k: &Kernel, b: &mut Brep, outer: LoopId) -> solidcore::Result<Option<Extent2d>> {
|
||||
let (lu, _) = loop_loopuses(b, outer)?;
|
||||
let eus = eu_p1::loopuse_edgeuses(b, lu)?;
|
||||
let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
|
||||
let mut any = false;
|
||||
for eu in eus {
|
||||
let fetched = get_or_create_uv_trim_curve(k, b, eu, false, false, true)?;
|
||||
if fetched.status.is_err() {
|
||||
return Ok(None);
|
||||
}
|
||||
let (Some(uv), _) = eu_p1::uv_trim_curve(b, eu)? else { return Ok(None) };
|
||||
let Some(e) = answer(uv.calculate_tight_bounding_box(k, &uv.natural_interval()))? else { return Ok(None) };
|
||||
let (mn, mx) = (e.min(), e.max());
|
||||
lo = [lo[0].min(mn.x), lo[1].min(mn.y)];
|
||||
hi = [hi[0].max(mx.x), hi[1].max(mx.y)];
|
||||
any = true;
|
||||
}
|
||||
if !any || !(hi[0] > lo[0]) || !(hi[1] > lo[1]) || !(lo[0] + lo[1] + hi[0] + hi[1]).is_finite() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(Extent2d::from_bounds(lo[0], lo[1], hi[0], hi[1])))
|
||||
}
|
||||
|
||||
/// One edge of a hole to rebuild: its edge, how the loop runs along it, its ends as the loop
|
||||
/// runs, its curve and interval (`Piece`).
|
||||
struct Piece {
|
||||
edge: EdgeId,
|
||||
orient: OrientType,
|
||||
from: Point3,
|
||||
to: Point3,
|
||||
curve: Curve,
|
||||
interval: solidcore::geom::extent1d::Extent1d,
|
||||
}
|
||||
|
||||
/// `pieces_of`; `None` where the capi gives up.
|
||||
fn pieces_of(b: &Brep, l: LoopId) -> solidcore::Result<Option<Vec<Piece>>> {
|
||||
let (lu, _) = loop_loopuses(b, l)?;
|
||||
let mut out = Vec::new();
|
||||
for eu in eu_p1::loopuse_edgeuses(b, lu)? {
|
||||
let Some(e) = eu_p1::edge(b, eu)? else { return Ok(None) };
|
||||
let ed = b.edge(e)?;
|
||||
let Some(curve) = ed.curve.clone() else { return Ok(None) };
|
||||
let (Some(v0), Some(v1)) = edge::start_end_vertices(b, e)? else { return Ok(None) };
|
||||
let same = eu_p1::orientation(b, eu)? == OrientType::Same;
|
||||
let (p0, p1) = (b.vertex(v0)?.point, b.vertex(v1)?.point);
|
||||
let (from, to) = if same { (p0, p1) } else { (p1, p0) };
|
||||
out.push(Piece { edge: e, orient: if same { OrientType::Same } else { OrientType::Opposite }, from, to, curve, interval: ed.interval });
|
||||
}
|
||||
Ok((!out.is_empty()).then_some(out))
|
||||
}
|
||||
|
||||
/// `is_disc`: whether `f` is bounded by one loop of exactly the edges `made`.
|
||||
fn is_disc(b: &Brep, f: FaceId, made: &[EdgeId]) -> solidcore::Result<bool> {
|
||||
let loops = face_p2::loops(b, f, false, None)?;
|
||||
let [l] = loops.as_slice() else { return Ok(false) };
|
||||
let edges: Vec<EdgeId> = loop_edges(b, *l)?.into_iter().flatten().collect();
|
||||
Ok(edges.len() == made.len() && edges.iter().all(|e| made.contains(e)))
|
||||
}
|
||||
|
||||
/// `rebuild_hole`: the hole `pieces` (edges of `src`) rebuilt in `face` of `r`: vertices and
|
||||
/// edges made in the face, the face the closed loop splits off inside it deleted. `face` is the
|
||||
/// face that keeps the rest.
|
||||
fn rebuild_hole(k: &mut Kernel, src: Option<&Brep>, r: &mut Brep, face: &mut FaceId, pieces: &[Piece]) -> solidcore::Result<Out<()>> {
|
||||
let Some(first_piece) = pieces.first() else { return Ok(Err(hole_refused())) };
|
||||
let editing = r.editing_enabled();
|
||||
r.set_editing_enabled(true);
|
||||
let tol = r.tolerance();
|
||||
// Each edit notifies through the kernel's callbacks with the body's own attributes
|
||||
// (`MakeEdgeInFace` does so itself), so the history tags follow.
|
||||
let vertex_loop = |k: &mut Kernel, r: &mut Brep, face: FaceId, at: Point3| {
|
||||
with_kernel_callbacks(k, r, |_, r, env| make_vertex_loop(r, face, at, OrientType::Opposite, env))
|
||||
};
|
||||
let done = (|| -> solidcore::Result<Out<()>> {
|
||||
let (_, first) = match vertex_loop(k, r, *face, first_piece.from) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
};
|
||||
let mut at = first;
|
||||
let mut made = Vec::with_capacity(pieces.len());
|
||||
let mut split = None;
|
||||
for (i, p) in pieces.iter().enumerate() {
|
||||
let last = i + 1 == pieces.len();
|
||||
let next = if last {
|
||||
first
|
||||
} else {
|
||||
match vertex_loop(k, r, *face, p.to) {
|
||||
Ok((_, v)) => v,
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
}
|
||||
};
|
||||
let curve = p.curve.copy(k)?;
|
||||
let mut out = EdgeInFace::default();
|
||||
if let Err(e) = make_edge_in_face(k, r, true, Some(*face), Some(at), Some(next), curve, None, p.interval, p.orient, tol, &mut out, None) {
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
let Some(e) = out.edge else { return Ok(Err(Error::Empty)) };
|
||||
match src {
|
||||
Some(s) => copy_attributes(s, EntityRef::Edge(p.edge), r, EntityRef::Edge(e))?,
|
||||
None => copy_attributes_within(r, EntityRef::Edge(p.edge), EntityRef::Edge(e))?,
|
||||
}
|
||||
made.push(e);
|
||||
if let Some(nf) = out.face {
|
||||
if !last || split.is_some() {
|
||||
return Ok(Err(hole_refused()));
|
||||
}
|
||||
split = Some(nf);
|
||||
}
|
||||
at = next;
|
||||
}
|
||||
let Some(split) = split else { return Ok(Err(hole_refused())) };
|
||||
// One of the two faces is the disc inside the hole: it goes, its edges stay on the other.
|
||||
let disc = if is_disc(r, split, &made)? {
|
||||
split
|
||||
} else if is_disc(r, *face, &made)? {
|
||||
*face
|
||||
} else {
|
||||
return Ok(Err(hole_refused()));
|
||||
};
|
||||
let rest = if disc == split { *face } else { split };
|
||||
let deleted = with_kernel_callbacks(k, r, |k, r, env| {
|
||||
let mut marks = Marks::default();
|
||||
let options = DeleteFaceOptions { delete_edges_vertices: false, ..DeleteFaceOptions::default() };
|
||||
delete_face(k, r, env, &mut marks, disc, options)
|
||||
});
|
||||
if let Err(e) = deleted {
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
*face = rest;
|
||||
Ok(Ok(()))
|
||||
})();
|
||||
r.set_editing_enabled(editing);
|
||||
r.refresh_cached_topology();
|
||||
done
|
||||
}
|
||||
|
||||
/// The orientation of `f`'s upward faceuse.
|
||||
fn upward(b: &Brep, f: FaceId) -> solidcore::Result<Option<OrientType>> {
|
||||
Ok(match face_p2::upward_faceuse(b, f)? {
|
||||
Some(fu) => Some(b.faceuse(fu)?.orientation),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// `smlib_untrim_face`: the face of `body` nearest `point` (within `tol`) restored towards its
|
||||
/// surface: the holes nearest `holes` (all of them for `None`) taken away and, with `outer`, its
|
||||
/// outer boundary back on the surface's parameter box. The face alone, or with `in_place` the
|
||||
/// whole body with it replaced.
|
||||
pub(crate) fn untrim_face(body: &Body, point: [f64; 3], tol: f64, holes: Option<&[[f64; 3]]>, outer: bool, in_place: bool) -> Result<Body> {
|
||||
let n_holes = holes.map(<[_]>::len);
|
||||
if !point.iter().all(|x| x.is_finite())
|
||||
|| !tol.is_finite()
|
||||
|| !(tol > 0.0)
|
||||
|| n_holes.is_some_and(|n| n > MAX_HOLE_POINTS)
|
||||
|| holes.is_some_and(|h| !h.iter().flatten().all(|x| x.is_finite()))
|
||||
|| (n_holes == Some(0) && !outer)
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if body.0.is_poisoned() {
|
||||
return Err(Error::Internal("smlib_untrim_face: the body was left unfinished by an earlier panic".to_owned()));
|
||||
}
|
||||
let at = p3(point);
|
||||
run("smlib_untrim_face", |k| {
|
||||
let mut whole = {
|
||||
let g = body.0.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
super::dump::input(k, "brep", &g);
|
||||
brep_copy(k, &g)?
|
||||
};
|
||||
// The face to untrim, in a body of its own (its hole edges lamina there) or in the copy
|
||||
// of the whole body.
|
||||
let (mut w, f) = if in_place {
|
||||
let f = face_at(k, &mut whole, at, tol)?;
|
||||
(whole, f)
|
||||
} else {
|
||||
let Some(src) = face_at(k, &mut whole, at, tol)? else { return Ok(Err(Error::NoMatch { point: 0 })) };
|
||||
let mut w = new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
w.set_tolerance(whole.tolerance());
|
||||
// Through SmMerge, with both bodies' attributes in the notifications, so the copy
|
||||
// carries its source's history tags (`SM_NO_COPY`).
|
||||
let copied = {
|
||||
let mut state = CopyNotifyState::with_attributes_of(&mut whole, &mut w)?;
|
||||
let this_face = |f: FaceId| notify_ref(EntityRef::Face(f));
|
||||
let copied = copy_faces_from(k, &mut whole, &[src], &mut w, true, ©_source_ref, &this_face, &mut state.env(None));
|
||||
state.restore_attributes(&mut whole, &mut w)?;
|
||||
copied
|
||||
};
|
||||
let copied = match copied {
|
||||
Ok((c, _)) => c,
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
};
|
||||
// The capi's `copied.GetSize() != 1`: `SMLIB_ERR_EMPTY`.
|
||||
let [f] = copied.as_slice() else { return Ok(Err(Error::Empty)) };
|
||||
w.refresh_cached_topology();
|
||||
(w, Some(*f))
|
||||
};
|
||||
let Some(f) = f else { return Ok(Err(Error::NoMatch { point: 0 })) };
|
||||
let loops = face_p2::loops(&w, f, false, None)?;
|
||||
if loops.is_empty() {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
let inner = loops.get(1..).unwrap_or_default();
|
||||
// The holes taken away: all of them, or the one nearest each point.
|
||||
let mut removed: Vec<LoopId> = Vec::new();
|
||||
match holes {
|
||||
None => removed.extend_from_slice(inner),
|
||||
Some(points) => {
|
||||
for p in points {
|
||||
let p = p3(*p);
|
||||
let (mut best, mut d0) = (None, f64::INFINITY);
|
||||
for &l in inner {
|
||||
let d = loop_distance(k, &w, l, p)?;
|
||||
if d < d0 {
|
||||
d0 = d;
|
||||
best = Some(l);
|
||||
}
|
||||
}
|
||||
let Some(best) = best.filter(|_| d0 <= tol) else { return Ok(Err(Error::NoMatch { point: 0 })) };
|
||||
if !removed.contains(&best) {
|
||||
removed.push(best);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !outer {
|
||||
for &l in &removed {
|
||||
// A hole whose edges another face of the body uses is no hole of a sheet.
|
||||
let deleted = face_p1::delete_inner_loop_with_kernel_callbacks(k, &mut w, f, l);
|
||||
if deleted.is_err() {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
}
|
||||
w.refresh_cached_topology();
|
||||
return Ok(Ok(w));
|
||||
}
|
||||
// The outer boundary: the surface's parameter box (a free-form surface's own, an
|
||||
// analytic one's the span of the face).
|
||||
let Some(surf) = face_p2::surface(&w, f)?.cloned() else { return Ok(Err(Error::Unsupported)) };
|
||||
let mut bx = surf.natural_uv_domain_with_nurb(k)?;
|
||||
if !matches!(surf.kind(), SurfaceKind::BSpline(_)) {
|
||||
let Some(outer_loop) = loops.first() else { return Ok(Err(Error::Unsupported)) };
|
||||
match span_of(k, &mut w, *outer_loop)? {
|
||||
Some(s) => bx = s,
|
||||
None => return Ok(Err(Error::Unsupported)),
|
||||
}
|
||||
}
|
||||
let mut kept = Vec::new();
|
||||
for &l in inner {
|
||||
if removed.contains(&l) {
|
||||
continue;
|
||||
}
|
||||
match pieces_of(&w, l)? {
|
||||
Some(p) => kept.push(p),
|
||||
None => return Ok(Err(Error::Unsupported)),
|
||||
}
|
||||
}
|
||||
let copy = surf.copy(k)?;
|
||||
let was = upward(&w, f)?;
|
||||
let mut r = if in_place {
|
||||
None
|
||||
} else {
|
||||
let mut r = new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
r.set_tolerance(w.tolerance());
|
||||
Some(r)
|
||||
};
|
||||
let target: &mut Brep = match r.as_mut() {
|
||||
Some(r) => r,
|
||||
None => &mut w,
|
||||
};
|
||||
let mut nf = match build_faces(target, |t, fenv| create_face_from_surface(k, t, Some(copy), &bx, fenv)) {
|
||||
Ok(nf) => nf,
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
};
|
||||
target.refresh_cached_topology();
|
||||
let _ = target;
|
||||
for hole in &kept {
|
||||
let done = match r.as_mut() {
|
||||
Some(r) => rebuild_hole(k, Some(&w), r, &mut nf, hole)?,
|
||||
None => rebuild_hole(k, None, &mut w, &mut nf, hole)?,
|
||||
};
|
||||
if let Err(e) = done {
|
||||
return Ok(Err(e));
|
||||
}
|
||||
}
|
||||
match r.as_mut() {
|
||||
Some(r) => copy_attributes(&w, EntityRef::Face(f), r, EntityRef::Face(nf))?,
|
||||
None => copy_attributes_within(&mut w, EntityRef::Face(f), EntityRef::Face(nf))?,
|
||||
}
|
||||
// The face faces the way the old one did.
|
||||
let target: &mut Brep = match r.as_mut() {
|
||||
Some(r) => r,
|
||||
None => &mut w,
|
||||
};
|
||||
let now = upward(target, nf)?;
|
||||
if let (Some(a), Some(b)) = (was, now)
|
||||
&& a != b
|
||||
&& let Err(e) = face_p1::flip_face_orientation(target, nf)
|
||||
{
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
if let Some(r) = r {
|
||||
return Ok(Ok(r));
|
||||
}
|
||||
// The old face goes, with the edges and vertices only it used: an edit of the brep,
|
||||
// which SMLib allows only in edit mode.
|
||||
let editing = w.editing_enabled();
|
||||
w.set_editing_enabled(true);
|
||||
let deleted = with_kernel_callbacks(k, &mut w, |k, w, env| {
|
||||
let mut marks = Marks::default();
|
||||
let options = DeleteFaceOptions { delete_edges_vertices: true, ..DeleteFaceOptions::default() };
|
||||
delete_face(k, w, env, &mut marks, f, options)
|
||||
});
|
||||
w.set_editing_enabled(editing);
|
||||
w.refresh_cached_topology();
|
||||
if let Err(e) = deleted {
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
Ok(Ok(w))
|
||||
})?
|
||||
.map(Body::new)
|
||||
}
|
||||
|
||||
/// One side of a face's parameter box (`Side`): which, whether it is a u = const line (the face
|
||||
/// grows in u), and whether it is at the box's max.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Side {
|
||||
which: SurfParamType,
|
||||
u: bool,
|
||||
high: bool,
|
||||
}
|
||||
|
||||
/// `SIDES`.
|
||||
const SIDES: [Side; 4] = [
|
||||
Side { which: SurfParamType::UMin, u: true, high: false },
|
||||
Side { which: SurfParamType::UMax, u: true, high: true },
|
||||
Side { which: SurfParamType::VMin, u: false, high: false },
|
||||
Side { which: SurfParamType::VMax, u: false, high: true },
|
||||
];
|
||||
|
||||
/// `SAMPLES`.
|
||||
const SAMPLES: usize = 9;
|
||||
|
||||
/// `on_side`: the point of side `s` of `dom` at `t` in [0, 1] along it, in UV.
|
||||
fn on_side(dom: &Extent2d, s: Side, t: f64) -> Point2 {
|
||||
let (lo, hi) = (dom.min(), dom.max());
|
||||
if s.u {
|
||||
Point2::new(if s.high { hi.x } else { lo.x }, lo.y + t * (hi.y - lo.y))
|
||||
} else {
|
||||
Point2::new(lo.x + t * (hi.x - lo.x), if s.high { hi.y } else { lo.y })
|
||||
}
|
||||
}
|
||||
|
||||
/// `eval`: the point at `uv` with its u and v derivatives; `None` where it does not evaluate or
|
||||
/// is not finite.
|
||||
fn eval(k: &Kernel, surf: &Surface, uv: Point2) -> Option<(Vec3, Vec3, Vec3)> {
|
||||
let d = surf.evaluate(k, uv, 1, 1, EvaluateFlags::default()).ok()?;
|
||||
let p = *d.first()?.first()?;
|
||||
let du = *d.get(1)?.first()?;
|
||||
let dv = *d.first()?.get(1)?;
|
||||
(p.length().is_finite() && du.length().is_finite() && dv.length().is_finite()).then_some((p, du, dv))
|
||||
}
|
||||
|
||||
fn point_at(k: &Kernel, surf: &Surface, uv: Point2) -> Option<Vec3> {
|
||||
eval(k, surf, uv).map(|(p, _, _)| p)
|
||||
}
|
||||
|
||||
/// `edge_distance`.
|
||||
fn edge_distance(k: &Kernel, b: &Brep, e: EdgeId, p: Vec3) -> f64 {
|
||||
match edge_closest_point(k, b, e, p) {
|
||||
Ok(c) if c.closest.distance.is_finite() => c.closest.distance,
|
||||
_ => f64::INFINITY,
|
||||
}
|
||||
}
|
||||
|
||||
/// `nearest_edge`: the distance to the nearest of `edges`, with that edge.
|
||||
fn nearest_edge(k: &Kernel, b: &Brep, edges: &[EdgeId], p: Vec3) -> (f64, Option<EdgeId>) {
|
||||
let (mut best, mut which) = (f64::INFINITY, None);
|
||||
for &e in edges {
|
||||
let d = edge_distance(k, b, e, p);
|
||||
if d < best {
|
||||
best = d;
|
||||
which = Some(e);
|
||||
}
|
||||
}
|
||||
(best, which)
|
||||
}
|
||||
|
||||
/// `is_boundary`: whether side `s` of the face's box is a side of the face (its samples on the
|
||||
/// face's edges), and whether it collapses to a point (a pole).
|
||||
fn is_boundary(k: &Kernel, b: &Brep, surf: &Surface, dom: &Extent2d, s: Side, edges: &[EdgeId], tol: f64) -> (bool, bool) {
|
||||
let (Some(a), Some(z), Some(m)) =
|
||||
(point_at(k, surf, on_side(dom, s, 0.0)), point_at(k, surf, on_side(dom, s, 1.0)), point_at(k, surf, on_side(dom, s, 0.5)))
|
||||
else {
|
||||
return (false, false);
|
||||
};
|
||||
if (a - z).length() <= tol && (a - m).length() <= tol {
|
||||
return (true, true);
|
||||
}
|
||||
let all = (0..=SAMPLES).all(|i| match point_at(k, surf, on_side(dom, s, i as f64 / SAMPLES as f64)) {
|
||||
Some(p) => nearest_edge(k, b, edges, p).0 <= tol,
|
||||
None => false,
|
||||
});
|
||||
(all, false)
|
||||
}
|
||||
|
||||
/// `side_edge`: the edge of `edges` side `s` of `dom` runs along (nearest the side's middle), or
|
||||
/// `None` for a side collapsed to a point.
|
||||
fn side_edge(k: &Kernel, b: &Brep, surf: &Surface, dom: &Extent2d, s: Side, edges: &[EdgeId], on: f64) -> Option<EdgeId> {
|
||||
let a = point_at(k, surf, on_side(dom, s, 0.0))?;
|
||||
let z = point_at(k, surf, on_side(dom, s, 1.0))?;
|
||||
let m = point_at(k, surf, on_side(dom, s, 0.5))?;
|
||||
if (a - z).length() <= on && (a - m).length() <= on {
|
||||
return None;
|
||||
}
|
||||
let (d, e) = nearest_edge(k, b, edges, m);
|
||||
e.filter(|_| d <= on)
|
||||
}
|
||||
|
||||
/// `across`: how far `ext` (the face's surface extended past side `s` of its box `dom`, on the
|
||||
/// same parameters) runs past that side, measured across it through the side's middle, by
|
||||
/// 16-point Gauss-Legendre quadrature on 64 pieces. Negative when it can't be evaluated.
|
||||
fn across(k: &Kernel, ext: &Surface, dom: &Extent2d, s: Side) -> f64 {
|
||||
const X: [f64; 8] = [
|
||||
0.0950125098376374,
|
||||
0.2816035507792589,
|
||||
0.4580167776572274,
|
||||
0.6178762444026438,
|
||||
0.755404408355003,
|
||||
0.8656312023878318,
|
||||
0.9445750230732326,
|
||||
0.9894009349916499,
|
||||
];
|
||||
const W: [f64; 8] = [
|
||||
0.1894506104550685,
|
||||
0.1826034150449236,
|
||||
0.1691565193950025,
|
||||
0.1495959888165767,
|
||||
0.1246289712555339,
|
||||
0.0951585116824928,
|
||||
0.0622535239386479,
|
||||
0.0271524594117541,
|
||||
];
|
||||
const PIECES: usize = 64;
|
||||
let Ok(bx) = ext.natural_uv_domain_with_nurb(k) else { return -1.0 };
|
||||
let mid = on_side(dom, s, 0.5);
|
||||
let from = if s.u { mid.x } else { mid.y };
|
||||
let to = match (s.u, s.high) {
|
||||
(true, true) => bx.max().x,
|
||||
(true, false) => bx.min().x,
|
||||
(false, true) => bx.max().y,
|
||||
(false, false) => bx.min().y,
|
||||
};
|
||||
let h = (to - from) / PIECES as f64;
|
||||
let mut length = 0.0;
|
||||
for piece in 0..PIECES {
|
||||
let c = from + (piece as f64 + 0.5) * h;
|
||||
for i in 0..16 {
|
||||
let (x, w) = if i < 8 { (-X[i], W[i]) } else { (X[i - 8], W[i - 8]) };
|
||||
let t = c + 0.5 * h * x;
|
||||
let uv = if s.u { Point2::new(t, mid.y) } else { Point2::new(mid.x, t) };
|
||||
let Some((_, du, dv)) = eval(k, ext, uv) else { return -1.0 };
|
||||
length += 0.5 * h.abs() * w * if s.u { du } else { dv }.length();
|
||||
}
|
||||
}
|
||||
length
|
||||
}
|
||||
|
||||
/// `cone_continued`: a cone's or cylinder's face continued naturally `distance` past side `s` of
|
||||
/// its box `dom`, as the analytic itself over a wider STEP domain (degrees round the axis,
|
||||
/// height along it). `None` where the cone can't be widened so (more than a turn, or past the
|
||||
/// apex).
|
||||
fn cone_continued(k: &Kernel, surf: &Surface, dom: &Extent2d, s: Side, distance: f64) -> Option<Surface> {
|
||||
let SurfaceKind::Cone(cone) = surf.kind() else { return None };
|
||||
let mut cone: Cone = cone.as_ref().clone();
|
||||
let step = |uv: Point2| cone.convert_uv_from_nurbs_to_step(k, uv).ok();
|
||||
let (a, z, m) = (step(on_side(dom, s, 0.0))?, step(on_side(dom, s, 1.0))?, step(on_side(dom, s, 0.5))?);
|
||||
let (lo, hi) = (step(dom.min())?, step(dom.max())?);
|
||||
let semi = cone.canonical().semi_angle_deg;
|
||||
let mut bx = [[lo.x.min(hi.x), lo.x.max(hi.x)], [lo.y.min(hi.y), lo.y.max(hi.y)]];
|
||||
let eps = 1e-9 * (1.0 + m.x.abs() + m.y.abs());
|
||||
// The STEP coordinate constant along the side is the one the face grows in.
|
||||
let c = if (a.x - z.x).abs() <= eps {
|
||||
0
|
||||
} else if (a.y - z.y).abs() <= eps {
|
||||
1
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let at = if c == 0 { m.x } else { m.y };
|
||||
let row = bx.get_mut(c)?;
|
||||
let end = if (at - row[1]).abs() <= (at - row[0]).abs() { 1 } else { 0 };
|
||||
let by = if c == 0 {
|
||||
let r = cone.radius(m.y);
|
||||
if !(r > 0.0) {
|
||||
return None;
|
||||
}
|
||||
(distance / r).to_degrees()
|
||||
} else {
|
||||
distance * semi.to_radians().cos()
|
||||
};
|
||||
if end == 1 {
|
||||
row[1] += by;
|
||||
} else {
|
||||
row[0] -= by;
|
||||
}
|
||||
if bx[0][1] - bx[0][0] > 360.0 + 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let wider = Extent2d::from_bounds(bx[0][0], bx[1][0], bx[0][1], bx[1][1]);
|
||||
cone.adjust_step_uv_domain(k, &wider).ok()?;
|
||||
Some(Surface::from(cone))
|
||||
}
|
||||
|
||||
/// `same_patch`: whether `p` and `q` span the same patch: their boxes' corners the same points,
|
||||
/// in any order, within `tol`.
|
||||
fn same_patch(k: &Kernel, p: &Surface, q: &Surface, tol: f64) -> bool {
|
||||
let (Ok(bp), Ok(bq)) = (p.natural_uv_domain_with_nurb(k), q.natural_uv_domain_with_nurb(k)) else { return false };
|
||||
let corners = |b: &Extent2d| {
|
||||
let (lo, hi) = (b.min(), b.max());
|
||||
[lo, Point2::new(hi.x, lo.y), hi, Point2::new(lo.x, hi.y)]
|
||||
};
|
||||
corners(&bp).into_iter().all(|u| {
|
||||
let Some(x) = point_at(k, p, u) else { return false };
|
||||
corners(&bq).into_iter().any(|v| point_at(k, q, v).is_some_and(|y| (x - y).length() <= tol))
|
||||
})
|
||||
}
|
||||
|
||||
/// `extended_by`: the surface `surf` (whose face is its whole box `dom`) extended past side `s` as
|
||||
/// `ct` says, so that it runs `distance` past that side measured across it ([`across`]), within
|
||||
/// 1e-9 of it. `CreateExtendedSurface` takes its distance as an estimate, and how far it then runs
|
||||
/// is increasing in what it is asked: a bracketed solve on that (each ask scaled by the distance
|
||||
/// over what it gave until two asks fall either side, then Illinois false position). An extension
|
||||
/// that moves the face's side off its parameters, or a target not met, is [`Error::NotExact`],
|
||||
/// never the surface as it came out.
|
||||
fn extended_by(k: &Kernel, surf: &Surface, dom: &Extent2d, s: Side, ct: ContinuityType, distance: f64, model_tol: f64) -> Result<Surface> {
|
||||
const BRACKET_TRIES: usize = 40;
|
||||
const SOLVE_TRIES: usize = 100;
|
||||
let tol = 1e-9 * distance;
|
||||
// How far asking `ask` runs, and the surface it makes.
|
||||
let try_ask = |ask: f64| -> Result<(Surface, f64)> {
|
||||
let tried = surf.create_extended_surface_dir(k, s.which, ask, ct).map_err(status)?;
|
||||
// The face's side stays where it was on the extended surface's parameters.
|
||||
let mid = on_side(dom, s, 0.5);
|
||||
let stays = matches!((point_at(k, surf, mid), point_at(k, &tried, mid)), (Some(was), Some(now)) if (was - now).length() <= model_tol);
|
||||
if !stays {
|
||||
return Err(Error::NotExact);
|
||||
}
|
||||
let got = across(k, &tried, dom, s);
|
||||
if got.is_finite() && got > 0.0 { Ok((tried, got)) } else { Err(Error::NotExact) }
|
||||
};
|
||||
// Asks that run short of and past `distance`, by f = got - distance.
|
||||
let (mut short, mut past) = (None, None);
|
||||
let mut ask = distance;
|
||||
for _ in 0..BRACKET_TRIES {
|
||||
let (made, got) = try_ask(ask)?;
|
||||
let f = got - distance;
|
||||
if f.abs() <= tol {
|
||||
return Ok(made);
|
||||
}
|
||||
if f < 0.0 {
|
||||
short = Some((ask, f));
|
||||
} else {
|
||||
past = Some((ask, f));
|
||||
}
|
||||
if short.is_some() && past.is_some() {
|
||||
break;
|
||||
}
|
||||
ask *= distance / got;
|
||||
}
|
||||
let (Some((mut lo, mut flo)), Some((mut hi, mut fhi))) = (short, past) else { return Err(Error::NotExact) };
|
||||
// Illinois: false position, halving the value at the end that stays when the same end moves
|
||||
// twice running.
|
||||
#[derive(PartialEq)]
|
||||
enum Moved {
|
||||
Neither,
|
||||
Lo,
|
||||
Hi,
|
||||
}
|
||||
let mut moved = Moved::Neither;
|
||||
for _ in 0..SOLVE_TRIES {
|
||||
let x = hi - fhi * (hi - lo) / (fhi - flo);
|
||||
if !(x > lo.min(hi) && x < lo.max(hi)) {
|
||||
break;
|
||||
}
|
||||
let (made, got) = try_ask(x)?;
|
||||
let f = got - distance;
|
||||
if f.abs() <= tol {
|
||||
return Ok(made);
|
||||
}
|
||||
if f < 0.0 {
|
||||
(lo, flo) = (x, f);
|
||||
if moved == Moved::Lo {
|
||||
fhi *= 0.5;
|
||||
}
|
||||
moved = Moved::Lo;
|
||||
} else {
|
||||
(hi, fhi) = (x, f);
|
||||
if moved == Moved::Hi {
|
||||
flo *= 0.5;
|
||||
}
|
||||
moved = Moved::Hi;
|
||||
}
|
||||
}
|
||||
Err(Error::NotExact)
|
||||
}
|
||||
|
||||
/// `smlib_extend_sheet`: the one-face sheet `sheet`, whose face is its surface's whole parameter
|
||||
/// box, continued `distance` past the side its edge nearest `point` (within `tol`) runs along,
|
||||
/// as `how` says. A sheet of another shape is [`Error::Unsupported`]; an extension that cannot be
|
||||
/// made `distance` long across the side (within 1e-9 of it) is [`Error::NotExact`].
|
||||
pub(crate) fn extend_sheet(sheet: &Body, point: [f64; 3], tol: f64, distance: f64, how: Continuation) -> Result<Body> {
|
||||
if !point.iter().all(|x| x.is_finite()) || !tol.is_finite() || !(tol > 0.0) || !distance.is_finite() || !(distance > 0.0) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if sheet.0.is_poisoned() {
|
||||
return Err(Error::Internal("smlib_extend_sheet: the body was left unfinished by an earlier panic".to_owned()));
|
||||
}
|
||||
run("smlib_extend_sheet", |k| {
|
||||
let b = {
|
||||
let g = sheet.0.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
super::dump::input(k, "sheet", &g);
|
||||
brep_copy(k, &g)?
|
||||
};
|
||||
let faces = brep_p1::faces(&b, None)?;
|
||||
let [f] = faces.as_slice() else { return Ok(Err(Error::Unsupported)) };
|
||||
let f = *f;
|
||||
let Some(surf) = face_p2::surface(&b, f)?.cloned() else { return Ok(Err(Error::Unsupported)) };
|
||||
if face_p2::loops(&b, f, false, None)?.len() != 1 {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
let edges = face_p4::edges(&b, f, None, false, None)?;
|
||||
let (d, picked) = nearest_edge(k, &b, &edges, p3(point));
|
||||
let Some(picked) = picked.filter(|_| d <= tol) else { return Ok(Err(Error::NoMatch { point: 0 })) };
|
||||
let model_tol = b.tolerance().max(1e-9);
|
||||
let on = 10.0 * model_tol;
|
||||
let dom = face_p2::uv_domain(&b, f)?;
|
||||
// Every side of the box is a side of the face (the face is its whole box), and the picked
|
||||
// edge is all of one side.
|
||||
let mut grow = None;
|
||||
for s in SIDES {
|
||||
let (boundary, collapsed) = is_boundary(k, &b, &surf, &dom, s, &edges, on);
|
||||
if !boundary {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
if collapsed {
|
||||
continue;
|
||||
}
|
||||
let all = (0..=SAMPLES).all(|i| match point_at(k, &surf, on_side(&dom, s, i as f64 / SAMPLES as f64)) {
|
||||
Some(p) => edge_distance(k, &b, picked, p) <= on,
|
||||
None => false,
|
||||
});
|
||||
if all {
|
||||
grow = Some(s);
|
||||
}
|
||||
}
|
||||
let Some(grow) = grow else { return Ok(Err(Error::Unsupported)) };
|
||||
// SMLib extends the whole surface; the face must be all of it.
|
||||
let natural = surf.natural_uv_domain_with_nurb(k)?;
|
||||
let (nl, nh, dl, dh) = (natural.min(), natural.max(), dom.min(), dom.max());
|
||||
let ptol = 1e-9 * 1f64.max((nh.x - nl.x).max(nh.y - nl.y));
|
||||
if (nl.x - dl.x).abs() > ptol || (nl.y - dl.y).abs() > ptol || (nh.x - dh.x).abs() > ptol || (nh.y - dh.y).abs() > ptol {
|
||||
return Ok(Err(Error::Unsupported));
|
||||
}
|
||||
let ct = match how {
|
||||
Continuation::Tangent => ContinuityType::G1,
|
||||
Continuation::Curvature => ContinuityType::G1G2,
|
||||
Continuation::Natural => ContinuityType::CInfinity,
|
||||
};
|
||||
// CreateExtendedSurface takes its distance as an estimate (a natural parabola falls 2.5%
|
||||
// short, a G2 run-off of a circle runs more than twice as far): the distance asked of it
|
||||
// is solved for until the extension measures `distance` across the side's middle.
|
||||
let extended = match extended_by(k, &surf, &dom, grow, ct, distance, model_tol) {
|
||||
Ok(e) => e,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
// A cone or cylinder continued naturally stays one, as the analytic widened, else SMLib
|
||||
// gives it back its analytic kind where it can.
|
||||
let grown = match how {
|
||||
Continuation::Natural => cone_continued(k, &surf, &dom, grow, distance).filter(|g| same_patch(k, g, &extended, 10.0 * model_tol)),
|
||||
_ => None,
|
||||
};
|
||||
let grown = match grown {
|
||||
Some(g) => g,
|
||||
None => match extended.copy_and_add_analytics(k) {
|
||||
Ok(g) => g,
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
},
|
||||
};
|
||||
let bx = grown.natural_uv_domain_with_nurb(k)?;
|
||||
let mut r = new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
r.set_tolerance(b.tolerance());
|
||||
let grown_eval = grown.copy(k)?;
|
||||
let nf = match build_faces(&mut r, |t, fenv| create_face_from_surface(k, t, Some(grown), &bx, fenv)) {
|
||||
Ok(nf) => nf,
|
||||
Err(e) => return Ok(Err(status(e))),
|
||||
};
|
||||
r.refresh_cached_topology();
|
||||
// The face and each side stand for the face and side they continue.
|
||||
copy_attributes(&b, EntityRef::Face(f), &mut r, EntityRef::Face(nf))?;
|
||||
let made = face_p4::edges(&r, nf, None, false, None)?;
|
||||
let new_on = 10.0 * r.tolerance().max(1e-9);
|
||||
for s in SIDES {
|
||||
let from = side_edge(k, &b, &surf, &dom, s, &edges, on);
|
||||
let to = side_edge(k, &r, &grown_eval, &bx, s, &made, new_on);
|
||||
if let (Some(from), Some(to)) = (from, to) {
|
||||
copy_attributes(&b, EntityRef::Edge(from), &mut r, EntityRef::Edge(to))?;
|
||||
}
|
||||
}
|
||||
// The new face faces the way the old one did.
|
||||
if let (Some(was), Some(now)) = (upward(&b, f)?, upward(&r, nf)?)
|
||||
&& was != now
|
||||
&& let Err(e) = face_p1::flip_face_orientation(&mut r, nf)
|
||||
{
|
||||
return Ok(Err(status(e)));
|
||||
}
|
||||
Ok(Ok(r))
|
||||
})?
|
||||
.map(Body::new)
|
||||
}
|
||||
|
||||
/// A point inside each face, in the body's face order (`smlib_brep_face_points`:
|
||||
/// `SmFace::CalculateAnInternalPoint`).
|
||||
pub(crate) fn face_points(body: &Body) -> Result<Vec<[f64; 3]>> {
|
||||
super::with_body("smlib_brep_face_points", body, |k, b| {
|
||||
let faces = b.face_list().to_vec();
|
||||
faces.into_iter().map(|f| face_internal_point(k, b, f).map(|p| [p.x, p.y, p.z])).collect()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
//! 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
|
||||
//! its surface's whole parameter box: a 10 mm square, or a quarter of a cylinder of radius 10 and
|
||||
//! height 10.
|
||||
|
||||
use std::f64::consts::{FRAC_1_SQRT_2, PI};
|
||||
|
||||
use super::trim::{extend_sheet, untrim_face};
|
||||
use super::{Body, area, counts, make_box, make_cylinder, to_arrays};
|
||||
use crate::export::native;
|
||||
use crate::trim::Continuation;
|
||||
use crate::{BooleanOp, Error};
|
||||
|
||||
const TOL: f64 = 0.01;
|
||||
/// A point on the top face, away from the hole.
|
||||
const TOP: [f64; 3] = [2.0, 2.0, 5.0];
|
||||
/// A point on the hole's rim on the top face.
|
||||
const RIM: [f64; 3] = [13.0, 10.0, 5.0];
|
||||
|
||||
/// The plate's top as a sheet of its own: a 20 mm square at z = 0 with a 3 mm hole at (10, 10).
|
||||
fn holed_sheet() -> Body {
|
||||
use crate::sweeps::Curve;
|
||||
let line = |a: [f64; 3], b: [f64; 3]| Curve::Line { start: a, end: b };
|
||||
let (a, b, c, d) = ([0.0, 0.0, 0.0], [20.0, 0.0, 0.0], [20.0, 20.0, 0.0], [0.0, 20.0, 0.0]);
|
||||
let hole = Curve::Arc { center: [10.0, 10.0, 0.0], axis: [0.0, 0.0, 1.0], reference: [1.0, 0.0, 0.0], radius: 3.0, start: 0.0, end: 2.0 * PI };
|
||||
super::sweep::planar_faces(&[line(a, b), line(b, c), line(c, d), line(d, a), hole]).unwrap()
|
||||
}
|
||||
|
||||
/// On the sheet: a point away from the hole, and one on the hole's rim.
|
||||
const SHEET: [f64; 3] = [2.0, 2.0, 0.0];
|
||||
const SHEET_RIM: [f64; 3] = [13.0, 10.0, 0.0];
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Input {
|
||||
Plate,
|
||||
Sheet,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
fn core(self) -> Body {
|
||||
match self {
|
||||
Input::Plate => plate(),
|
||||
Input::Sheet => holed_sheet(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plate() -> Body {
|
||||
let block = make_box([0.0; 3], [20.0, 20.0, 5.0]).unwrap();
|
||||
let pin = make_cylinder([10.0, 10.0, -1.0], 3.0, 7.0).unwrap();
|
||||
super::boolean::boolean(&block, &pin, BooleanOp::Subtract).unwrap().unwrap()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Case {
|
||||
input: Input,
|
||||
point: [f64; 3],
|
||||
/// `None`: every hole.
|
||||
holes: Option<&'static [[f64; 3]]>,
|
||||
outer: bool,
|
||||
in_place: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Expect {
|
||||
/// A sheet: its area and face count.
|
||||
Sheet(f64, usize),
|
||||
Fails(Error),
|
||||
}
|
||||
|
||||
fn case(name: &str) -> (Case, Expect) {
|
||||
let c = |input, point, holes, outer, in_place| Case { input, point, holes, outer, in_place };
|
||||
let square = 400.0;
|
||||
let holed = 400.0 - 9.0 * PI;
|
||||
let (plate, sheet) = (Input::Plate, Input::Sheet);
|
||||
match name {
|
||||
// The plate's top face, alone.
|
||||
"every_hole_taken_away" => (c(plate, TOP, None, false, false), Expect::Sheet(square, 1)),
|
||||
"the_hole_at_a_point_taken_away" => (c(plate, TOP, Some(&[RIM]), false, false), Expect::Sheet(square, 1)),
|
||||
// The outer boundary back on the plane's box of the face's span, the hole kept.
|
||||
"outer_boundary_back_hole_kept" => (c(plate, TOP, Some(&[]), true, false), Expect::Sheet(holed, 1)),
|
||||
"outer_boundary_back_holes_gone" => (c(plate, TOP, None, true, false), Expect::Sheet(square, 1)),
|
||||
// In the solid, the hole's edges are the bore's too: no hole of a sheet.
|
||||
"a_hole_of_a_solid_in_place" => (c(plate, TOP, None, false, true), Expect::Fails(Error::Unsupported)),
|
||||
"a_point_near_no_face" => (c(plate, [50.0, 50.0, 50.0], None, false, false), Expect::Fails(Error::NoMatch { point: 0 })),
|
||||
// The holed sheet, in place.
|
||||
"sheet_every_hole_taken_away" => (c(sheet, SHEET, None, false, true), Expect::Sheet(square, 1)),
|
||||
"sheet_the_hole_at_a_point_taken_away" => (c(sheet, SHEET, Some(&[SHEET_RIM]), false, true), Expect::Sheet(square, 1)),
|
||||
"sheet_outer_boundary_back_hole_kept" => (c(sheet, SHEET, Some(&[]), true, true), Expect::Sheet(holed, 1)),
|
||||
"sheet_outer_boundary_back_holes_gone" => (c(sheet, SHEET, None, true, true), Expect::Sheet(square, 1)),
|
||||
"sheet_a_point_near_no_hole" => (c(sheet, SHEET, Some(&[SHEET]), false, true), Expect::Fails(Error::NoMatch { point: 0 })),
|
||||
_ => panic!("no case {name}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Measured {
|
||||
area: f64,
|
||||
counts: [usize; 3],
|
||||
}
|
||||
|
||||
type Got = std::result::Result<Measured, Error>;
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs()
|
||||
}
|
||||
|
||||
fn judge(name: &str, who: &str, got: Got) -> Got {
|
||||
let (_, expect) = case(name);
|
||||
match (&expect, &got) {
|
||||
(Expect::Fails(e), Err(g)) => assert_eq!(g, e, "{name}: {who} failed otherwise"),
|
||||
(Expect::Fails(e), Ok(m)) => panic!("{name}: {who} gave {m:?}, expected {e:?}"),
|
||||
(_, Err(g)) => panic!("{name}: {who} failed: {g}"),
|
||||
(Expect::Sheet(a, faces), Ok(m)) => {
|
||||
assert!(rel(m.area, *a) < 1e-9, "{name}: {who} area {} vs analytic {a}", m.area);
|
||||
assert_eq!(m.counts[0], *faces, "{name}: {who} faces");
|
||||
}
|
||||
}
|
||||
got
|
||||
}
|
||||
|
||||
fn solidcore_case(name: &str) -> Got {
|
||||
let (c, _) = case(name);
|
||||
let got = untrim_face(&c.input.core(), c.point, TOL, c.holes, c.outer, c.in_place).map(|b| {
|
||||
let (rec, _) = native(&to_arrays(&b).unwrap()).unwrap_or_else(|e| panic!("{name}: record: {e:?}"));
|
||||
rec.validate().unwrap_or_else(|e| panic!("{name}: record: {e}"));
|
||||
Measured { area: area(&b, 1e-8).unwrap(), counts: counts(&b).unwrap() }
|
||||
});
|
||||
judge(name, "solidcore", got)
|
||||
}
|
||||
|
||||
macro_rules! cases {
|
||||
($($m:ident $(: $ign:meta)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
#[test]
|
||||
$(#[$ign])?
|
||||
fn solidcore_matches_the_closed_form() {
|
||||
let _ = super::solidcore_case(stringify!($m));
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
cases!(
|
||||
every_hole_taken_away,
|
||||
the_hole_at_a_point_taken_away,
|
||||
outer_boundary_back_hole_kept,
|
||||
outer_boundary_back_holes_gone,
|
||||
a_hole_of_a_solid_in_place,
|
||||
a_point_near_no_face,
|
||||
sheet_every_hole_taken_away,
|
||||
sheet_the_hole_at_a_point_taken_away,
|
||||
sheet_outer_boundary_back_hole_kept,
|
||||
sheet_outer_boundary_back_holes_gone,
|
||||
sheet_a_point_near_no_hole,
|
||||
);
|
||||
|
||||
/// The capi's argument checks, before any kernel call.
|
||||
#[test]
|
||||
fn hostile_arguments_are_refused() {
|
||||
let b = make_box([0.0; 3], [10.0; 3]).unwrap();
|
||||
let p = [5.0, 5.0, 10.0];
|
||||
assert_eq!(untrim_face(&b, [f64::NAN, 0.0, 0.0], TOL, None, true, false).err(), Some(Error::Argument));
|
||||
for t in [0.0, -1.0, f64::NAN, f64::INFINITY] {
|
||||
assert_eq!(untrim_face(&b, p, t, None, true, false).err(), Some(Error::Argument), "tolerance {t}");
|
||||
}
|
||||
assert_eq!(untrim_face(&b, p, TOL, Some(&[]), false, false).err(), Some(Error::Argument), "nothing to do");
|
||||
assert_eq!(untrim_face(&b, p, TOL, Some(&[[f64::INFINITY; 3]]), false, false).err(), Some(Error::Argument));
|
||||
let many = vec![p; 10_001];
|
||||
assert_eq!(untrim_face(&b, p, TOL, Some(&many), false, false).err(), Some(Error::Argument));
|
||||
}
|
||||
|
||||
/// The body untrimmed is never changed.
|
||||
#[test]
|
||||
fn the_body_stays_as_it_was() {
|
||||
let b = plate();
|
||||
let was = counts(&b).unwrap();
|
||||
let _ = untrim_face(&b, TOP, TOL, None, true, false);
|
||||
let _ = untrim_face(&b, TOP, TOL, None, false, true);
|
||||
assert_eq!(counts(&b).unwrap(), was);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
use solidcore::topology::brep::p8::create_face_from_surface;
|
||||
super::run("sheet", |k| {
|
||||
let mut b = new_brep(k, solidcore::geom::tol::USE_DEFAULT, solidcore::geom::tol::USE_DEFAULT)?;
|
||||
let domain = surface.natural_uv_domain_with_nurb(k)?;
|
||||
super::sweep::build_faces(&mut b, |b, fenv| create_face_from_surface(k, b, Some(surface), &domain, fenv))?;
|
||||
b.refresh_cached_topology();
|
||||
Ok(b)
|
||||
})
|
||||
.map(Body::new)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Sheet {
|
||||
/// [0, 10]^2 at z = 0.
|
||||
Square,
|
||||
/// Radius 10 about +Z, from 0 to 90 degrees, z from 0 to 10.
|
||||
QuarterCylinder,
|
||||
}
|
||||
|
||||
impl Sheet {
|
||||
fn core(self) -> Body {
|
||||
use solidcore::geom::extent2d::Extent2d;
|
||||
use solidcore::geom::vector2d::Vec2;
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::surface::cone::p1::Cone;
|
||||
use solidcore::surface::plane::p1::Plane;
|
||||
use solidcore::surface::surface::Surface;
|
||||
let (o, x, y) = (Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0));
|
||||
match self {
|
||||
Sheet::Square => whole_face(Surface::from(Plane::new(o, x, y, Vec2::new(1.0, 1.0), &Extent2d::from_bounds(0.0, 0.0, 10.0, 10.0)))),
|
||||
Sheet::QuarterCylinder => whole_face(Surface::from(Cone::new_cylinder_bounded(o, x, y, 10.0, 0.0, 90.0, 10.0, false, false, false))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_case(name: &str) -> (Sheet, [f64; 3], f64, Continuation, Expect) {
|
||||
let quarter = 5.0 * PI;
|
||||
match name {
|
||||
"extend_a_square" => (Sheet::Square, [10.0, 5.0, 0.0], 5.0, Continuation::Natural, Expect::Sheet(150.0, 1)),
|
||||
"extend_a_square_tangent" => (Sheet::Square, [5.0, 10.0, 0.0], 5.0, Continuation::Tangent, Expect::Sheet(150.0, 1)),
|
||||
"extend_a_square_curvature" => (Sheet::Square, [0.0, 5.0, 0.0], 2.0, Continuation::Curvature, Expect::Sheet(120.0, 1)),
|
||||
"extend_a_cylinder_along_its_axis" => {
|
||||
(Sheet::QuarterCylinder, [FRAC_1_SQRT_2 * 10.0, FRAC_1_SQRT_2 * 10.0, 10.0], 5.0, Continuation::Natural, Expect::Sheet(quarter * 15.0, 1))
|
||||
}
|
||||
"extend_a_cylinder_round_its_axis" => {
|
||||
(Sheet::QuarterCylinder, [10.0, 0.0, 5.0], 5.0, Continuation::Natural, Expect::Sheet(10.0 * (quarter + 5.0), 1))
|
||||
}
|
||||
// Run-offs 5 long across the surface whatever their curvature: the strip stays its section
|
||||
// swept 10 along z. A circle's G2 run-off runs about 2.3 times what it is asked and more
|
||||
// than doubles with it (fork 0f7b209), so the ask must be solved for.
|
||||
"extend_a_cylinder_tangent_run_off" => {
|
||||
(Sheet::QuarterCylinder, [10.0, 0.0, 5.0], 5.0, Continuation::Tangent, Expect::Sheet(10.0 * (quarter + 5.0), 1))
|
||||
}
|
||||
"extend_a_cylinder_curvature_run_off" => {
|
||||
(Sheet::QuarterCylinder, [10.0, 0.0, 5.0], 5.0, Continuation::Curvature, Expect::Sheet(10.0 * (quarter + 5.0), 1))
|
||||
}
|
||||
"extend_picks_near_no_edge" => (Sheet::Square, [50.0, 50.0, 0.0], 5.0, Continuation::Natural, Expect::Fails(Error::NoMatch { point: 0 })),
|
||||
_ => panic!("no case {name}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_judge(name: &str, who: &str, got: Got) -> Got {
|
||||
let (.., expect) = extend_case(name);
|
||||
match (&expect, &got) {
|
||||
(Expect::Fails(e), Err(g)) => assert_eq!(g, e, "{name}: {who} failed otherwise"),
|
||||
(Expect::Fails(e), Ok(m)) => panic!("{name}: {who} gave {m:?}, expected {e:?}"),
|
||||
(_, Err(g)) => panic!("{name}: {who} failed: {g}"),
|
||||
(Expect::Sheet(a, faces), Ok(m)) => {
|
||||
assert!(rel(m.area, *a) < 1e-6, "{name}: {who} area {} vs analytic {a}", m.area);
|
||||
assert_eq!(m.counts[0], *faces, "{name}: {who} faces");
|
||||
}
|
||||
}
|
||||
got
|
||||
}
|
||||
|
||||
fn extend_solidcore(name: &str) -> Got {
|
||||
let (sheet, point, distance, how, _) = extend_case(name);
|
||||
let got = extend_sheet(&sheet.core(), point, TOL, distance, how).map(|b| {
|
||||
let (rec, _) = native(&to_arrays(&b).unwrap()).unwrap_or_else(|e| panic!("{name}: record: {e:?}"));
|
||||
rec.validate().unwrap_or_else(|e| panic!("{name}: record: {e}"));
|
||||
Measured { area: area(&b, 1e-8).unwrap(), counts: counts(&b).unwrap() }
|
||||
});
|
||||
extend_judge(name, "solidcore", got)
|
||||
}
|
||||
|
||||
macro_rules! extend_cases {
|
||||
($($m:ident $(: $ign:meta)?),* $(,)?) => {$(
|
||||
mod $m {
|
||||
#[test]
|
||||
$(#[$ign])?
|
||||
fn solidcore_matches_the_closed_form() {
|
||||
let _ = super::extend_solidcore(stringify!($m));
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
extend_cases!(
|
||||
extend_a_square,
|
||||
extend_a_square_tangent,
|
||||
extend_a_square_curvature,
|
||||
extend_a_cylinder_along_its_axis,
|
||||
extend_a_cylinder_round_its_axis,
|
||||
extend_a_cylinder_tangent_run_off,
|
||||
extend_a_cylinder_curvature_run_off,
|
||||
extend_picks_near_no_edge,
|
||||
);
|
||||
|
||||
/// The capi's argument checks for an extend, before any kernel call, and a body of more than
|
||||
/// one face.
|
||||
#[test]
|
||||
fn hostile_extend_arguments_are_refused() {
|
||||
let s = Sheet::Square.core();
|
||||
let p = [10.0, 5.0, 0.0];
|
||||
assert_eq!(extend_sheet(&s, [f64::NAN; 3], TOL, 5.0, Continuation::Natural).err(), Some(Error::Argument));
|
||||
for t in [0.0, -1.0, f64::NAN, f64::INFINITY] {
|
||||
assert_eq!(extend_sheet(&s, p, t, 5.0, Continuation::Natural).err(), Some(Error::Argument), "tolerance {t}");
|
||||
assert_eq!(extend_sheet(&s, p, TOL, t, Continuation::Natural).err(), Some(Error::Argument), "distance {t}");
|
||||
}
|
||||
let cube = make_box([0.0; 3], [10.0; 3]).unwrap();
|
||||
assert_eq!(extend_sheet(&cube, p, TOL, 5.0, Continuation::Natural).err(), Some(Error::Unsupported));
|
||||
// A face that is not its surface's whole box: a square with a hole.
|
||||
assert_eq!(extend_sheet(&holed_sheet(), [20.0, 5.0, 0.0], TOL, 5.0, Continuation::Natural).err(), Some(Error::Unsupported));
|
||||
}
|
||||
+10
-107
@@ -45,45 +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> {
|
||||
init()?;
|
||||
ffi::make_cylinder(&origin, radius, height).map(Brep).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> {
|
||||
init()?;
|
||||
ffi::make_sphere(¢er, radius).map(Brep).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> {
|
||||
init()?;
|
||||
ffi::make_cone(&origin, base_radius, top_radius, height).map(Brep).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> {
|
||||
init()?;
|
||||
ffi::make_torus(¢er, major_radius, minor_radius).map(Brep).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 ([`Brep::export`]), copied out.
|
||||
/// The flat arrays of this body that the mapping reads.
|
||||
pub fn arrays(&self) -> Result<Arrays> {
|
||||
let f = self.export()?;
|
||||
let mut out = Arrays::default();
|
||||
for &id in I64_IDS {
|
||||
out.ints.insert(id, f.get_i64(id)?);
|
||||
}
|
||||
for &id in F64_IDS {
|
||||
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.
|
||||
@@ -150,92 +138,6 @@ fn unsupported(what: impl std::fmt::Display) -> ExportError {
|
||||
|
||||
// ---- The arrays (ids and kinds from `crate::flat`, as in capi/include/smlib.h) ------------
|
||||
|
||||
/// The int64 arrays this mapping reads.
|
||||
const I64_IDS: &[i32] = &[
|
||||
REGION_SHELL_COUNT,
|
||||
REGION_TYPE,
|
||||
SHELL_FACEUSE_COUNT,
|
||||
SHELL_WIRE_EDGE_COUNT,
|
||||
SHELL_POINT_TYPE,
|
||||
FACEUSE_FACE,
|
||||
FACEUSE_ORIENTATION,
|
||||
FACE_LOOP_COUNT,
|
||||
FACE_SURFACE_TYPE,
|
||||
LOOP_EDGEUSE_COUNT,
|
||||
LOOP_VERTEX,
|
||||
EDGEUSE_EDGE,
|
||||
EDGEUSE_ORIENTATION,
|
||||
EDGEUSE_NEXT_RADIAL,
|
||||
EDGEUSE_RADIAL_ENTRY,
|
||||
EDGEUSE_NURBS_VERTEX_COUNT,
|
||||
EDGEUSE_NURBS_ORDER,
|
||||
EDGE_CURVE_TYPE,
|
||||
EDGE_VERTICES,
|
||||
EDGE_NURBS_VERTEX_COUNT,
|
||||
EDGE_NURBS_ORDER,
|
||||
FACE_NURBS_U_VERTEX_COUNT,
|
||||
FACE_NURBS_V_VERTEX_COUNT,
|
||||
FACE_NURBS_U_ORDER,
|
||||
FACE_NURBS_V_ORDER,
|
||||
FACE_NURBS_FORM,
|
||||
EDGE_NURBS_FORM,
|
||||
FACE_DEFINITION_REAL_COUNT,
|
||||
];
|
||||
|
||||
/// The double arrays this mapping reads.
|
||||
const F64_IDS: &[i32] = &[
|
||||
TOLERANCE,
|
||||
EXTENT,
|
||||
FACE_RANGE,
|
||||
EDGE_RANGE,
|
||||
VERTEX_POSITION,
|
||||
EDGE_NURBS_CVS,
|
||||
EDGE_NURBS_KNOTS,
|
||||
EDGE_NURBS_WEIGHTS,
|
||||
EDGE_CIRCLE_CENTER,
|
||||
EDGE_CIRCLE_AXIS,
|
||||
EDGE_CIRCLE_REF_DIRECTION,
|
||||
EDGE_CIRCLE_RADIUS,
|
||||
EDGE_LINE_ORIGIN,
|
||||
EDGE_LINE_DIRECTION,
|
||||
EDGE_ELLIPSE_CENTER,
|
||||
EDGE_ELLIPSE_AXIS,
|
||||
EDGE_ELLIPSE_REF_DIRECTION,
|
||||
EDGE_ELLIPSE_X_RADIUS,
|
||||
EDGE_ELLIPSE_Y_RADIUS,
|
||||
EDGEUSE_NURBS_CVS,
|
||||
EDGEUSE_NURBS_KNOTS,
|
||||
EDGEUSE_NURBS_WEIGHTS,
|
||||
FACE_NURBS_CVS,
|
||||
FACE_NURBS_U_KNOTS,
|
||||
FACE_NURBS_V_KNOTS,
|
||||
FACE_NURBS_WEIGHTS,
|
||||
FACE_SPHERE_CENTER,
|
||||
FACE_SPHERE_AXIS,
|
||||
FACE_SPHERE_REF_DIRECTION,
|
||||
FACE_SPHERE_RADIUS,
|
||||
FACE_PLANE_ORIGIN,
|
||||
FACE_PLANE_AXIS,
|
||||
FACE_PLANE_REF_DIRECTION,
|
||||
FACE_CYLINDER_ORIGIN,
|
||||
FACE_CYLINDER_AXIS,
|
||||
FACE_CYLINDER_REF_DIRECTION,
|
||||
FACE_CYLINDER_RADIUS,
|
||||
FACE_CONE_ORIGIN,
|
||||
FACE_CONE_AXIS,
|
||||
FACE_CONE_REF_DIRECTION,
|
||||
FACE_CONE_RADIUS,
|
||||
FACE_CONE_SEMI_ANGLE,
|
||||
FACE_TORUS_ORIGIN,
|
||||
FACE_TORUS_AXIS,
|
||||
FACE_TORUS_REF_DIRECTION,
|
||||
FACE_TORUS_MAJOR_RADIUS,
|
||||
FACE_TORUS_MINOR_RADIUS,
|
||||
EDGE_TOLERANCE,
|
||||
VERTEX_TOLERANCE,
|
||||
FACE_DEFINITION_REALS,
|
||||
];
|
||||
|
||||
// The kind values as the arrays hold them (int64); a local item shadows the glob import.
|
||||
const SURFACE_NURBS: i64 = flat::SURFACE_NURBS as i64;
|
||||
const SURFACE_PLANE: i64 = flat::SURFACE_PLANE as i64;
|
||||
@@ -384,8 +286,9 @@ fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surfac
|
||||
v3(flat.f(FACE_CONE_REF_DIRECTION), i, "cone")?,
|
||||
);
|
||||
let (radius, half) = (at(flat.f(FACE_CONE_RADIUS), i, "cone")?, at(flat.f(FACE_CONE_SEMI_ANGLE), i, "cone")?);
|
||||
// SMLib's cone of semi-angle 0 is its cylinder.
|
||||
if half == 0.0 {
|
||||
// SMLib's cone of semi-angle 0 is its cylinder, and one within rounding of it (a thread's
|
||||
// cone of 1e-13 comes out of the trims) is too: the portable record refuses it as a cone.
|
||||
if half.abs() < 1e-12 {
|
||||
b::Surface::Cylinder { frame: f, radius }
|
||||
} else {
|
||||
b::Surface::Cone { frame: f, radius_at_origin: radius, half_angle: half }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#[cfg(feature = "native")]
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
mod native_tests {
|
||||
use super::*;
|
||||
use crate::{CheckLevel, MEASURE_ACCURACY};
|
||||
@@ -55,65 +53,46 @@ mod native_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A 20 mm cube with every edge rounded r 2: its eight corners are SMLib's ball patches
|
||||
/// (`SmBallPatch`, flat kind 6). The record keeps each as a ball patch on the sphere of
|
||||
/// radius 2 about the corner pulled in by 2 along each axis, its net the NURBS form, and
|
||||
/// every point of the net on that sphere. Imported back, the patches are kind 6 again with
|
||||
/// the same definitions, bit for bit, and the body keeps its closed-form volume.
|
||||
/// A 20 mm cube with every edge rounded r 2: each of its eight corners is a sphere of radius 2
|
||||
/// about the corner pulled in by 2 along each axis. The record keeps each corner on that
|
||||
/// sphere; imported back, the body is valid and keeps its closed-form volume.
|
||||
#[test]
|
||||
fn a_rounded_cubes_ball_patches_go_round_with_their_spheres() {
|
||||
fn a_rounded_cubes_corners_go_round_on_their_spheres() {
|
||||
let (a, r) = (20.0, 2.0);
|
||||
let h = a / 2.0;
|
||||
let mids: Vec<[f64; 3]> = [(0.0, 0.0), (a, 0.0), (0.0, a), (a, a)].iter().flat_map(|&(p, q)| [[h, p, q], [p, h, q], [p, q, h]]).collect();
|
||||
let f = Brep::make_box([0.0; 3], [a; 3]).unwrap().fillet(&mids, 0.1, r).unwrap();
|
||||
let record = f.to_native().unwrap();
|
||||
record.validate().unwrap();
|
||||
let patches: Vec<(&b::Face, [f64; 3], f64)> = record
|
||||
let corners: Vec<([f64; 3], f64)> = record
|
||||
.faces
|
||||
.iter()
|
||||
.filter_map(|face| match face.surface {
|
||||
b::Surface::BallPatch { center, radius } => Some((face, center, radius)),
|
||||
.filter_map(|face| match &face.surface {
|
||||
b::Surface::Sphere { frame, radius } => Some((frame.origin, *radius)),
|
||||
b::Surface::BallPatch { center, radius } => Some((*center, *radius)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(patches.len(), 8, "one ball patch per corner");
|
||||
let mut centers: Vec<[f64; 3]> = patches.iter().map(|p| p.1).collect();
|
||||
centers.sort_by(|p, q| p.partial_cmp(q).unwrap());
|
||||
assert_eq!(corners.len(), 8, "one sphere per corner");
|
||||
// Each corner's sphere is about the corner pulled in by r along each axis: one sphere each.
|
||||
let inset = [r, a - r];
|
||||
let want: Vec<[f64; 3]> = (0..8).map(|i| [inset[i >> 2], inset[(i >> 1) & 1], inset[i & 1]]).collect();
|
||||
for (got, want) in centers.iter().zip(&want) {
|
||||
assert!((0..3).all(|k| (got[k] - want[k]).abs() < 1e-9), "centre {got:?}, the corner's {want:?}");
|
||||
for i in 0..8 {
|
||||
let want = [inset[i >> 2], inset[(i >> 1) & 1], inset[i & 1]];
|
||||
let at = corners.iter().filter(|(c, _)| (0..3).all(|k| (c[k] - want[k]).abs() < 1e-9)).count();
|
||||
assert_eq!(at, 1, "spheres about {want:?}: {corners:?}");
|
||||
}
|
||||
for (face, c, radius) in &patches {
|
||||
for (_, radius) in &corners {
|
||||
assert!((radius - r).abs() < 1e-12, "radius {radius}");
|
||||
assert!(matches!(face.nurbs, Some(b::Surface::RationalSpline { .. })), "a ball patch's form is its rational net");
|
||||
let eval = b::eval::SurfaceEval::new(face.parameters()).unwrap();
|
||||
let [[u0, u1], [v0, v1]] = face.domain;
|
||||
for (s, t) in [(0.25, 0.25), (0.5, 0.5), (0.75, 0.3), (0.1, 0.9)] {
|
||||
let q = eval.point([u0 + s * (u1 - u0), v0 + t * (v1 - v0)]).unwrap();
|
||||
let d = ((q.x - c[0]).powi(2) + (q.y - c[1]).powi(2) + (q.z - c[2]).powi(2)).sqrt();
|
||||
assert!((d - r).abs() < 1e-12 * a, "a point of the patch {} off its sphere", d - r);
|
||||
}
|
||||
}
|
||||
let back = Brep::from_record(&record, true).unwrap();
|
||||
let check = back.check(CheckLevel::All).unwrap();
|
||||
assert!(check.is_valid(), "{check}");
|
||||
let arrays = back.arrays().unwrap();
|
||||
let kinds = arrays.ints.get(&crate::flat::FACE_SURFACE_TYPE).cloned().unwrap_or_default();
|
||||
assert_eq!(kinds.iter().filter(|k| **k == i64::from(crate::flat::SURFACE_BALL_PATCH)).count(), 8);
|
||||
// The record numbers faces in canonical order, not SMLib's: the same definitions, each bit
|
||||
// for bit, in another order.
|
||||
let defs = |b: &Brep| {
|
||||
let reals = b.arrays().unwrap().reals.get(&crate::flat::FACE_DEFINITION_REALS).cloned().unwrap_or_default();
|
||||
let mut d: Vec<[u64; 4]> = reals.chunks(4).map(|c| [0, 1, 2, 3].map(|k| c[k].to_bits())).collect();
|
||||
d.sort_unstable();
|
||||
d
|
||||
};
|
||||
assert_eq!(defs(&back), defs(&f), "the definitions come back bit for bit");
|
||||
let b = a - 2.0 * r;
|
||||
let want = b.powi(3) + 6.0 * b * b * r + 3.0 * PI * r * r * b + 4.0 / 3.0 * PI * r.powi(3);
|
||||
let got = back.mass_properties(MEASURE_ACCURACY).unwrap().volume;
|
||||
assert!((got - want).abs() < 1e-6 * want, "volume {got} vs {want}");
|
||||
for (name, body) in [("as built", &f), ("through the record", &back)] {
|
||||
let got = body.mass_properties(MEASURE_ACCURACY).unwrap().volume;
|
||||
assert!((got - want).abs() < 1e-6 * want, "{name}: volume {got} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A tagged body's own history, renumbered with its export's order on both sides, is the
|
||||
@@ -174,6 +153,16 @@ mod native_tests {
|
||||
assert!((half.unwrap() + 0.5f64.atan()).abs() < 1e-12);
|
||||
}
|
||||
|
||||
/// A cone whose half angle is within rounding of zero (a52's thread bosses: 1e-13) is exported as
|
||||
/// a cylinder; as a cone the portable record's validation refuses it.
|
||||
#[test]
|
||||
fn a_cone_of_rounding_angle_is_exported_as_a_cylinder() {
|
||||
let near = Brep::make_cone([0.0; 3], 4.0, 4.0 - 1e-12, 10.0).unwrap().to_native().unwrap();
|
||||
assert!(near.faces.iter().any(|f| matches!(f.surface, b::Surface::Cylinder { .. })));
|
||||
assert!(!near.faces.iter().any(|f| matches!(f.surface, b::Surface::Cone { .. })));
|
||||
near.validate().unwrap();
|
||||
}
|
||||
|
||||
/// A hole tangent to the block's outline from inside: the material touches itself along the
|
||||
/// tangent line, where SMLib's one shell uses the edge four times. The record has two copies
|
||||
/// of that edge, each bounding one wedge of material (a face of the side and the half of the
|
||||
|
||||
@@ -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;
|
||||
@@ -39,6 +39,7 @@ pub const FACE_NURBS_V_ORDER: i32 = 31;
|
||||
pub const SOURCE: i32 = 32;
|
||||
pub const FACE_NURBS_FORM: i32 = 33;
|
||||
pub const EDGE_NURBS_FORM: i32 = 34;
|
||||
pub const WIRE_EDGE_NURBS_FORM: i32 = 35;
|
||||
/// Per face whose kind carries a definition (a ball patch), in face order: how many reals of
|
||||
/// `FACE_DEFINITION_REALS` are its definition (a ball patch: 4).
|
||||
pub const FACE_DEFINITION_REAL_COUNT: i32 = 36;
|
||||
@@ -128,62 +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).map_err(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl Brep {
|
||||
/// This body as flat arrays.
|
||||
pub fn export(&self) -> Result<Flat> {
|
||||
ffi::flat::export(&self.0).map(Flat).map_err(error)
|
||||
}
|
||||
|
||||
/// Whether SMLib finds this body one closed, manifold solid.
|
||||
pub fn is_manifold_solid(&self) -> Result<bool> {
|
||||
ffi::flat::is_manifold_solid(&self.0).map_err(error)
|
||||
crate::core::is_manifold_solid(self.core())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, error, ffi};
|
||||
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.
|
||||
@@ -68,53 +68,17 @@ impl Brep {
|
||||
/// SMLib's order, which the copy keeps). Run an operation on tagged copies, then read its
|
||||
/// result's [`Brep::history`].
|
||||
pub fn tagged(&self, operand: usize) -> Result<Brep> {
|
||||
let operand = i32::try_from(operand).map_err(|_| Error::Argument)?;
|
||||
ffi::history::tag(&self.0, operand).map(Brep).map_err(error)
|
||||
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.
|
||||
pub fn history(&self) -> Result<Raw> {
|
||||
let v = ffi::history::read(&self.0).map_err(error)?;
|
||||
Raw::decode(&v).ok_or(Error::Exception)
|
||||
core::history::read(self.core())
|
||||
}
|
||||
}
|
||||
|
||||
impl Raw {
|
||||
/// 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
|
||||
@@ -258,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,15 +666,20 @@ mod analytic;
|
||||
mod tests;
|
||||
|
||||
impl crate::Brep {
|
||||
/// A body from flat arrays.
|
||||
pub(crate) fn from_arrays(a: &Arrays) -> crate::Result<crate::Brep> {
|
||||
crate::core::from_arrays(a).map(crate::Brep::made_core)
|
||||
}
|
||||
|
||||
/// The SMLib B-rep of a schema 4 record. `trusted` as in [`arrays`].
|
||||
pub fn from_record(brep: &b::Brep, trusted: bool) -> crate::Result<crate::Brep> {
|
||||
let a = arrays(brep, trusted).map_err(crate::Error::Record)?;
|
||||
crate::flat::Flat::from_arrays(&a)?.import()
|
||||
crate::Brep::from_arrays(&a)
|
||||
}
|
||||
|
||||
/// [`crate::Brep::from_record`] through [`arrays_keeping_splines`].
|
||||
pub fn from_record_keeping_splines(brep: &b::Brep, trusted: bool) -> crate::Result<crate::Brep> {
|
||||
let a = arrays_keeping_splines(brep, trusted).map_err(crate::Error::Record)?;
|
||||
crate::flat::Flat::from_arrays(&a)?.import()
|
||||
crate::Brep::from_arrays(&a)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+44
-121
@@ -1,23 +1,15 @@
|
||||
//! 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`).
|
||||
//!
|
||||
//! Built without the `native` feature, the crate needs no C++ toolchain and every call returns
|
||||
//! [`Error::NotBuilt`].
|
||||
//! 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::Once;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
mod check;
|
||||
mod core;
|
||||
pub mod export;
|
||||
mod ffi;
|
||||
pub mod flat;
|
||||
pub mod history;
|
||||
pub mod import;
|
||||
@@ -32,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)]
|
||||
@@ -71,14 +63,18 @@ 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),
|
||||
/// This build has no SMLib (the `native` feature is off).
|
||||
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 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),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
@@ -113,9 +109,10 @@ impl std::fmt::Display for Error {
|
||||
}
|
||||
}
|
||||
Error::Exception => write!(f, "SMLib raised an internal exception"),
|
||||
Error::Version(v) => write!(f, "SMLib C API version {v}, expected {}", ffi::ABI_VERSION),
|
||||
Error::NotBuilt => write!(f, "not supported yet: this build has no SMLib"),
|
||||
Error::Record(u) => write!(f, "{u}"),
|
||||
Error::Solidcore(e) => write!(f, "solidcore: {e}"),
|
||||
Error::Internal(e) => write!(f, "solidcore failed internally in {e}"),
|
||||
Error::Unported(what) => write!(f, "not supported yet: {what}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,101 +121,20 @@ impl std::error::Error for Error {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
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),
|
||||
}
|
||||
/// 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));
|
||||
}
|
||||
|
||||
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<()> {
|
||||
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 (see the C API's `smlib_set_dump_dir`);
|
||||
/// `None` turns that off.
|
||||
pub fn set_dump_dir(dir: Option<&Path>) -> Result<()> {
|
||||
init()?;
|
||||
let c = dir.map(c_path).transpose()?;
|
||||
check(ffi::set_dump_dir(c.as_deref()))
|
||||
}
|
||||
|
||||
/// The SMLib call running right now, 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> {
|
||||
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()
|
||||
}
|
||||
|
||||
/// An SMLib B-rep. Immutable: operations return new bodies.
|
||||
pub struct Brep(ffi::Handle);
|
||||
/// A B-rep. Immutable: operations return new bodies.
|
||||
pub struct Brep {
|
||||
body: core::Body,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Brep {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@@ -227,26 +143,34 @@ impl std::fmt::Debug for Brep {
|
||||
}
|
||||
|
||||
impl Brep {
|
||||
/// A body solidcore made.
|
||||
fn made_core(body: core::Body) -> Brep {
|
||||
Brep { body }
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
init()?;
|
||||
ffi::make_box(&origin, &size).map(Brep).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> {
|
||||
init()?;
|
||||
ffi::read(&c_path(path)?, ascii).map(Brep).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<()> {
|
||||
check(ffi::write(&self.0, &c_path(path)?, ascii))
|
||||
core::file::write(self.core(), path, ascii)
|
||||
}
|
||||
|
||||
/// A deep copy.
|
||||
pub fn try_clone(&self) -> Result<Brep> {
|
||||
ffi::copy(&self.0).map(Brep).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
|
||||
@@ -254,18 +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> {
|
||||
ffi::transform(&self.0, matrix).map(Brep).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> {
|
||||
ffi::volume(&self.0, relative_accuracy).map_err(error)
|
||||
core::volume(self.core(), relative_accuracy)
|
||||
}
|
||||
|
||||
/// Face, edge and vertex counts.
|
||||
pub fn counts(&self) -> Result<[usize; 3]> {
|
||||
let c = ffi::counts(&self.0).map_err(error)?;
|
||||
Ok(c.map(|n| usize::try_from(n).unwrap_or(0)))
|
||||
core::counts(self.core())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+112
-70
@@ -2,7 +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, error, ffi};
|
||||
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.
|
||||
@@ -50,13 +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> {
|
||||
match ffi::measure::classify_point(&self.0, 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
|
||||
@@ -64,15 +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> {
|
||||
let m = ffi::measure::mass_properties(&self.0, 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])> {
|
||||
ffi::measure::bounds(&self.0).map_err(error)
|
||||
core::bounds(self.core())
|
||||
}
|
||||
|
||||
/// How far edge `edge` (its curve over its interval) lies from the surface of face `face`,
|
||||
@@ -82,22 +74,113 @@ 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> {
|
||||
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.0, 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> {
|
||||
ffi::measure::area(&self.0, relative_accuracy).map_err(error)
|
||||
core::area(self.core(), relative_accuracy)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "native"))]
|
||||
/// The closed forms of the primitives' measures, for the tests.
|
||||
#[cfg(test)]
|
||||
pub(crate) mod closed {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
/// Area, volume, centroid, Ixx, Iyy, Izz about it (products are 0 for all), and the exact box.
|
||||
pub(crate) struct Closed {
|
||||
pub area: f64,
|
||||
pub volume: f64,
|
||||
pub centroid: [f64; 3],
|
||||
pub moments: [f64; 3],
|
||||
pub bounds: ([f64; 3], [f64; 3]),
|
||||
}
|
||||
|
||||
/// An axis-aligned box from its lower corner.
|
||||
pub(crate) fn cuboid(o: [f64; 3], size: [f64; 3]) -> Closed {
|
||||
let [a, b, c] = size;
|
||||
let v = a * b * c;
|
||||
Closed {
|
||||
area: 2.0 * (a * b + b * c + c * a),
|
||||
volume: v,
|
||||
centroid: [o[0] + a / 2.0, o[1] + b / 2.0, o[2] + c / 2.0],
|
||||
moments: [v * (b * b + c * c) / 12.0, v * (a * a + c * c) / 12.0, v * (a * a + b * b) / 12.0],
|
||||
bounds: (o, [o[0] + a, o[1] + b, o[2] + c]),
|
||||
}
|
||||
}
|
||||
|
||||
/// A cylinder about +Z on its base centre.
|
||||
pub(crate) fn cylinder(o: [f64; 3], r: f64, h: f64) -> Closed {
|
||||
let v = PI * r * r * h;
|
||||
let ixx = v * (3.0 * r * r + h * h) / 12.0;
|
||||
Closed {
|
||||
area: 2.0 * PI * r * r + 2.0 * PI * r * h,
|
||||
volume: v,
|
||||
centroid: [o[0], o[1], o[2] + h / 2.0],
|
||||
moments: [ixx, ixx, v * r * r / 2.0],
|
||||
bounds: ([o[0] - r, o[1] - r, o[2]], [o[0] + r, o[1] + r, o[2] + h]),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn sphere(o: [f64; 3], r: f64) -> Closed {
|
||||
let v = 4.0 / 3.0 * PI * r * r * r;
|
||||
let i = 0.4 * v * r * r;
|
||||
Closed {
|
||||
area: 4.0 * PI * r * r,
|
||||
volume: v,
|
||||
centroid: o,
|
||||
moments: [i; 3],
|
||||
bounds: ([o[0] - r, o[1] - r, o[2] - r], [o[0] + r, o[1] + r, o[2] + r]),
|
||||
}
|
||||
}
|
||||
|
||||
/// A frustum (a cone when `rt` is 0) about +Z on its base centre. Its volume integrals are
|
||||
/// polynomials in z of degree at most 4, so three-point Gauss-Legendre gives them exactly.
|
||||
pub(crate) fn cone(o: [f64; 3], rb: f64, rt: f64, h: f64) -> Closed {
|
||||
let (x, w) = ([-(0.6f64).sqrt(), 0.0, (0.6f64).sqrt()], [5.0 / 9.0, 8.0 / 9.0, 5.0 / 9.0]);
|
||||
let (mut v, mut mz, mut zz, mut izz) = (0.0, 0.0, 0.0, 0.0);
|
||||
for (xi, wi) in x.iter().zip(w) {
|
||||
let (z, wi) = (h / 2.0 * (1.0 + xi), wi * h / 2.0);
|
||||
let rho = rb + (rt - rb) * z / h;
|
||||
let disc = PI * rho * rho;
|
||||
v += wi * disc;
|
||||
mz += wi * disc * z;
|
||||
zz += wi * disc * z * z;
|
||||
izz += wi * disc * rho * rho / 2.0;
|
||||
}
|
||||
let zc = mz / v;
|
||||
let ixx = izz / 2.0 + zz - v * zc * zc;
|
||||
let s = ((rb - rt).powi(2) + h * h).sqrt();
|
||||
let m = rb.max(rt);
|
||||
Closed {
|
||||
area: PI * (rb * rb + rt * rt + (rb + rt) * s),
|
||||
volume: v,
|
||||
centroid: [o[0], o[1], o[2] + zc],
|
||||
moments: [ixx, ixx, izz],
|
||||
bounds: ([o[0] - m, o[1] - m, o[2]], [o[0] + m, o[1] + m, o[2] + h]),
|
||||
}
|
||||
}
|
||||
|
||||
/// A ring torus about +Z (Pappus for area and volume).
|
||||
pub(crate) fn torus(o: [f64; 3], big: f64, r: f64) -> Closed {
|
||||
let v = 2.0 * PI * PI * big * r * r;
|
||||
let ixx = v * (big * big / 2.0 + 5.0 * r * r / 8.0);
|
||||
let m = big + r;
|
||||
Closed {
|
||||
area: 4.0 * PI * PI * big * r,
|
||||
volume: v,
|
||||
centroid: o,
|
||||
moments: [ixx, ixx, v * (big * big + 0.75 * r * r)],
|
||||
bounds: ([o[0] - m, o[1] - m, o[2] - r], [o[0] + m, o[1] + m, o[2] + r]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Error;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
if b == 0.0 { a.abs() } else { (a / b - 1.0).abs() }
|
||||
}
|
||||
@@ -152,7 +235,9 @@ mod tests {
|
||||
v
|
||||
}
|
||||
|
||||
/// Closed forms: area, volume, centroid and Ixx, Iyy, Izz about it (products are 0 for all).
|
||||
use closed::Closed;
|
||||
|
||||
/// A primitive with its closed forms.
|
||||
struct Expected {
|
||||
name: &'static str,
|
||||
body: Brep,
|
||||
@@ -163,67 +248,24 @@ mod tests {
|
||||
}
|
||||
|
||||
#[allow(clippy::unwrap_used)] // test fixture
|
||||
fn expected(name: &'static str, body: Result<Brep>, c: Closed) -> Expected {
|
||||
Expected { name, body: body.unwrap(), area: c.area, volume: c.volume, centroid: c.centroid, moments: c.moments }
|
||||
}
|
||||
|
||||
fn cylinder(name: &'static str, o: [f64; 3], r: f64, h: f64) -> Expected {
|
||||
let v = PI * r * r * h;
|
||||
let ixx = v * (3.0 * r * r + h * h) / 12.0;
|
||||
Expected {
|
||||
name,
|
||||
body: Brep::make_cylinder(o, r, h).unwrap(),
|
||||
area: 2.0 * PI * r * r + 2.0 * PI * r * h,
|
||||
volume: v,
|
||||
centroid: [o[0], o[1], o[2] + h / 2.0],
|
||||
moments: [ixx, ixx, v * r * r / 2.0],
|
||||
}
|
||||
expected(name, Brep::make_cylinder(o, r, h), closed::cylinder(o, r, h))
|
||||
}
|
||||
|
||||
#[allow(clippy::unwrap_used)] // test fixture
|
||||
fn sphere(name: &'static str, o: [f64; 3], r: f64) -> Expected {
|
||||
let v = 4.0 / 3.0 * PI * r * r * r;
|
||||
let i = 0.4 * v * r * r;
|
||||
Expected { name, body: Brep::make_sphere(o, r).unwrap(), area: 4.0 * PI * r * r, volume: v, centroid: o, moments: [i; 3] }
|
||||
expected(name, Brep::make_sphere(o, r), closed::sphere(o, r))
|
||||
}
|
||||
|
||||
/// A frustum (a cone when `rt` is 0) about +Z on its base centre. Its volume integrals are
|
||||
/// polynomials in z of degree at most 4, so three-point Gauss-Legendre gives them exactly.
|
||||
#[allow(clippy::unwrap_used)] // test fixture
|
||||
fn cone(name: &'static str, o: [f64; 3], rb: f64, rt: f64, h: f64) -> Expected {
|
||||
let (x, w) = ([-(0.6f64).sqrt(), 0.0, (0.6f64).sqrt()], [5.0 / 9.0, 8.0 / 9.0, 5.0 / 9.0]);
|
||||
let (mut v, mut mz, mut zz, mut izz) = (0.0, 0.0, 0.0, 0.0);
|
||||
for i in 0..3 {
|
||||
let (z, wi) = (h / 2.0 * (1.0 + x[i]), w[i] * h / 2.0);
|
||||
let rho = rb + (rt - rb) * z / h;
|
||||
let disc = PI * rho * rho;
|
||||
v += wi * disc;
|
||||
mz += wi * disc * z;
|
||||
zz += wi * disc * z * z;
|
||||
izz += wi * disc * rho * rho / 2.0;
|
||||
}
|
||||
let zc = mz / v;
|
||||
let ixx = izz / 2.0 + zz - v * zc * zc;
|
||||
let s = ((rb - rt).powi(2) + h * h).sqrt();
|
||||
Expected {
|
||||
name,
|
||||
body: Brep::make_cone(o, rb, rt, h).unwrap(),
|
||||
area: PI * (rb * rb + rt * rt + (rb + rt) * s),
|
||||
volume: v,
|
||||
centroid: [o[0], o[1], o[2] + zc],
|
||||
moments: [ixx, ixx, izz],
|
||||
}
|
||||
expected(name, Brep::make_cone(o, rb, rt, h), closed::cone(o, rb, rt, h))
|
||||
}
|
||||
|
||||
/// A ring torus about +Z (Pappus for area and volume).
|
||||
#[allow(clippy::unwrap_used)] // test fixture
|
||||
fn torus(name: &'static str, o: [f64; 3], big: f64, r: f64) -> Expected {
|
||||
let v = 2.0 * PI * PI * big * r * r;
|
||||
let ixx = v * (big * big / 2.0 + 5.0 * r * r / 8.0);
|
||||
Expected {
|
||||
name,
|
||||
body: Brep::make_torus(o, big, r).unwrap(),
|
||||
area: 4.0 * PI * PI * big * r,
|
||||
volume: v,
|
||||
centroid: o,
|
||||
moments: [ixx, ixx, v * (big * big + 0.75 * r * r)],
|
||||
}
|
||||
expected(name, Brep::make_torus(o, big, r), closed::torus(o, big, r))
|
||||
}
|
||||
|
||||
/// Every primitive against its closed form, at the origin and 2e5 mm out, to 1e-6 (tests/oracle
|
||||
|
||||
+38
-160
@@ -6,36 +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, error, ffi, init};
|
||||
|
||||
/// 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> {
|
||||
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)]
|
||||
@@ -70,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;
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -107,41 +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>> {
|
||||
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.0, &other.0, op) {
|
||||
Ok(h) => Ok(Some(Brep(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
|
||||
@@ -149,92 +77,62 @@ 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> {
|
||||
init()?;
|
||||
if breps.is_empty() || breps.len() > MAX_UNION_BREPS {
|
||||
return Err(error(ffi::ERR_ARG));
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let handles: Vec<&ffi::Handle> = breps.iter().map(|b| &b.0).collect();
|
||||
ffi::ops::boolean_union_all(&handles).map(Brep).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>> {
|
||||
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.0, &other.0, op) {
|
||||
Ok(h) => Ok(Some(Brep(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>> {
|
||||
init()?;
|
||||
match ffi::ops::imprint(&self.0, &tool.0) {
|
||||
Ok(h) => Ok(Some(Brep(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> {
|
||||
ffi::ops::solid_count(&self.0).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> {
|
||||
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> {
|
||||
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
|
||||
/// as `kind`, edge i at `sizes[i]`.
|
||||
pub fn blend(&self, kind: Blend, edges: &[[f64; 3]], sizes: &[f64], tolerance: f64) -> Result<Brep> {
|
||||
init()?;
|
||||
if edges.len() > MAX_POINTS || edges.is_empty() || sizes.len() != edges.len() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
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.0, edges, sizes, tolerance).map(Brep).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
|
||||
/// its faces, removed whole: that edge is gone and not rounded (SMLib rounds the edges whose
|
||||
/// blends overflow first, then what is left of the others).
|
||||
pub fn blend_dropping(&self, kind: Blend, edges: &[[f64; 3]], sizes: &[f64], tolerance: f64) -> Result<(Brep, Vec<bool>)> {
|
||||
init()?;
|
||||
if edges.len() > MAX_POINTS || edges.is_empty() || sizes.len() != edges.len() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
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.0, edges, sizes, tolerance).map(|(b, d)| (Brep(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
|
||||
@@ -251,7 +149,6 @@ impl Brep {
|
||||
setbacks: &[f64],
|
||||
tolerance: f64,
|
||||
) -> Result<Brep> {
|
||||
init()?;
|
||||
if edges.len() > MAX_POINTS
|
||||
|| edges.is_empty()
|
||||
|| sizes.len() != edges.len()
|
||||
@@ -261,13 +158,7 @@ impl Brep {
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
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.0, edges, sizes, corners, setbacks, tolerance).map(Brep).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
|
||||
@@ -284,7 +175,6 @@ impl Brep {
|
||||
setbacks: &[f64],
|
||||
tolerance: f64,
|
||||
) -> Result<(Brep, Vec<Hole>)> {
|
||||
init()?;
|
||||
if edges.len() > MAX_POINTS
|
||||
|| edges.is_empty()
|
||||
|| sizes.len() != edges.len()
|
||||
@@ -294,16 +184,8 @@ impl Brep {
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
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.0, 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(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
|
||||
@@ -311,13 +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> {
|
||||
init()?;
|
||||
let degree = i32::try_from(degree).map_err(|_| Error::Argument)?;
|
||||
ffi::ops::fillet_variable(&self.0, &edge, tolerance, &start, degree, knots, radii).map(Brep).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
|
||||
@@ -325,15 +201,10 @@ impl Brep {
|
||||
/// and by `second` on the other, with bevelled corners (setbacks measured at each edge's
|
||||
/// midpoint).
|
||||
pub fn chamfer_sides(&self, edges: &[[f64; 3]], tolerance: f64, distance: f64, second: ChamferSecond, first: [f64; 3]) -> Result<Brep> {
|
||||
init()?;
|
||||
if edges.is_empty() || edges.len() > MAX_POINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let (value, angle) = match second {
|
||||
ChamferSecond::Distance(d) => (d, false),
|
||||
ChamferSecond::Angle(a) => (a, true),
|
||||
};
|
||||
ffi::ops::chamfer_sides(&self.0, edges, tolerance, distance, value, angle, &first).map(Brep).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:
|
||||
@@ -342,7 +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> {
|
||||
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`
|
||||
@@ -350,15 +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> {
|
||||
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> {
|
||||
init()?;
|
||||
ffi::ops::thicken(&self.0, thickness, symmetric).map(Brep).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
|
||||
@@ -367,11 +237,10 @@ impl Brep {
|
||||
/// The edges nearest to `keep` (each within `tolerance`) stay. The shape is unchanged, and
|
||||
/// SMLib's history tags follow the merge.
|
||||
pub fn merge_faces(&self, keep: &[[f64; 3]], tolerance: f64) -> Result<Brep> {
|
||||
init()?;
|
||||
if keep.len() > MAX_POINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
ffi::ops::merge_faces(&self.0, keep, tolerance).map(Brep).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
|
||||
@@ -388,19 +257,28 @@ impl Brep {
|
||||
pull: [f64; 3],
|
||||
angle: f64,
|
||||
) -> Result<Brep> {
|
||||
init()?;
|
||||
if faces.is_empty() || faces.len() > MAX_POINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
ffi::ops::draft(&self.0, faces, tolerance, &neutral_origin, &neutral_normal, &pull, angle).map(Brep).map_err(pick_error)
|
||||
core::offset::draft(self.core(), faces, tolerance, neutral_origin, neutral_normal, pull, angle).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) {
|
||||
/// [`Brep::blend`] and its kin on solidcore (`core::blend::blend`).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn core_blend(
|
||||
&self,
|
||||
kind: Blend,
|
||||
edges: &[[f64; 3]],
|
||||
sizes: &[f64],
|
||||
tolerance: f64,
|
||||
corners: &[[f64; 3]],
|
||||
setbacks: &[f64],
|
||||
open: core::blend::Corners,
|
||||
) -> Result<Brep> {
|
||||
if edges.len() > MAX_POINTS || edges.is_empty() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
ffi::ops::pick_op(kind, &self.0, points, tolerance, size).map(Brep).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,19 +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();
|
||||
assert!(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];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user