mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 699eb837b5 |
@@ -442,6 +442,9 @@ 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
|
||||
|
||||
@@ -461,6 +464,9 @@ 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 }}
|
||||
@@ -504,6 +510,9 @@ 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:
|
||||
@@ -575,6 +584,9 @@ 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:
|
||||
@@ -691,6 +703,9 @@ 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:
|
||||
@@ -847,6 +862,9 @@ 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:
|
||||
@@ -905,6 +923,9 @@ 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:
|
||||
@@ -944,6 +965,9 @@ 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
|
||||
@@ -968,6 +992,9 @@ 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
|
||||
@@ -1105,6 +1132,9 @@ 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:
|
||||
@@ -1127,6 +1157,9 @@ 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.
|
||||
@@ -1147,6 +1180,8 @@ 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,6 +53,9 @@ 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,6 +29,9 @@ 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/ through one release build of
|
||||
# runs every case in tests/kernel-cases/fillets/ on SMLib, 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,6 +25,7 @@ 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]
|
||||
@@ -41,6 +42,7 @@ 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:
|
||||
|
||||
@@ -84,12 +86,15 @@ 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)
|
||||
run: cargo build --release --locked -p solidcraft-cli
|
||||
- name: Build the CLI (release, SMLib)
|
||||
run: cargo build --release --locked -p solidcraft-cli --features smlib
|
||||
- name: Run the fillet cases against the known-failing list
|
||||
run: >-
|
||||
cargo run -q --locked -p xtask -- kernel-cases --area fillets
|
||||
|
||||
@@ -48,6 +48,9 @@ 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
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# The kernel rig and the complexity ladder as a regression gate. A plan job maps a pull request's
|
||||
# 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, and when the solidcore pin in
|
||||
# the root Cargo.toml and Cargo.lock moves (the plan reads their diff); the ladder runs whenever any
|
||||
# area does. One job then builds solidcraft-cli once (release), checks that every
|
||||
# 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
|
||||
# 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.
|
||||
@@ -35,13 +34,11 @@ on:
|
||||
- "crates/doc/src/**"
|
||||
- "crates/engine/src/**"
|
||||
- "tests/kernel-cases/**"
|
||||
# The solidcore pin (the plan runs every area only when its lines change).
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
# The rig itself: the grading, the runner, the selection and this workflow.
|
||||
- "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"
|
||||
@@ -57,13 +54,11 @@ on:
|
||||
- "crates/doc/src/**"
|
||||
- "crates/engine/src/**"
|
||||
- "tests/kernel-cases/**"
|
||||
# The solidcore pin (the plan runs every area only when its lines change).
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
# The rig itself: the grading, the runner, the selection and this workflow.
|
||||
- "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"
|
||||
@@ -112,9 +107,7 @@ jobs:
|
||||
# gate and a run by hand run every area.
|
||||
if [ "$EVENT_NAME" = pull_request ]; then
|
||||
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/rig-paths"
|
||||
git diff --no-renames "$BASE_SHA...$HEAD_SHA" -- Cargo.toml Cargo.lock > "$RUNNER_TEMP/rig-pin.diff"
|
||||
python3 -I scripts/ci/kernel_rig_plan.py --paths "$RUNNER_TEMP/rig-paths" --pin-diff "$RUNNER_TEMP/rig-pin.diff" \
|
||||
--github-output "$GITHUB_OUTPUT"
|
||||
python3 -I scripts/ci/kernel_rig_plan.py --paths "$RUNNER_TEMP/rig-paths" --github-output "$GITHUB_OUTPUT"
|
||||
else
|
||||
python3 -I scripts/ci/kernel_rig_plan.py --all --github-output "$GITHUB_OUTPUT"
|
||||
fi
|
||||
@@ -148,13 +141,16 @@ 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) and the rig's runner
|
||||
- name: Build the CLI (release, SMLib) and the rig's runner
|
||||
run: |
|
||||
cargo build --release --locked -p solidcraft-cli
|
||||
cargo build --release --locked -p solidcraft-cli --features smlib
|
||||
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
|
||||
@@ -213,7 +209,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.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# The documentation site on GitHub Pages: the book (docs/book) at https://docs.solidcraft.io/
|
||||
# and the bundled web app at /app/. Needs Settings → Pages → Source = GitHub Actions; skipped while the
|
||||
# and the web app at /app/ (the latest release), /app-rc/ (the newest release candidate) and
|
||||
# /app-edge/ (this commit). Needs Settings → Pages → Source = GitHub Actions; skipped while the
|
||||
# repository is private.
|
||||
name: Pages
|
||||
|
||||
@@ -41,6 +42,9 @@ 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"
|
||||
@@ -66,7 +70,10 @@ jobs:
|
||||
mkdir -p site
|
||||
cp -r target/book/. site/
|
||||
bash docs/book/seo.sh site # canonical links, robots.txt, sitemap.xml
|
||||
cp -r dist/web site/app
|
||||
- name: Add the web app channels (stable and release candidate from their releases, edge from this build)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: bash packaging/web-channels.sh site dist/web
|
||||
- uses: actions/configure-pages@v5
|
||||
id: pages
|
||||
- name: Install Flatpak repository tools
|
||||
|
||||
@@ -126,6 +126,9 @@ 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
|
||||
@@ -165,6 +168,9 @@ 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
|
||||
@@ -234,8 +240,6 @@ jobs:
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# makepkg only repackages the tarball's prebuilt binaries, so both architectures build here.
|
||||
runs-on: ubuntu-24.04
|
||||
# A pre-release or nightly without an Arch package is still worth having; a stable release isn't.
|
||||
continue-on-error: ${{ contains(needs.version.outputs.version, '-') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -252,29 +256,10 @@ jobs:
|
||||
# makepkg in an archlinux:base-devel container. The package is named after the tarball's
|
||||
# .desktop file (io.tangencylabs.solidcraft) and written as
|
||||
# dist/release/solidcraft-<version>-linux-<arch>.pkg.tar.zst.
|
||||
# An Arch version can't hold a hyphen, and the action reads the version from the tarball's name
|
||||
# (a hyphenated one comes out as 0.0.0): 0.40.0-rc.1 goes in as 0.40.0rc.1, which pacman sorts
|
||||
# before 0.40.0, and the package keeps the release's name.
|
||||
- id: pkgver
|
||||
name: Arch version
|
||||
run: |
|
||||
pkgver="${VERSION//-/}"
|
||||
echo "pkgver=$pkgver" >>"$GITHUB_OUTPUT"
|
||||
if [ "$pkgver" != "$VERSION" ]; then
|
||||
cp "dist/linux/solidcraft-$VERSION-linux-$ARCH.tar.gz" "dist/linux/solidcraft-$pkgver-linux-$ARCH.tar.gz"
|
||||
fi
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
- uses: storytold/craft-common-actions/actions/arch-package@fa5ef18780cf5e7d5d3b8de068de56cffc83cac5 # main
|
||||
with:
|
||||
tarball: dist/linux/solidcraft-${{ steps.pkgver.outputs.pkgver }}-linux-${{ matrix.arch }}.tar.gz
|
||||
tarball: dist/linux/solidcraft-${{ env.VERSION }}-linux-${{ matrix.arch }}.tar.gz
|
||||
arch: ${{ matrix.arch }}
|
||||
- name: The package under the release's version
|
||||
if: steps.pkgver.outputs.pkgver != env.VERSION
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
PKGVER: ${{ steps.pkgver.outputs.pkgver }}
|
||||
run: sudo mv "dist/release/solidcraft-$PKGVER-linux-$ARCH.pkg.tar.zst" "dist/release/solidcraft-$VERSION-linux-$ARCH.pkg.tar.zst"
|
||||
- name: Check the package
|
||||
env:
|
||||
PKG: dist/release/solidcraft-${{ env.VERSION }}-linux-${{ matrix.arch }}.pkg.tar.zst
|
||||
@@ -410,6 +395,9 @@ 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.
|
||||
@@ -508,6 +496,9 @@ 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: |
|
||||
@@ -639,6 +630,9 @@ 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,15 +811,6 @@ 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)
|
||||
@@ -835,7 +826,6 @@ 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
+23
-30
@@ -4051,17 +4051,9 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcore"
|
||||
version = "1.0.1"
|
||||
source = "git+https://github.com/tangency-labs/solidcore?rev=8ddce901671f1cebd9214a31d21895d725e886c8#8ddce901671f1cebd9214a31d21895d725e886c8"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"eframe",
|
||||
@@ -4098,11 +4090,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-3dx-client"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4115,7 +4107,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-derive"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4124,7 +4116,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -4142,7 +4134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4164,7 +4156,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4173,7 +4165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4193,7 +4185,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"cadmpeg-ir",
|
||||
@@ -4219,7 +4211,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -4237,7 +4229,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde_json",
|
||||
@@ -4247,7 +4239,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-profile"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"web-time",
|
||||
@@ -4255,7 +4247,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"moxcms",
|
||||
@@ -4270,7 +4262,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"harfrust",
|
||||
"serde",
|
||||
@@ -4283,26 +4275,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-smlib"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"flate2",
|
||||
"log",
|
||||
"serde_json",
|
||||
"solidcore",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-profile",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-space-mouse"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-step"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"solidcraft-geom",
|
||||
"solidcraft-profile",
|
||||
@@ -4310,11 +4303,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-testkit"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"egui",
|
||||
@@ -4345,7 +4338,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -5752,7 +5745,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
|
||||
+1
-6
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.40.0-rc.1"
|
||||
version = "0.30.1"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -31,11 +31,6 @@ 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 = "8ddce901671f1cebd9214a31d21895d725e886c8" }
|
||||
# test tooling: a dev-dependency only (xtask/src/layers.rs)
|
||||
solidcraft-testkit = { path = "crates/testkit" }
|
||||
|
||||
|
||||
+1
-245
@@ -1,6 +1,6 @@
|
||||
Third-party software in SolidCraft
|
||||
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 536 crates below.
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 535 crates below.
|
||||
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
|
||||
SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
|
||||
|
||||
@@ -378,7 +378,6 @@ smithay-client-toolkit 0.19.2 (MIT)
|
||||
smithay-client-toolkit 0.20.0 (MIT)
|
||||
smithay-clipboard 0.7.3 (MIT)
|
||||
smol_str 0.2.2 (MIT OR Apache-2.0)
|
||||
solidcore 1.0.1 (Apache-2.0)
|
||||
spade 2.15.1 (MIT OR Apache-2.0)
|
||||
spirv 0.4.0+sdk-1.4.341.0 (Apache-2.0)
|
||||
stable_deref_trait 1.2.1 (MIT OR Apache-2.0)
|
||||
@@ -12400,249 +12399,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
solidcore 1.0.1
|
||||
================================================================================
|
||||
|
||||
Copyright (c) 2020-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) 2020-2026 NVIDIA CORPORATION & AFFILIATES
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================================================================================
|
||||
solidcore 1.0.1
|
||||
================================================================================
|
||||
|
||||
solidcore
|
||||
Copyright (c) 2026 The solidcore contributors
|
||||
|
||||
This product is a Rust port (a derivative work) of the SMLib and SM_API
|
||||
components of USD BRep, https://github.com/NVIDIA-Omniverse/usd-brep
|
||||
(v1.0.1, commit 3fd5b95), licensed under the Apache License, Version 2.0.
|
||||
See ATTRIBUTION.md for exactly which upstream material is used.
|
||||
|
||||
The upstream work carries the following notice:
|
||||
|
||||
USD BRep is Copyright (c) 2020-2026, NVIDIA CORPORATION & AFFILIATES. All
|
||||
rights reserved. NVIDIA-authored portions of this product are licensed under
|
||||
the Apache License, Version 2.0, except where otherwise noted.
|
||||
|
||||
The upstream SMLib and SM_API source files carry the SPDX header:
|
||||
|
||||
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
Upstream third-party notices (THIRD_PARTY_NOTICES.md at the pinned commit) cover
|
||||
OpenUSD, oneTBB, hwloc, MaterialX, OpenSubdiv, CPython, HACL*, pybind11, the
|
||||
UsdSolid proposals, usd-validation-nvidia, the repo bootstrap scripts and optional
|
||||
Python GUI tooling. None of those components is part of SMLib or SM_API, and none
|
||||
is used, ported or redistributed by solidcore, so no further third-party notices
|
||||
apply.
|
||||
|
||||
Modifications: the C++ sources were translated to Rust and restructured (arena
|
||||
topology, typed ids, error values instead of status codes, explicit kernel
|
||||
state instead of a global context). Each ported Rust file names the upstream
|
||||
file(s) it derives from in its header.
|
||||
|
||||
================================================================================
|
||||
spade 2.15.1
|
||||
================================================================================
|
||||
|
||||
@@ -20,9 +20,12 @@ name = "solvecraft-cli"
|
||||
path = "src/alias.rs"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu", "profile"]
|
||||
default = ["raytrace-cpu", "smlib", "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: solidcore, the only one.
|
||||
/// The kernel the cases run on: SMLib, the only one.
|
||||
fn kernel() -> String {
|
||||
"solidcore".into()
|
||||
"smlib".into()
|
||||
}
|
||||
|
||||
/// How many cases a chain of `base` recipes may go through (a ladder case builds on the one
|
||||
@@ -692,21 +692,28 @@ 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 kernel calls that fail in this case leave their inputs in its own folder.
|
||||
// The SMLib 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) {
|
||||
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()))
|
||||
{
|
||||
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);
|
||||
@@ -978,7 +985,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);
|
||||
solidcraft_engine::kernel::smlib_dump_dir(None).map_err(|e| format!("kernel-cases: --dumps: {e}"))?;
|
||||
}
|
||||
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, the kernel's calls to the
|
||||
//! `SOLIDCRAFT_PROFILE=1` for any command): record where the time goes, SMLib'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,8 +14,12 @@ name = "solidcraft-web"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["profile"]
|
||||
default = ["smlib", "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"]
|
||||
@@ -36,7 +40,7 @@ js-sys = "0.3"
|
||||
serde_json = { workspace = true }
|
||||
web-sys = { version = "0.3", features = [
|
||||
"Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "Location", "Navigator", "StorageManager",
|
||||
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
|
||||
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemGetDirectoryOptions", "FileSystemWritableFileStream",
|
||||
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage", "StorageEvent",
|
||||
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
|
||||
"Headers", "ReadableStream", "ReadableStreamDefaultReader",
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
//! 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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
This build keeps its designs and settings apart per release channel (src/channel.rs).
|
||||
packaging/web-channels.sh only offers a release candidate that has this file.
|
||||
@@ -34,6 +34,7 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="icons/32x32/io.tangencylabs.solidcraft.png">
|
||||
<link rel="apple-touch-icon" href="icons/256x256/io.tangencylabs.solidcraft.png">
|
||||
<link data-trunk rel="copy-file" href="manifest.webmanifest" />
|
||||
<link data-trunk rel="copy-file" href="channel-storage.txt" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/32x32/apps/io.tangencylabs.solidcraft.png" data-target-path="icons/32x32" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/256x256/apps/io.tangencylabs.solidcraft.png" data-target-path="icons/256x256" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/512x512/apps/io.tangencylabs.solidcraft.png" data-target-path="icons/512x512" />
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Release channels. The site serves the stable web app at `/app/`, the release candidate at
|
||||
//! `/app-rc/` and the bleeding-edge build at `/app-edge/`: one origin, so one set of browser
|
||||
//! storage. A channel that kept its designs and settings in the same places could save a design
|
||||
//! in a file format the stable app can't open, or overwrite stable's settings. So each
|
||||
//! pre-release channel keeps everything under its own name, and stable (and any copy served from
|
||||
//! another path: the release zip, `trunk serve`) keeps the names it has always used, so what
|
||||
//! people have saved at `/app/` is untouched.
|
||||
//!
|
||||
//! The channel comes from the directory the app is served from, not from the build, so a build
|
||||
//! taken from a release zip is as separate as one built on the spot.
|
||||
|
||||
/// The channel the app is served as, from the page's path: `"rc"`, `"edge"`, or `""` (stable).
|
||||
pub fn of_path(pathname: &str) -> &'static str {
|
||||
match pathname.trim_end_matches("index.html").trim_end_matches('/').rsplit('/').next() {
|
||||
Some("app-rc") => "rc",
|
||||
Some("app-edge") => "edge",
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// `name` as this channel stores it: unchanged for stable, `rc:name` and `edge:name` otherwise.
|
||||
pub fn name_in(channel: &str, name: &str) -> String {
|
||||
if channel.is_empty() { name.to_owned() } else { format!("{channel}:{name}") }
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn current() -> &'static str {
|
||||
web_sys::window().and_then(|w| w.location().pathname().ok()).map_or("", |p| of_path(&p))
|
||||
}
|
||||
|
||||
/// A storage key (local storage, IndexedDB database name) of the running channel.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn key(name: &str) -> String {
|
||||
name_in(current(), name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn channel_follows_the_directory() {
|
||||
assert_eq!(of_path("/app/"), "");
|
||||
assert_eq!(of_path("/app/index.html"), "");
|
||||
assert_eq!(of_path("/app-rc/"), "rc");
|
||||
assert_eq!(of_path("/app-rc"), "rc");
|
||||
assert_eq!(of_path("/app-rc/index.html"), "rc");
|
||||
assert_eq!(of_path("/app-edge/"), "edge");
|
||||
assert_eq!(of_path("/"), "");
|
||||
assert_eq!(of_path("/solidcraft-web-0.30.1/"), "");
|
||||
assert_eq!(of_path("/app/app-rc/"), "rc", "only the directory's own name counts");
|
||||
assert_eq!(of_path("/app-rc/app/"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stable_keeps_its_names() {
|
||||
assert_eq!(name_in("", "solidcraft.prefs"), "solidcraft.prefs");
|
||||
assert_eq!(name_in("rc", "solidcraft.prefs"), "rc:solidcraft.prefs");
|
||||
assert_ne!(name_in("rc", "x"), name_in("edge", "x"));
|
||||
}
|
||||
}
|
||||
@@ -467,7 +467,7 @@ mod web {
|
||||
|
||||
async fn open_db() -> Result<IdbDatabase, String> {
|
||||
let factory = web_sys::window().ok_or("no window")?.indexed_db().map_err(err)?.ok_or("this browser has no IndexedDB")?;
|
||||
let request: IdbOpenDbRequest = factory.open_with_u32(DB, 1).map_err(err)?;
|
||||
let request: IdbOpenDbRequest = factory.open_with_u32(&crate::channel::key(DB), 1).map_err(err)?;
|
||||
let target = request.clone();
|
||||
let upgrade = Closure::once_into_js(move || {
|
||||
if let Ok(db) = target.result().and_then(|r| r.dyn_into::<IdbDatabase>()) {
|
||||
|
||||
@@ -16,7 +16,9 @@ use solidcraft_engine::io::vfs;
|
||||
use crate::disk;
|
||||
use wasm_bindgen::{JsCast as _, JsValue, closure::Closure};
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
use web_sys::{FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetFileOptions, FileSystemWritableFileStream};
|
||||
use web_sys::{
|
||||
FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions, FileSystemGetFileOptions, FileSystemWritableFileStream,
|
||||
};
|
||||
|
||||
/// Where OPFS files appear to the engine.
|
||||
pub const OPFS: &str = disk::OPFS_PREFIX;
|
||||
@@ -111,7 +113,7 @@ thread_local! {
|
||||
static LOCAL: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
|
||||
/// Local storage keys of stored files.
|
||||
/// Local storage keys of stored files (under the channel's name, see `channel.rs`).
|
||||
const LOCAL_PREFIX: &str = "solidcraft.file.";
|
||||
/// Local-storage files kept by earlier releases (compatibility shim, docs/compatibility.md):
|
||||
/// still listed and opened; removing one removes either key.
|
||||
@@ -169,7 +171,17 @@ fn has(sm: &web_sys::StorageManager, method: &str) -> bool {
|
||||
async fn root() -> Result<FileSystemDirectoryHandle, String> {
|
||||
let sm = storage_manager().filter(|sm| has(sm, "getDirectory")).ok_or("this page has no private file storage (it needs https or localhost)")?;
|
||||
let dir = JsFuture::from(sm.get_directory()).await.map_err(err)?;
|
||||
dir.dyn_into::<FileSystemDirectoryHandle>().map_err(|_| "this browser has no private file storage (OPFS)".to_string())
|
||||
let dir = dir.dyn_into::<FileSystemDirectoryHandle>().map_err(|_| "this browser has no private file storage (OPFS)".to_string())?;
|
||||
// Stable keeps its files in the root, where they have always been; a pre-release channel has a folder of its own.
|
||||
match crate::channel::current() {
|
||||
"" => Ok(dir),
|
||||
channel => {
|
||||
let opts = FileSystemGetDirectoryOptions::new();
|
||||
opts.set_create(true);
|
||||
let sub = JsFuture::from(dir.get_directory_handle_with_options(&format!("channel-{channel}"), &opts)).await.map_err(err)?;
|
||||
sub.dyn_into::<FileSystemDirectoryHandle>().map_err(|_| "not a folder".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn read_handle(h: &FileSystemFileHandle) -> Result<Vec<u8>, String> {
|
||||
@@ -191,7 +203,7 @@ async fn file_handle(dir: &FileSystemDirectoryHandle, name: &str, create: bool)
|
||||
async fn write_file(name: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
if local_only() {
|
||||
return local()?
|
||||
.set_item(&format!("{LOCAL_PREFIX}{name}"), &encode(bytes))
|
||||
.set_item(&crate::channel::key(&format!("{LOCAL_PREFIX}{name}")), &encode(bytes))
|
||||
.map_err(|e| format!("local storage is full or blocked: {}", err(e)));
|
||||
}
|
||||
let dir = root().await?;
|
||||
@@ -207,7 +219,7 @@ async fn remove_file(name: &str) -> Result<(), String> {
|
||||
if local_only() {
|
||||
let st = local()?;
|
||||
st.remove_item(&format!("{LEGACY_LOCAL_PREFIX}{name}")).map_err(err)?;
|
||||
return st.remove_item(&format!("{LOCAL_PREFIX}{name}")).map_err(err);
|
||||
return st.remove_item(&crate::channel::key(&format!("{LOCAL_PREFIX}{name}"))).map_err(err);
|
||||
}
|
||||
let dir = root().await?;
|
||||
JsFuture::from(dir.remove_entry(name)).await.map_err(err)?;
|
||||
@@ -220,7 +232,10 @@ fn read_local() -> Result<Vec<(String, Vec<u8>, f64)>, String> {
|
||||
let mut out = Vec::new();
|
||||
for i in 0..st.length().unwrap_or(0).min(10_000) {
|
||||
let Some(k) = st.key(i).ok().flatten() else { continue };
|
||||
let Some(name) = k.strip_prefix(LOCAL_PREFIX).or_else(|| k.strip_prefix(LEGACY_LOCAL_PREFIX)) else { continue };
|
||||
let own = crate::channel::key(LOCAL_PREFIX);
|
||||
// The old names belong to stable; a pre-release channel has none.
|
||||
let legacy = crate::channel::current().is_empty().then_some(LEGACY_LOCAL_PREFIX);
|
||||
let Some(name) = k.strip_prefix(own.as_str()).or_else(|| legacy.and_then(|l| k.strip_prefix(l))) else { continue };
|
||||
if let Some(v) = st.get_item(&k).ok().flatten() {
|
||||
out.push((name.to_string(), decode(&v), 0.0));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod api;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod channel;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod disk;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod easyprint;
|
||||
@@ -152,8 +154,15 @@ impl PrefsWriter {
|
||||
/// tab of the page when one tab writes). Other keys, and settings taken away (clearing the site's
|
||||
/// data), leave this tab's settings as they are.
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn settings_written(key: Option<&str>, value: Option<String>) -> Option<String> {
|
||||
value.filter(|_| key == Some(solidcraft_ui_egui::prefs::STORAGE_KEY))
|
||||
fn settings_written(own_key: &str, key: Option<&str>, value: Option<String>) -> Option<String> {
|
||||
// Tabs of another channel write to the same storage, under their own key.
|
||||
value.filter(|_| key == Some(own_key))
|
||||
}
|
||||
|
||||
/// The settings key of `channel` (see [`channel`]).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn prefs_key() -> String {
|
||||
channel::key(solidcraft_ui_egui::prefs::STORAGE_KEY)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -262,7 +271,7 @@ mod web {
|
||||
fn hear_settings_from_other_tabs(window: &web_sys::Window, ctx: &egui::Context, inbox: PrefsInbox) {
|
||||
let ctx = ctx.clone();
|
||||
let on_storage = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::StorageEvent)>::new(move |e: web_sys::StorageEvent| {
|
||||
if let Some(prefs) = crate::settings_written(e.key().as_deref(), e.new_value()) {
|
||||
if let Some(prefs) = crate::settings_written(&crate::prefs_key(), e.key().as_deref(), e.new_value()) {
|
||||
inbox.set(Some(prefs));
|
||||
ctx.request_repaint();
|
||||
}
|
||||
@@ -335,7 +344,7 @@ mod web {
|
||||
let prefs = self.persisted_prefs();
|
||||
let Some(prefs) = self.prefs_writer.take(prefs) else { return };
|
||||
let stored =
|
||||
web_sys::window().and_then(|w| w.local_storage().ok().flatten()).map(|s| s.set_item(solidcraft_ui_egui::prefs::STORAGE_KEY, &prefs));
|
||||
web_sys::window().and_then(|w| w.local_storage().ok().flatten()).map(|s| s.set_item(&crate::prefs_key(), &prefs));
|
||||
if !matches!(stored, Some(Ok(()))) {
|
||||
log::warn!("settings: the browser's storage did not take them; they are saved again later");
|
||||
self.prefs_writer = crate::PrefsWriter::default();
|
||||
@@ -834,14 +843,15 @@ mod web {
|
||||
self.store_prefs(&ctx);
|
||||
}
|
||||
fn persist_egui_memory(&self) -> bool {
|
||||
self.launch.persists()
|
||||
// eframe keeps this under one fixed key: only stable uses it, so a pre-release never rewrites it.
|
||||
self.launch.persists() && crate::channel::current().is_empty()
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
if !self.launch.persists() {
|
||||
return;
|
||||
}
|
||||
let prefs = self.persisted_prefs();
|
||||
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, prefs);
|
||||
storage.set_string(&crate::prefs_key(), prefs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,7 +861,7 @@ mod web {
|
||||
|
||||
/// Where `?webgl` is remembered. The installed app starts from its manifest's start URL, which
|
||||
/// has no query, so the choice has to outlive the page it was made on.
|
||||
const WEBGL_KEY: &str = "solidcraft.webgl";
|
||||
const WEBGL_KEY: &str = "solidcraft.webgl"; // stored under crate::channel::key
|
||||
|
||||
/// Render with WebGL2: asked for now (`?webgl`, remembered) or before (until `?webgpu`).
|
||||
fn webgl() -> bool {
|
||||
@@ -859,17 +869,17 @@ mod web {
|
||||
let q = query();
|
||||
if crate::query_flag(&q, "webgl") {
|
||||
if let Some(s) = &storage {
|
||||
let _ = s.set_item(WEBGL_KEY, "1");
|
||||
let _ = s.set_item(&crate::channel::key(WEBGL_KEY), "1");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if crate::query_flag(&q, "webgpu") {
|
||||
if let Some(s) = &storage {
|
||||
let _ = s.remove_item(WEBGL_KEY);
|
||||
let _ = s.remove_item(&crate::channel::key(WEBGL_KEY));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
storage.and_then(|s| s.get_item(WEBGL_KEY).ok().flatten()).is_some()
|
||||
storage.and_then(|s| s.get_item(&crate::channel::key(WEBGL_KEY)).ok().flatten()).is_some()
|
||||
}
|
||||
|
||||
/// Fetch font bytes asynchronously through the browser.
|
||||
@@ -968,7 +978,7 @@ mod web {
|
||||
if launch.persists() {
|
||||
hear_settings_from_other_tabs(&window, &cc.egui_ctx, other_tab_prefs.clone());
|
||||
}
|
||||
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
|
||||
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(&crate::channel::key(k)))) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
@@ -1171,9 +1181,10 @@ mod tests {
|
||||
#[test]
|
||||
fn only_settings_another_tab_wrote_are_loaded() {
|
||||
let key = solidcraft_ui_egui::prefs::STORAGE_KEY;
|
||||
assert_eq!(settings_written(Some(key), Some("{}".into())).as_deref(), Some("{}"));
|
||||
assert_eq!(settings_written(Some("solidcraft.webgl"), Some("1".into())), None);
|
||||
assert_eq!(settings_written(Some(key), None), None, "removed settings leave this tab's");
|
||||
assert_eq!(settings_written(None, None), None, "clearing all storage leaves this tab's settings");
|
||||
assert_eq!(settings_written(key, Some(key), Some("{}".into())).as_deref(), Some("{}"));
|
||||
assert_eq!(settings_written(key, Some("solidcraft.webgl"), Some("1".into())), None);
|
||||
assert_eq!(settings_written(key, Some(key), None), None, "removed settings leave this tab's");
|
||||
assert_eq!(settings_written(key, None, None), None, "clearing all storage leaves this tab's settings");
|
||||
assert_eq!(settings_written(key, Some("rc:solidcraft.prefs"), Some("{}".into())), None, "another channel's settings are not loaded");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ publish = false
|
||||
default-run = "solidcraft"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu", "bundled-render-library", "profile"]
|
||||
default = ["raytrace-cpu", "bundled-render-library", "smlib", "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"]
|
||||
|
||||
+4
-22
@@ -309,7 +309,7 @@ impl ModelBody {
|
||||
return None;
|
||||
}
|
||||
let body = self.clone();
|
||||
let started = kernel::kernel_thread("plan-mesh").spawn(move || {
|
||||
let started = std::thread::Builder::new().name("plan-mesh".into()).spawn(move || {
|
||||
let _ = body.plan_mesh_at(q);
|
||||
if let Ok(mut warming) = WARMING.lock() {
|
||||
warming.retain(|k| *k != key);
|
||||
@@ -444,12 +444,8 @@ fn with_largest_fit(
|
||||
size: &str,
|
||||
search: impl FnOnce() -> std::result::Result<Option<(f64, kernel::Body)>, kernel::KernelError>,
|
||||
) -> kernel::KernelError {
|
||||
// On solidcore the refusal names a size already, less a margin (34.99 where 35 builds), and
|
||||
// its edge: the edge stays, the size is the largest the search builds, solidcore's where the
|
||||
// search finds none.
|
||||
match search() {
|
||||
Ok(Some((largest, _))) if kernel::names_largest_fit(&why) => kernel::KernelError::TooLarge(kernel::with_fit_note(&why, size, largest)),
|
||||
Ok(Some((largest, _))) => kernel::KernelError::TooLarge(format!("{why}; {}", kernel::largest_fit_note(size, largest))),
|
||||
Ok(Some((largest, _))) => kernel::KernelError::TooLarge(format!("{why}; the largest {size} that fits is {} mm", show_mm(largest))),
|
||||
Ok(None) => kernel::KernelError::TooLarge(why),
|
||||
Err(e) => {
|
||||
log::debug!("the largest {size} that fits: {e}");
|
||||
@@ -1030,10 +1026,6 @@ pub struct FeatureResult {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub error: Option<String>,
|
||||
/// `error` is the kernel's internal error (a panic it caught, [`kernel::KernelError::Internal`]),
|
||||
/// not the feature's failure: the command that made the feature reports it as one.
|
||||
#[serde(skip)]
|
||||
pub internal: bool,
|
||||
pub warning: Option<String>,
|
||||
/// An Import feature's warnings by kind and part, with what came in of the file's parts
|
||||
/// (`warning` is the first of them as the kernel words them).
|
||||
@@ -2089,7 +2081,6 @@ impl Model {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
error: None,
|
||||
internal: false,
|
||||
warning: None,
|
||||
import_report: None,
|
||||
repair: None,
|
||||
@@ -2123,7 +2114,6 @@ impl Model {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
error: Some(e),
|
||||
internal: false,
|
||||
warning: None,
|
||||
import_report: None,
|
||||
repair: None,
|
||||
@@ -2162,7 +2152,6 @@ impl Model {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
error: c.error,
|
||||
internal: false,
|
||||
warning: c.warning,
|
||||
import_report: c.import_report,
|
||||
repair: c.repair,
|
||||
@@ -2231,11 +2220,7 @@ impl Model {
|
||||
None => kernel::guard(&f.name, || {
|
||||
let (r, deferred) =
|
||||
kernel::validating(validation, || eval_feature(doc, &vals, f, &mut next, &mut warning, &mut import_report, &mut repair));
|
||||
r.map_err(|e| match e {
|
||||
// A panic the kernel caught stays one.
|
||||
crate::DocError::Kernel(kernel::KernelError::Internal(m)) => kernel::KernelError::Internal(m),
|
||||
e => kernel::KernelError::Failed(e.in_feature(&f.name).to_string()),
|
||||
})?;
|
||||
r.map_err(|e| kernel::KernelError::Failed(e.in_feature(&f.name).to_string()))?;
|
||||
// The checks the feature's steps left to its end: the bodies it keeps.
|
||||
if deferred { check_kept(&state, &next) } else { Ok(()) }
|
||||
}),
|
||||
@@ -2255,7 +2240,6 @@ impl Model {
|
||||
note_join_apart(&state, &next, f, &mut warning);
|
||||
}
|
||||
let ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||
let internal = matches!(r, Err(kernel::KernelError::Internal(_)));
|
||||
let error = match r {
|
||||
Ok(()) => {
|
||||
// Copies are named after their sources through the instance transforms.
|
||||
@@ -2299,8 +2283,7 @@ impl Model {
|
||||
}
|
||||
Err(e) => Some(e.to_string().trim_start_matches("the operation failed: ").to_string()),
|
||||
};
|
||||
// A panic is no result to reuse: the kernel was replaced after it.
|
||||
if !self.no_cache && !watched.cancelled() && !internal {
|
||||
if !self.no_cache && !watched.cancelled() {
|
||||
let e = CachedEval {
|
||||
error: error.clone(),
|
||||
warning: warning.clone(),
|
||||
@@ -2322,7 +2305,6 @@ impl Model {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
error,
|
||||
internal,
|
||||
warning,
|
||||
import_report,
|
||||
repair,
|
||||
|
||||
@@ -786,7 +786,6 @@ impl Model {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
error: None,
|
||||
internal: false,
|
||||
warning: r.get("warning").map(|w| text(Some(w))).transpose()?,
|
||||
import_report: r
|
||||
.get("import_report")
|
||||
|
||||
@@ -13,6 +13,8 @@ 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: solidcore (the kernel's calls), kernel, feature (one per feature \
|
||||
self time first. Categories: smlib (SMLib's C API calls, timed by the C API), 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",
|
||||
@@ -131,7 +131,9 @@ fn crash_test(_: &mut Session, p: &Value) -> Result<Value> {
|
||||
match str_(p, "mode") {
|
||||
Some("panic") => std::panic::panic_any(String::from("crash test: an internal error in a command")),
|
||||
Some("kernel") => {
|
||||
solidcraft_kernel::crash_test()?;
|
||||
solidcraft_kernel::guard("crash test", || -> solidcraft_kernel::Result<()> {
|
||||
std::panic::panic_any(String::from("an internal error in the kernel"))
|
||||
})?;
|
||||
Err(EngineError::Other("crash test: the kernel error was not caught".into()))
|
||||
}
|
||||
Some("abort") => std::process::abort(),
|
||||
|
||||
@@ -826,10 +826,8 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
|
||||
}
|
||||
}
|
||||
let r = s.model.result(id).cloned();
|
||||
if let Some(r) = &r
|
||||
&& let Some(e) = r.error.clone()
|
||||
{
|
||||
return Err(EngineError::of_feature(r, e));
|
||||
if let Some(e) = r.as_ref().and_then(|r| r.error.clone()) {
|
||||
return Err(EngineError::Other(e));
|
||||
}
|
||||
let st = s.model.state();
|
||||
let bodies: Vec<&str> = st.bodies.iter().map(|b| b.name.as_str()).collect();
|
||||
|
||||
@@ -1124,7 +1124,8 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
/// While the profiler records, a STEP import's reply carries its own profile: the import's
|
||||
/// stages and the kernel's calls, as diagnostics.profile reports them.
|
||||
/// stages and SMLib's calls, as diagnostics.profile reports them.
|
||||
#[cfg(feature = "smlib")]
|
||||
#[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);
|
||||
@@ -1143,7 +1144,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("solidcore"), "{p}");
|
||||
assert!(has("import") && has("smlib"), "{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}");
|
||||
}
|
||||
|
||||
|
||||
@@ -324,7 +324,7 @@ fn run_ordered(jobs: Vec<Job>, budget: std::time::Duration) -> (Vec<Option<Done>
|
||||
let mut threads = 0;
|
||||
for _ in 0..want {
|
||||
let (jobs, next, stop, tx) = (jobs.clone(), next.clone(), stop.clone(), tx.clone());
|
||||
let spawned = solidcraft_kernel::kernel_thread("solidcraft-interference").spawn(move || {
|
||||
let spawned = std::thread::Builder::new().name("solidcraft-interference".into()).spawn(move || {
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
let k = next.fetch_add(1, Ordering::Relaxed);
|
||||
let Some(job) = jobs.get(k) else { break };
|
||||
|
||||
@@ -34,7 +34,7 @@ mod component_clone;
|
||||
mod component_clone_assembly;
|
||||
mod construct;
|
||||
mod delete;
|
||||
pub(crate) mod diagnostics;
|
||||
mod diagnostics;
|
||||
mod edit;
|
||||
mod face;
|
||||
mod features;
|
||||
|
||||
@@ -168,10 +168,8 @@ fn child() {
|
||||
}
|
||||
assert_eq!(s.doc.features.len(), 1, "the design is unchanged");
|
||||
}
|
||||
// A panic inside a solidcore call: caught there, an internal error of the command.
|
||||
"kernel" => {
|
||||
let r = s.execute("diagnostics.crash_test", &json!({"mode": "kernel"}));
|
||||
assert!(matches!(&r, Err(crate::EngineError::Internal(id, _)) if id == "diagnostics.crash_test"), "{r:?}");
|
||||
assert!(s.execute("diagnostics.crash_test", &json!({"mode": "kernel"})).is_err());
|
||||
}
|
||||
// `thread_dies`: the panic ends the app, as one on the window's thread does.
|
||||
"thread" | "thread_dies" => {
|
||||
|
||||
@@ -84,10 +84,7 @@ pub enum EngineError {
|
||||
|
||||
impl From<solidcraft_doc::DocError> for EngineError {
|
||||
fn from(e: solidcraft_doc::DocError) -> Self {
|
||||
match e {
|
||||
solidcraft_doc::DocError::Kernel(e) => e.into(),
|
||||
e => EngineError::Other(e.to_string()),
|
||||
}
|
||||
EngineError::Other(e.to_string())
|
||||
}
|
||||
}
|
||||
impl From<solidcraft_sketch::SketchError> for EngineError {
|
||||
@@ -97,19 +94,7 @@ impl From<solidcraft_sketch::SketchError> for EngineError {
|
||||
}
|
||||
impl From<solidcraft_kernel::KernelError> for EngineError {
|
||||
fn from(e: solidcraft_kernel::KernelError) -> Self {
|
||||
match e {
|
||||
// A panic the kernel caught: internal, as one in the command would be. The command's id
|
||||
// is filled in where the command is run (`Session::execute`).
|
||||
solidcraft_kernel::KernelError::Internal(m) => EngineError::Internal(String::new(), m),
|
||||
e => EngineError::Other(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EngineError {
|
||||
/// The error a failed feature's result reports: internal when the kernel caught a panic.
|
||||
pub(crate) fn of_feature(r: &solidcraft_doc::FeatureResult, error: String) -> EngineError {
|
||||
if r.internal { EngineError::Internal(String::new(), error) } else { EngineError::Other(error) }
|
||||
EngineError::Other(e.to_string())
|
||||
}
|
||||
}
|
||||
impl From<solidcraft_io::IoError> for EngineError {
|
||||
@@ -1663,11 +1648,6 @@ impl Session {
|
||||
Err(EngineError::Internal(spec.id.to_string(), msg))
|
||||
}
|
||||
};
|
||||
// The kernel's internal errors come up without the command's id.
|
||||
let r = r.map_err(|e| match e {
|
||||
EngineError::Internal(id, msg) if id.is_empty() => EngineError::Internal(spec.id.to_string(), msg),
|
||||
e => e,
|
||||
});
|
||||
// A value typed as `name = expression` defines the user parameter `name` (#4071), in the
|
||||
// same undo step; a refused definition fails the command, which leaves the design as it was.
|
||||
let r = match r {
|
||||
@@ -2194,10 +2174,8 @@ impl Session {
|
||||
{
|
||||
self.doc_mut().marker = Some(idx + 1);
|
||||
self.refresh();
|
||||
if let Some(r) = self.model.result(fid)
|
||||
&& let Some(e) = r.error.clone()
|
||||
{
|
||||
return Err(EngineError::of_feature(r, e));
|
||||
if let Some(e) = self.model.result(fid).and_then(|r| r.error.clone()) {
|
||||
return Err(EngineError::Other(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("wgpu") && super::THIRD_PARTY.contains("egui"));
|
||||
assert!(super::THIRD_PARTY.contains("truck") && super::THIRD_PARTY.contains("egui"));
|
||||
assert!(super::all().contains("NOTICE"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,8 @@ pub struct ProfileParams {
|
||||
/// One category's share of the recorded self time.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct ProfileCategory {
|
||||
/// `solidcore` (the kernel's own calls), `kernel`, `feature`, `doc`, `command`, `import`, `io`.
|
||||
/// `smlib` (SMLib's own time, measured by its C API), `kernel`, `feature`, `doc`, `command`,
|
||||
/// `import`, `io`.
|
||||
pub category: String,
|
||||
pub self_ms: f64,
|
||||
/// Of all recorded self time, 0–1.
|
||||
|
||||
@@ -110,10 +110,9 @@ fn fillet_as_wide_as_a_face_rounds_it_off() {
|
||||
/// The owner's ruling of 2026-10-30: two edges of a 20 mm box rounded r2 in one fillet (they
|
||||
/// meet in a mitre at the corner (20, 0, 20), the upright edge there sharp), then the upright edge
|
||||
/// r3 in a second fillet, whose tangent chain takes the mitre's seam with it. The corner is the
|
||||
/// one solidcore 8ddce9016 builds for three rounds of different radii there: one patch, 10
|
||||
/// faces, 7928.743041456134 mm³ (the fork's setback corner, 15 faces and 7928.142563938, waits
|
||||
/// for solidcore#2310). The r2 rounds keep their names, and the r3 round and the corner's patch
|
||||
/// are the second fillet's.
|
||||
/// body rounding all three at once makes (kernel `fillet_radii` of the box with each edge's
|
||||
/// radius, `three_rounds_at_a_corner`: SMLib's setback corner, 15 faces, 7928.142563938 mm³); the
|
||||
/// r2 rounds keep their names, and the r3 round and the corner's patches are the second fillet's.
|
||||
#[test]
|
||||
fn a_later_round_at_a_mitred_corner_is_blended_with_the_rounds_there() {
|
||||
let mut s = Session::default();
|
||||
@@ -135,17 +134,17 @@ fn a_later_round_at_a_mitred_corner_is_blended_with_the_rounds_there() {
|
||||
let second = run(&mut s, "solid.fillet", json!({"edges": [[20, 0, 10]], "radius": 3}))["feature"].as_u64().unwrap();
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{:?}", s.model.results);
|
||||
let v = volume(&mut s);
|
||||
assert!((v - 7928.743041456134).abs() < 1e-6, "{v}");
|
||||
assert!((v - 7928.142563938).abs() < 1e-6, "{v}");
|
||||
let faces = run(&mut s, "model.faces", json!({"body": "Box"}))["faces"].as_array().cloned().unwrap_or_default();
|
||||
assert_eq!(faces.len(), 10);
|
||||
assert_eq!(faces.len(), 15);
|
||||
let after: Vec<String> = rounds.iter().map(|p| named(&mut s, *p)).collect();
|
||||
assert_eq!(after, before);
|
||||
// The r3 round (halfway up, at 45°) and the corner's patches: the faces the second fillet made.
|
||||
let k3 = 3.0 - 4.5_f64.sqrt();
|
||||
assert!(named(&mut s, solidcraft_geom::Vec3::new(20.0 - k3, k3, 10.0)).starts_with(&format!("F{second}:blend:")));
|
||||
let made = faces.iter().filter(|f| f["name"].as_str().is_some_and(|n| n.starts_with(&format!("F{second}:")))).count();
|
||||
// the r3 round and the corner's patch
|
||||
assert_eq!(made, 2, "{faces:?}");
|
||||
// the r3 round and the corner's six patches (a setback patch per round, three in the triangle)
|
||||
assert_eq!(made, 7, "{faces:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -45,24 +45,6 @@ fn chamfer_that_fits_still_builds() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #199's bar, 14 x 6 x 10, its four upright edges rounded: past a full round of its 6 mm ends
|
||||
/// the two rounds on each end would cross. Refused, naming 3 mm: the kernel's refusal carries a
|
||||
/// size and the edge it was found for (`SolidError::FilletRefused`), and the message names the
|
||||
/// edge.
|
||||
#[test]
|
||||
fn a_round_past_a_full_round_is_refused_naming_the_largest_that_fits_and_its_edge() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 14, "width": 6, "height": 10, "corner": [14, 0, 0]}));
|
||||
let uprights = json!([[14, 0, 5], [28, 0, 5], [14, 6, 5], [28, 6, 5]]);
|
||||
for r in [3.001, 3.5] {
|
||||
let e = s.execute("solid.fillet", &json!({"edges": uprights, "radius": r})).unwrap_err().to_string();
|
||||
assert!(e.contains("the largest radius that fits is 3 mm"), "{r}: {e}");
|
||||
assert!(e.contains("allow, at the edge at ("), "{r}: {e}");
|
||||
assert_eq!(e.matches("that fits is").count(), 1, "{r}: {e}");
|
||||
}
|
||||
assert!((volume(&mut s) - 840.0).abs() < 1e-6, "the part stays as it was");
|
||||
}
|
||||
|
||||
/// A rib 4 mm wide: blends of its two top edges meet in its top's middle at 2 mm and past that
|
||||
/// would cross each other. Refused as too large, naming the largest size that fits.
|
||||
#[test]
|
||||
|
||||
@@ -109,25 +109,12 @@ fn clamped(p: usize, n: usize, k: &[f64]) -> bool {
|
||||
p >= 1 && k.len() == n + p + 1 && ends(1..=p) && ends(n..=n + p - 1)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// `x` moved by whole periods `b − a` into `[a, b]` when it lies outside.
|
||||
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)
|
||||
}
|
||||
@@ -423,9 +410,6 @@ 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, solidcore, as in the
|
||||
# apps.
|
||||
solidcraft-engine = { workspace = true, features = ["bundled-render-library"] }
|
||||
# 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"] }
|
||||
solidcraft-io = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
# The HLSL the D3D12 backend compiles (tests check it suits FXC).
|
||||
|
||||
+2
-13
@@ -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(), 12, "{:?}", tree.as_object().unwrap().keys().collect::<Vec<_>>());
|
||||
assert_eq!(tree.as_object().unwrap().len(), 11, "{:?}", 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", "kernel", "document", "payload"] {
|
||||
for key in ["format", "producer", "evaluation", "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,17 +1548,6 @@ 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, platform and geometry kernel that computed it
|
||||
//! cache to the exact document, and to the source and platform that computed it
|
||||
//! (docs/design-container.md).
|
||||
use super::*;
|
||||
use solidcraft_doc::{ModelState, SavedModel};
|
||||
@@ -29,11 +29,6 @@ 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()
|
||||
@@ -301,7 +296,6 @@ impl Encoded {
|
||||
document: digest(document),
|
||||
evaluation: evaluation(),
|
||||
format: FORMAT,
|
||||
kernel: KERNEL,
|
||||
model: records,
|
||||
payload: digest(&data),
|
||||
producer: PRODUCER,
|
||||
@@ -326,7 +320,6 @@ struct Manifest<'a> {
|
||||
document: String,
|
||||
evaluation: String,
|
||||
format: &'a str,
|
||||
kernel: &'a str,
|
||||
model: &'a Value,
|
||||
payload: String,
|
||||
producer: &'a str,
|
||||
@@ -343,15 +336,14 @@ 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 and kernel 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 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 solidcore)"
|
||||
description = "SolidCraft B-rep kernel boundary: solids, extrude/revolve/booleans/fillets, meshes, measures and STEP, behind our own API (implemented with SMLib)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -8,6 +8,12 @@ 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,13 +2,21 @@
|
||||
//! 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 --example step_board_fixture -- board.step
|
||||
//! cargo run --release -p solidcraft-kernel --features smlib --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}");
|
||||
@@ -16,6 +24,7 @@ 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};
|
||||
|
||||
@@ -25,34 +25,6 @@ pub(crate) fn too_large(msg: &str) -> KernelError {
|
||||
KernelError::TooLarge(msg.into())
|
||||
}
|
||||
|
||||
/// The note on a blend's refusal as too large that names the largest `size` (radius, distance)
|
||||
/// that fits: "the largest radius that fits is 3 mm". The kernel's own sized refusals and a fillet
|
||||
/// feature's search for the size ([`fillet_max`]) word it alike.
|
||||
pub fn largest_fit_note(size: &str, largest: f64) -> String {
|
||||
format!("the largest {size} that fits is {} mm", trimmed_mm(largest))
|
||||
}
|
||||
|
||||
/// Whether a refusal already names the largest size that fits ([`largest_fit_note`]).
|
||||
pub fn names_largest_fit(why: &str) -> bool {
|
||||
why.contains("that fits is ")
|
||||
}
|
||||
|
||||
/// `why` with the largest size that fits it names ([`largest_fit_note`]) replaced by `largest`:
|
||||
/// solidcore names the size less a margin (34.99 where 35 builds), a feature's search the
|
||||
/// largest it built. `why` itself where it names none.
|
||||
pub fn with_fit_note(why: &str, size: &str, largest: f64) -> String {
|
||||
let Some(start) = why.find("the largest ") else { return why.to_owned() };
|
||||
let Some(len) = why.get(start..).and_then(|rest| rest.find(" mm")) else { return why.to_owned() };
|
||||
let (head, tail) = (why.get(..start).unwrap_or_default(), why.get(start + len + 3..).unwrap_or_default());
|
||||
format!("{head}{}{tail}", largest_fit_note(size, largest))
|
||||
}
|
||||
|
||||
/// `v` to the micrometre, without trailing zeros.
|
||||
pub(crate) fn trimmed_mm(v: f64) -> String {
|
||||
let s = format!("{v:.6}");
|
||||
s.trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
}
|
||||
|
||||
fn v3(p: [f64; 3]) -> Vec3 {
|
||||
Vec3::new(p[0], p[1], p[2])
|
||||
}
|
||||
|
||||
@@ -123,8 +123,8 @@ mod imp {
|
||||
use crate::native::{body_from_smlib_ordered, smlib_body_ordered};
|
||||
use crate::{KernelError, Result, guard, record};
|
||||
|
||||
fn failed(what: &str, step: &str, e: impl crate::Cause) -> KernelError {
|
||||
KernelError::smlib(format!("SMLib {what}: {step}"), e)
|
||||
fn failed(what: &str, step: &str, e: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("SMLib {what}: {step}: {e}"))
|
||||
}
|
||||
|
||||
/// How far a pick may lie from its edge.
|
||||
@@ -444,24 +444,15 @@ 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)"
|
||||
)),
|
||||
SmError::NoRoom => KernelError::TooLarge(format!("the {what} is larger than the faces beside its edges allow")),
|
||||
SmError::TooLarge { size, kind, at, why } => {
|
||||
let mut beside = match at {
|
||||
Some([x, y, z]) => format!(", at the edge at ({x:.3}, {y:.3}, {z:.3})"),
|
||||
None => String::new(),
|
||||
};
|
||||
if let Some(why) = why {
|
||||
beside = format!("{beside}: {}", why.words());
|
||||
}
|
||||
let fits = match kind {
|
||||
solidcraft_smlib::FitKind::Scale => {
|
||||
format!("the largest that fits is {} times the distances given", crate::blend::trimmed_mm(size))
|
||||
}
|
||||
kind => crate::blend::largest_fit_note(kind.word(), size),
|
||||
};
|
||||
KernelError::TooLarge(format!("the {what} is larger than the faces beside its edges allow{beside}; {fits}"))
|
||||
}
|
||||
SmError::Unported(why) => KernelError::Invalid(format!("not supported yet: {why}")),
|
||||
// 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")),
|
||||
},
|
||||
e => failed(what, "operation", e),
|
||||
})?;
|
||||
// Checked here unless the evaluation's validation mode leaves it to the feature's end
|
||||
|
||||
@@ -420,8 +420,6 @@ pub struct Refusal {
|
||||
pub surface: Option<SurfaceKind>,
|
||||
/// SMLib's reason for the whole body, as the log words it.
|
||||
pub why: String,
|
||||
/// The reason is a panic the wrapper caught, not a refusal ([`KernelError::Internal`]).
|
||||
pub internal: bool,
|
||||
}
|
||||
|
||||
/// Where a point lies against a body ([`Body::classify_point`]).
|
||||
@@ -453,16 +451,13 @@ const MAX_REFUSAL_PROBES: usize = 20_000;
|
||||
impl Refusal {
|
||||
/// The refusal of `record` for `error`, with the face SMLib refuses on its own.
|
||||
fn of(record: &Brep, error: &solidcraft_smlib::Error) -> Refusal {
|
||||
if let solidcraft_smlib::Error::Internal(why) = error {
|
||||
return Refusal { face: None, surface: None, why: why.clone(), internal: true };
|
||||
}
|
||||
let _span = solidcraft_profile::span("kernel", "smlib_refusal_face");
|
||||
let alone = |f: usize| crate::surfaces::Parts::of(record, &[(f, false)]).and_then(|p| p.canonical()).map(|c| c.brep);
|
||||
let face = (record.faces.len() <= MAX_REFUSAL_PROBES)
|
||||
.then(|| (0..record.faces.len()).find(|&f| alone(f).is_ok_and(|r| solidcraft_smlib::Brep::from_record(&r, true).is_err())))
|
||||
.flatten();
|
||||
let surface = face.and_then(|f| record.faces.get(f)).map(|f| SurfaceKind::of(&f.surface));
|
||||
Refusal { face, surface, why: error.to_string(), internal: false }
|
||||
Refusal { face, surface, why: error.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,9 +540,7 @@ impl Body {
|
||||
self.smlib
|
||||
.get_or_init(|| solidcraft_smlib::Brep::from_record(&self.record, true).map_err(|e| Refusal::of(&self.record, &e)))
|
||||
.as_ref()
|
||||
.map_err(
|
||||
|r| if r.internal { KernelError::Internal(format!("SMLib: {}", r.why)) } else { KernelError::Failed(format!("SMLib: {}", r.why)) },
|
||||
)
|
||||
.map_err(|r| KernelError::Failed(format!("SMLib: {}", r.why)))
|
||||
}
|
||||
|
||||
/// Why SMLib refuses the body, if it does: the body is sent into SMLib as an operation
|
||||
@@ -1054,7 +1047,7 @@ impl Body {
|
||||
if self.is_mesh() {
|
||||
return Ok(self.outline_bounds());
|
||||
}
|
||||
let (lo, hi) = self.smlib()?.bounds().map_err(|e| KernelError::smlib("SMLib bounds", e))?;
|
||||
let (lo, hi) = self.smlib()?.bounds().map_err(|e| KernelError::Failed(format!("SMLib bounds: {e}")))?;
|
||||
Ok(Aabb3 { min: v3(lo), max: v3(hi) })
|
||||
}
|
||||
|
||||
@@ -1387,7 +1380,7 @@ impl Body {
|
||||
PointClass::Outside
|
||||
});
|
||||
}
|
||||
let class = self.smlib()?.classify_point([p.x, p.y, p.z]).map_err(|e| KernelError::smlib("SMLib point classification", e))?;
|
||||
let class = self.smlib()?.classify_point([p.x, p.y, p.z]).map_err(|e| KernelError::Failed(format!("SMLib point classification: {e}")))?;
|
||||
Ok(match class {
|
||||
solidcraft_smlib::PointClass::Inside => PointClass::Inside,
|
||||
solidcraft_smlib::PointClass::Outside => PointClass::Outside,
|
||||
|
||||
@@ -429,6 +429,7 @@ 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;
|
||||
|
||||
@@ -919,7 +919,7 @@ pub fn fair_faces(body: &Body, faces: &[usize], opts: &FairOptions) -> Result<(B
|
||||
if changed.is_empty() {
|
||||
return Ok((body.clone(), report));
|
||||
}
|
||||
let smlib = solidcraft_smlib::Brep::from_record(&record, true).map_err(|e| KernelError::smlib("fair: SMLib", e))?;
|
||||
let smlib = solidcraft_smlib::Brep::from_record(&record, true).map_err(|e| KernelError::Failed(format!("fair: SMLib: {e}")))?;
|
||||
let (result, order) = crate::native::body_from_smlib_ordered(&smlib)?;
|
||||
// The topology is the body's own, so SMLib's order is the body's: each face kept or
|
||||
// modified, each edge kept. Where SMLib's order is not the record's the history is not known.
|
||||
|
||||
@@ -795,7 +795,7 @@ fn patch_body(body: &Body, record: &b::Brep, chain: &[(usize, bool)], surface: b
|
||||
}
|
||||
let mut made = assemble(Parts { vertices, edges, faces: vec![PartFace { surface, forward: true, loops: vec![uses] }], tolerance: tol }, false)?;
|
||||
hold_gap(&mut made.brep, tol);
|
||||
let smlib = solidcraft_smlib::Brep::from_record(&made.brep, true).map_err(|e| KernelError::smlib("patch: SMLib", e))?;
|
||||
let smlib = solidcraft_smlib::Brep::from_record(&made.brep, true).map_err(|e| KernelError::Failed(format!("patch: SMLib: {e}")))?;
|
||||
let (patch, order) = crate::native::body_from_smlib_ordered(&smlib)?;
|
||||
// The patch's edges in its own order, each from the loop edge it is.
|
||||
let Some(order) = order else { return Ok(patch) };
|
||||
@@ -920,7 +920,7 @@ fn quilt_body(body: &Body, record: &b::Brep, chain: &[Piece], fill: &IsoFill, to
|
||||
}
|
||||
let mut made = assemble(Parts { vertices, edges, faces, tolerance: tol }, false)?;
|
||||
hold_gap(&mut made.brep, tol);
|
||||
let smlib = solidcraft_smlib::Brep::from_record(&made.brep, true).map_err(|e| KernelError::smlib("patch: SMLib", e))?;
|
||||
let smlib = solidcraft_smlib::Brep::from_record(&made.brep, true).map_err(|e| KernelError::Failed(format!("patch: SMLib: {e}")))?;
|
||||
let (sheet, order) = crate::native::body_from_smlib_ordered(&smlib)?;
|
||||
let Some(order) = order else { return Ok(sheet) };
|
||||
let place = |list: &[usize], c: usize| if list.is_empty() { Some(c) } else { list.get(c).copied() };
|
||||
|
||||
@@ -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 the corner fill (certified convex only from a setback on, as SMLib
|
||||
/// f916a8d's setback corner does) is refused naming the smallest that builds,
|
||||
/// 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,
|
||||
/// 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() {
|
||||
|
||||
@@ -22,8 +22,8 @@ pub struct MeetingEdge {
|
||||
pub points: Vec<Vec3>,
|
||||
}
|
||||
|
||||
fn failed(what: &str, e: impl crate::Cause) -> KernelError {
|
||||
KernelError::smlib(format!("SMLib intersection curve: {what}"), e)
|
||||
fn failed(what: &str, e: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("SMLib intersection curve: {what}: {e}"))
|
||||
}
|
||||
|
||||
/// `body` itself, or its faces `faces` (in its face order) alone as a surface body.
|
||||
|
||||
@@ -361,26 +361,3 @@ fn rounds_ending_in_a_sphere_of_any_size_never_panic() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The front's top edge rounded r 2, then the edge between that round and the arc wall (the
|
||||
/// middle of the arc top edge's tangent chain) at the round's own radius: the ball sits on the
|
||||
/// round's axis, fixed, and the plane square to the edge at its top end misses that centre, so
|
||||
/// no round ends there. solidcore refuses it as a collapsed offset (`NoRoomReason::
|
||||
/// OffsetCollapses`, solidcore#2431), naming 2 less its fit margin; the refusal says why.
|
||||
#[test]
|
||||
fn a_ball_as_wide_as_the_round_it_rolls_on_is_refused_saying_why() {
|
||||
let rounded = fillet(&part(), &[tf()], 2.0).unwrap();
|
||||
let middle = Vec3::new(41.17486688363082, -20.41321806796856, 9.415188112149071);
|
||||
let why = match fillet(&rounded, &[middle], 2.0) {
|
||||
Err(KernelError::TooLarge(why)) => why,
|
||||
other => panic!("{:?}", other.map(|b| b.face_count())),
|
||||
};
|
||||
assert!(why.contains("the ball is as wide as a face it rolls on, so its centre stays put and the round cannot end at the edge's end"), "{why}");
|
||||
// 2 less solidcore's fit margin.
|
||||
let largest: f64 = why
|
||||
.split_once("the largest radius that fits is ")
|
||||
.and_then(|(_, n)| n.split_once(" mm"))
|
||||
.and_then(|(n, _)| n.parse().ok())
|
||||
.unwrap_or_else(|| panic!("{why}"));
|
||||
assert!(largest < 2.0 && largest > 1.9, "{why}");
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ mod ops;
|
||||
#[cfg(test)]
|
||||
mod outline_tests;
|
||||
mod par;
|
||||
pub use par::{KERNEL_STACK, for_each_on_threads, kernel_thread, map_on_threads, map_with_local_pools};
|
||||
pub use par::{for_each_on_threads, map_on_threads, map_with_local_pools};
|
||||
#[cfg(test)]
|
||||
mod pinch_tests;
|
||||
#[cfg(test)]
|
||||
@@ -157,8 +157,7 @@ pub mod watch;
|
||||
|
||||
pub use blend::{
|
||||
ChamferSide, SETBACK_AT_LEAST, blend_edges, chamfer, chamfer_max, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners,
|
||||
fillet_g2_with_g2_corners, fillet_least_setback, fillet_max, fillet_max_each, fillet_removing, fillet_setback, largest_fit_note,
|
||||
names_largest_fit, tangent_chain, with_fit_note,
|
||||
fillet_g2_with_g2_corners, fillet_least_setback, fillet_max, fillet_max_each, fillet_removing, fillet_setback, tangent_chain,
|
||||
};
|
||||
pub use blend_corner::fillet_radii;
|
||||
pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fillet_variable, fillet_variable_points, fillet_variable_spline, taper};
|
||||
@@ -237,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_dump_dir, validity};
|
||||
pub use validity::{SmlibCheck, Validity, smlib_built, smlib_dump_dir, validity};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
pub enum KernelError {
|
||||
@@ -256,48 +255,6 @@ pub enum KernelError {
|
||||
|
||||
pub type Result<T> = std::result::Result<T, KernelError>;
|
||||
|
||||
/// Why a kernel step failed: a message, or an error of the solidcore wrapper, which may be a
|
||||
/// panic the wrapper caught ([`solidcraft_smlib::Error::Internal`]).
|
||||
pub(crate) trait Cause: std::fmt::Display {
|
||||
/// The panic's message, when the cause is one.
|
||||
fn panic(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Cause for solidcraft_smlib::Error {
|
||||
fn panic(&self) -> Option<&str> {
|
||||
match self {
|
||||
solidcraft_smlib::Error::Internal(m) => Some(m),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Cause for str {}
|
||||
impl Cause for String {}
|
||||
impl Cause for solidcraft_smlib::CheckReport {}
|
||||
impl Cause for solidcraft_smlib::export::ExportError {}
|
||||
impl Cause for solidcraft_geom::brep::history::Failed {}
|
||||
impl Cause for solidcraft_geom::brep::pcurve::Error {}
|
||||
impl<T: Cause + ?Sized> Cause for &T {
|
||||
fn panic(&self) -> Option<&str> {
|
||||
(**self).panic()
|
||||
}
|
||||
}
|
||||
|
||||
impl KernelError {
|
||||
/// A kernel step's failure, `context` before its cause: a panic the solidcore wrapper caught
|
||||
/// (its kernel replaced by a fresh one) stays an internal error ([`KernelError::Internal`]),
|
||||
/// so the engine reports it as one; any other cause is the operation's failure.
|
||||
pub(crate) fn smlib(context: impl std::fmt::Display, e: impl Cause) -> KernelError {
|
||||
match e.panic() {
|
||||
Some(m) => KernelError::Internal(format!("{context}: {m}")),
|
||||
None => KernelError::Failed(format!("{context}: {e}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The native kernel call running right now (SMLib), for crash reports.
|
||||
pub fn native_call_in_flight() -> Option<String> {
|
||||
solidcraft_smlib::current_call()
|
||||
@@ -310,13 +267,7 @@ thread_local! {
|
||||
/// Is this thread inside [`guard`]? (A panic now will be caught and turned into an error; the
|
||||
/// engine's crash reports mark it recovered.)
|
||||
pub fn guarded() -> bool {
|
||||
GUARDED.with(std::cell::Cell::get) > 0 || solidcraft_smlib::in_call()
|
||||
}
|
||||
|
||||
/// A panic inside a solidcore call, caught there and reported as the kernel's internal error
|
||||
/// ([`KernelError::Internal`]): the crash-report check (`diagnostics.crash_test`'s kernel mode).
|
||||
pub fn crash_test() -> Result<()> {
|
||||
solidcraft_smlib::crash_test().map_err(|e| KernelError::smlib("crash test", e))
|
||||
GUARDED.with(std::cell::Cell::get) > 0
|
||||
}
|
||||
|
||||
/// Run third-party kernel code, converting a panic into an error.
|
||||
|
||||
@@ -102,7 +102,7 @@ pub fn measure_mass(b: &Body) -> Result<MassMeasure> {
|
||||
return Ok(MassMeasure { volume: m.volume, area: m.area, centroid: m.centroid, bbox: m.bbox, measured_by: m.measured_by, notes: m.notes });
|
||||
}
|
||||
let m = crate::guard("SMLib mass properties", || {
|
||||
b.smlib()?.mass_properties(ACCURACY).map_err(|e| crate::KernelError::smlib("SMLib mass properties", e))
|
||||
b.smlib()?.mass_properties(ACCURACY).map_err(|e| crate::KernelError::Failed(format!("SMLib mass properties: {e}")))
|
||||
})?;
|
||||
let [x, y, z] = m.centroid;
|
||||
Ok(MassMeasure { volume: m.volume, area: m.area, centroid: Vec3::new(x, y, z), bbox: b.exact_bounds()?, measured_by: "smlib", notes: Vec::new() })
|
||||
|
||||
@@ -523,6 +523,7 @@ 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;
|
||||
|
||||
@@ -87,7 +87,7 @@ pub fn brep_record(body: &Body) -> Result<solidcraft_geom::brep::Brep> {
|
||||
/// The body in SMLib (see [`Body::smlib`]), as an owned copy.
|
||||
pub fn smlib_body(body: &Body) -> Result<solidcraft_smlib::Brep> {
|
||||
let _span = solidcraft_profile::span("kernel", "smlib_body");
|
||||
body.smlib()?.try_clone().map_err(|e| KernelError::smlib("SMLib", e))
|
||||
body.smlib()?.try_clone().map_err(|e| KernelError::Failed(format!("SMLib: {e}")))
|
||||
}
|
||||
|
||||
/// Decode bounded untrusted data. Validate indices before topology extraction.
|
||||
@@ -140,7 +140,7 @@ pub(crate) fn body_from_smlib_ordered(brep: &solidcraft_smlib::Brep) -> Result<(
|
||||
/// the body's record does. `None` where it does not.
|
||||
pub(crate) fn smlib_body_ordered(body: &Body) -> Result<(solidcraft_smlib::Brep, Option<solidcraft_smlib::history::Order>)> {
|
||||
let brep = smlib_body(body)?;
|
||||
Ok((brep.try_clone().map_err(|e| KernelError::smlib("SMLib", e))?, numbering(body.record(), &brep)))
|
||||
Ok((brep.try_clone().map_err(|e| KernelError::Failed(format!("SMLib: {e}")))?, numbering(body.record(), &brep)))
|
||||
}
|
||||
|
||||
/// `record` in SMLib, imported as given (SMLib's own form; a record it refuses is an error that
|
||||
@@ -149,7 +149,7 @@ pub(crate) fn smlib_record_ordered(
|
||||
record: &solidcraft_geom::brep::Brep,
|
||||
) -> Result<(solidcraft_smlib::Brep, Option<solidcraft_smlib::history::Order>)> {
|
||||
let _span = solidcraft_profile::span("kernel", "smlib_record_ordered");
|
||||
let brep = solidcraft_smlib::Brep::from_record(record, true).map_err(|e| KernelError::smlib("SMLib", e))?;
|
||||
let brep = solidcraft_smlib::Brep::from_record(record, true).map_err(|e| KernelError::Failed(format!("SMLib: {e}")))?;
|
||||
let order = numbering(record, &brep);
|
||||
Ok((brep, order))
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ use crate::history::{Change, EdgeOrigin, FaceOrigin, History};
|
||||
use crate::native::{body_from_smlib_ordered, smlib_body_ordered};
|
||||
use crate::{KernelError, Result, record};
|
||||
|
||||
fn failed(op: &str, what: &str, e: impl crate::Cause) -> KernelError {
|
||||
KernelError::smlib(format!("SMLib {op}: {what}"), e)
|
||||
fn failed(op: &str, what: &str, e: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("SMLib {op}: {what}: {e}"))
|
||||
}
|
||||
|
||||
/// The error for SMLib's refusal of `op`, in the words of the operation; `wall` is its wall's
|
||||
@@ -39,11 +39,6 @@ fn refused(op: &str, wall: Option<f64>, e: SmError) -> KernelError {
|
||||
use a thinner or thicker wall, or move the feature"
|
||||
))
|
||||
}
|
||||
// solidcore's one construction: a face curves tighter than the wall is thick.
|
||||
SmError::WallFolds { largest } => {
|
||||
let largest = largest.map(|w| format!("; the largest wall that works is {} mm", crate::blend::trimmed_mm(w))).unwrap_or_default();
|
||||
KernelError::Invalid(format!("{op}: the wall is thicker than a face curves, so the face's offset would fold{largest}"))
|
||||
}
|
||||
SmError::NoMatch { point } => KernelError::Invalid(format!("{op}: no face at selection point {}", point + 1)),
|
||||
SmError::Ambiguous { point } => KernelError::Invalid(format!("{op}: selection point {} is as near to two faces", point + 1)),
|
||||
SmError::Unsupported => KernelError::Failed(format!("not supported yet: SMLib {op} for this geometry")),
|
||||
|
||||
@@ -206,9 +206,9 @@ fn moved_from(history: Option<&History>, f: usize) -> Option<usize> {
|
||||
|
||||
/// `body` moved by a matrix that is not a rigid motion, in SMLib, its history from SMLib's tags.
|
||||
fn transformed(body: &Body, m: &[[f64; 4]; 4]) -> Result<Body> {
|
||||
let failed = |what: &str, e: solidcraft_smlib::Error| KernelError::smlib(format!("SMLib transform: {what}"), e);
|
||||
let failed = |what: &str, e: &dyn std::fmt::Display| KernelError::Failed(format!("SMLib transform: {what}: {e}"));
|
||||
let (handle, order) = crate::native::smlib_body_ordered(body)?;
|
||||
let moved = handle.tagged(0).map_err(|e| failed("tag", e))?.transformed(m).map_err(|e| failed("operation", e))?;
|
||||
let moved = handle.tagged(0).map_err(|e| failed("tag", &e))?.transformed(m).map_err(|e| failed("operation", &e))?;
|
||||
let (result, result_order) = crate::native::body_from_smlib_ordered(&moved)?;
|
||||
let history = match (order, result_order) {
|
||||
(Some(o), Some(r)) => moved.history().ok().and_then(|h| h.history(&[o], &r).ok()),
|
||||
|
||||
@@ -4,26 +4,6 @@
|
||||
|
||||
use crate::{Result, guard};
|
||||
|
||||
/// The stack of a thread that runs kernel work: solidcore's algorithms recurse (subdivision,
|
||||
/// intersection, filleting), and its README asks for 8 MiB, a native main thread's stack, where
|
||||
/// Rust starts other threads with 2 MiB.
|
||||
pub const KERNEL_STACK: usize = 8 << 20;
|
||||
|
||||
/// A thread named `name` with the kernel's stack ([`KERNEL_STACK`]), for a thread that runs
|
||||
/// kernel work (an operation, an evaluation, a mesh).
|
||||
pub fn kernel_thread(name: &str) -> std::thread::Builder {
|
||||
std::thread::Builder::new().name(name.to_owned()).stack_size(KERNEL_STACK)
|
||||
}
|
||||
|
||||
/// The kernel's worker pool, its threads with the kernel's stack; `None` when it cannot start
|
||||
/// (the work then runs on the calling thread).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn pool() -> Option<&'static rayon::ThreadPool> {
|
||||
static POOL: std::sync::OnceLock<Option<rayon::ThreadPool>> = std::sync::OnceLock::new();
|
||||
POOL.get_or_init(|| rayon::ThreadPoolBuilder::new().thread_name(|i| format!("solidcraft-kernel-{i}")).stack_size(KERNEL_STACK).build().ok())
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
/// `f` over `items`, results in item order: on the pool when `parallel`, else on the calling
|
||||
/// thread. Waking the pool's workers costs tens of microseconds, so callers ask for it only
|
||||
/// when the items carry more work than that (decided from the work, not the item count).
|
||||
@@ -37,18 +17,8 @@ pub(crate) fn map<T: Sync, R: Send>(what: &'static str, items: &[T], parallel: b
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use rayon::prelude::*;
|
||||
if parallel && items.len() > 1 {
|
||||
if rayon::current_thread_index().is_some() {
|
||||
// On a pool's worker (the kernel's, or a one-thread pool of
|
||||
// `map_with_local_pools`): that pool's threads, as many as it has.
|
||||
if rayon::current_num_threads() > 1 {
|
||||
return items.par_iter().map(one).collect();
|
||||
}
|
||||
} else if let Some(pool) = pool()
|
||||
&& pool.current_num_threads() > 1
|
||||
{
|
||||
return pool.install(|| items.par_iter().map(one).collect());
|
||||
}
|
||||
if parallel && items.len() > 1 && rayon::current_num_threads() > 1 {
|
||||
return items.par_iter().map(one).collect();
|
||||
}
|
||||
}
|
||||
items.iter().map(one).collect()
|
||||
@@ -80,7 +50,6 @@ pub fn map_with_local_pools<T: Sync, R: Send>(items: &[T], max_threads: usize, f
|
||||
if cell.get().is_none() {
|
||||
let pool = rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(1)
|
||||
.stack_size(KERNEL_STACK)
|
||||
.build()
|
||||
.map_err(|e| crate::KernelError::Failed(format!("cannot start mesh worker: {e}")))?;
|
||||
let _ = cell.set(pool);
|
||||
@@ -122,7 +91,8 @@ pub fn map_on_threads<T: Sync, R: Send>(items: &[T], max_threads: usize, f: impl
|
||||
let mut out: Vec<Option<R>> = std::iter::repeat_with(|| None).take(items.len()).collect();
|
||||
std::thread::scope(|scope| {
|
||||
// A thread that cannot start leaves its share to the others.
|
||||
let helpers: Vec<_> = (1..threads).filter_map(|_| kernel_thread("solidcraft-each").spawn_scoped(scope, work).ok()).collect();
|
||||
let helpers: Vec<_> =
|
||||
(1..threads).filter_map(|_| std::thread::Builder::new().name("solidcraft-each".into()).spawn_scoped(scope, work).ok()).collect();
|
||||
let mut all = work();
|
||||
for h in helpers {
|
||||
// `work` never panics (every item runs under the guard).
|
||||
@@ -185,29 +155,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// `depth` frames of recursion, each holding 64 KiB on the stack, summing their depths: 80 is
|
||||
/// 5 MiB, past the 2 MiB Rust gives a thread and within the kernel's 8 MiB.
|
||||
fn deep(depth: u8) -> u64 {
|
||||
let frame = std::hint::black_box([depth; 64 * 1024]);
|
||||
let here = u64::from(frame[usize::from(depth)]);
|
||||
if depth == 0 { here } else { here + deep(depth - 1) }
|
||||
}
|
||||
|
||||
/// Kernel work recurses as deep as solidcore's algorithms do on every thread the kernel runs
|
||||
/// it on: a kernel thread, the pool's workers, and the helpers of `map_on_threads`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn kernel_threads_have_the_kernels_stack() {
|
||||
let want: u64 = (0..=80).sum();
|
||||
let on_thread = kernel_thread("deep").spawn(|| deep(80)).unwrap().join().unwrap();
|
||||
assert_eq!(on_thread, want);
|
||||
let items = [80u8; 16];
|
||||
let on_pool = kernel_thread("deep-pool").spawn(move || map("deep", &items, true, |&d| Ok(deep(d)))).unwrap().join().unwrap();
|
||||
assert!(on_pool.iter().all(|r| r.as_ref().ok() == Some(&want)), "{on_pool:?}");
|
||||
let on_helpers = kernel_thread("deep-helpers").spawn(move || map_on_threads(&items, 8, |&d| deep(d))).unwrap().join().unwrap();
|
||||
assert!(on_helpers.iter().all(|r| *r == Some(want)), "{on_helpers:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_order_and_errors() {
|
||||
let items: Vec<u32> = (0..1000).collect();
|
||||
|
||||
@@ -21,8 +21,8 @@ fn edge_middle(e: &brep::Edge) -> Option<Vec3> {
|
||||
CurveEval::new(e.parameters()).ok()?.point(0.5 * (t0 + t1))
|
||||
}
|
||||
|
||||
fn failed(e: impl crate::Cause) -> KernelError {
|
||||
KernelError::smlib("SMLib planar regularization", e)
|
||||
fn failed(e: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("SMLib planar regularization: {e}"))
|
||||
}
|
||||
|
||||
impl Body {
|
||||
|
||||
@@ -108,6 +108,7 @@ struct Regions {
|
||||
touching: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "smlib")]
|
||||
mod sheets {
|
||||
use solidcraft_geom::Loop2;
|
||||
use solidcraft_geom::Vec3;
|
||||
@@ -118,8 +119,8 @@ mod sheets {
|
||||
use super::*;
|
||||
use crate::prism_curve::{Run, projected};
|
||||
|
||||
fn failed(what: &str, e: impl crate::Cause) -> KernelError {
|
||||
KernelError::smlib(format!("SMLib planar regions: {what}"), e)
|
||||
fn failed(what: &str, e: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("SMLib planar regions: {what}: {e}"))
|
||||
}
|
||||
|
||||
/// `regions` (in the plane's own coordinates) as one sheet on z = 0. SMLib's face builder
|
||||
@@ -288,6 +289,30 @@ 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,7 +302,8 @@ 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.
|
||||
/// in a plane parallel to `plane` or isn't read exactly. Only planar sheets read on SMLib use it.
|
||||
#[cfg(feature = "smlib")]
|
||||
pub(crate) fn projected(run: &Run, plane: &Plane, tol: f64, work: &mut usize) -> Option<Vec<Seg2>> {
|
||||
read(run, plane, tol, work, true)
|
||||
}
|
||||
|
||||
@@ -192,10 +192,9 @@ mod tests {
|
||||
let k = row("kernel", "boolean").unwrap_or_else(|| panic!("{rep:?}"));
|
||||
assert!(k.count >= 1 && row("kernel", "box_solid").is_some(), "{rep:?}");
|
||||
if r.is_ok() {
|
||||
// The geometry kernel's calls 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:?}");
|
||||
// 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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1221,7 +1221,7 @@ fn sew_impl(faces: &[FaceSpec], touching: bool, sources: Option<&[Option<crate::
|
||||
}
|
||||
}
|
||||
let made = assemble(Parts { vertices, edges, faces: out, tolerance: tol }, true)?;
|
||||
let smlib = solidcraft_smlib::Brep::from_record(&made.brep, true).map_err(|e| KernelError::smlib("sew: SMLib", e))?;
|
||||
let smlib = solidcraft_smlib::Brep::from_record(&made.brep, true).map_err(|e| KernelError::Failed(format!("sew: SMLib: {e}")))?;
|
||||
let (body, order) = crate::native::body_from_smlib_ordered(&smlib)?;
|
||||
// The face of `faces` each face of the body was built from: record face `c` is SMLib's
|
||||
// face `c`, which the order places in the body.
|
||||
|
||||
@@ -16,8 +16,8 @@ mod smlib {
|
||||
use crate::validation::{Checked, Step};
|
||||
use crate::{KernelError, Result, record};
|
||||
|
||||
fn failed(what: &str, e: impl crate::Cause) -> KernelError {
|
||||
KernelError::smlib(format!("SMLib boolean: {what}"), e)
|
||||
fn failed(what: &str, e: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("SMLib boolean: {what}: {e}"))
|
||||
}
|
||||
|
||||
/// `a` and `b` combined by `op` in SMLib, with the history SMLib's tags give (none where a
|
||||
|
||||
@@ -93,8 +93,8 @@ mod imp {
|
||||
use crate::provenance::{closest_on_tri, face_tris};
|
||||
use crate::record;
|
||||
|
||||
fn failed(what: &str, e: impl crate::Cause) -> KernelError {
|
||||
KernelError::smlib(format!("SMLib {what}"), e)
|
||||
fn failed(what: &str, e: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("SMLib {what}: {e}"))
|
||||
}
|
||||
|
||||
fn corner(c: crate::DraftCorner) -> DraftCorner {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! 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};
|
||||
|
||||
|
||||
@@ -49,8 +49,8 @@ fn invalid(what: impl Into<String>) -> KernelError {
|
||||
KernelError::Invalid(what.into())
|
||||
}
|
||||
|
||||
fn failed(what: &str, e: impl crate::Cause) -> KernelError {
|
||||
KernelError::smlib(format!("SMLib {what}"), e)
|
||||
fn failed(what: &str, e: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("SMLib {what}: {e}"))
|
||||
}
|
||||
|
||||
fn a3(v: Vec3) -> [f64; 3] {
|
||||
|
||||
@@ -540,6 +540,17 @@ 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();
|
||||
@@ -2002,10 +2013,11 @@ 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 the kernel: the STEP reader's
|
||||
/// The step-conformance parts read into SMLib (`SOLIDCRAFT_KERNEL=smlib`): 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");
|
||||
|
||||
@@ -387,8 +387,8 @@ fn back_to_bounds(body: &Body, faces: &[usize]) -> Result<Body> {
|
||||
Ok(back)
|
||||
}
|
||||
|
||||
fn failed(what: &str, e: impl crate::Cause) -> KernelError {
|
||||
KernelError::smlib(format!("SMLib extend: {what}"), e)
|
||||
fn failed(what: &str, e: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("SMLib extend: {what}: {e}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -102,7 +102,7 @@ pub fn trim_keep(body: &Body, tool: &SplitTool, keep: TrimKeep) -> Result<Body>
|
||||
fn face_sides(body: &Body, side: &ToolSide) -> Result<Vec<f64>> {
|
||||
let (sm, order) = crate::native::smlib_body_ordered(body)?;
|
||||
let order = order.ok_or_else(|| KernelError::Failed("trim: SMLib numbers the pieces' faces differently from their record".into()))?;
|
||||
let points = sm.face_points().map_err(|e| KernelError::smlib("SMLib trim: a point inside each piece", e))?;
|
||||
let points = sm.face_points().map_err(|e| KernelError::Failed(format!("SMLib trim: a point inside each piece: {e}")))?;
|
||||
let mut sides = vec![f64::NAN; points.len()];
|
||||
for (i, p) in points.iter().enumerate() {
|
||||
let face = if order.faces.is_empty() { i } else { order.faces.get(i).copied().unwrap_or(usize::MAX) };
|
||||
|
||||
@@ -102,8 +102,8 @@ pub fn untrim(body: &Body, face: usize, holes: UntrimHoles, outer: bool, in_plac
|
||||
Ok(out.with_checked_history(history, &crate::record::counts(&[body])))
|
||||
}
|
||||
|
||||
fn failed(what: &str, e: impl crate::Cause) -> KernelError {
|
||||
KernelError::smlib(format!("SMLib untrim: {what}"), e)
|
||||
fn failed(what: &str, e: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("SMLib untrim: {what}: {e}"))
|
||||
}
|
||||
|
||||
/// A point inside face `face` of `body`: the centre of its largest mesh triangle.
|
||||
|
||||
@@ -1236,7 +1236,7 @@ mod imp {
|
||||
use super::*;
|
||||
|
||||
fn sm(what: &'static str) -> impl Fn(solidcraft_smlib::Error) -> KernelError {
|
||||
move |e| KernelError::smlib(format!("SMLib {what}"), e)
|
||||
move |e| failed(format!("SMLib {what}: {e}"))
|
||||
}
|
||||
|
||||
pub(super) fn refuse(what: &str) -> KernelError {
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
use super::*;
|
||||
|
||||
/// A wedge 40 x 20, 15 high, its top front edge along x from 0 to 40.
|
||||
fn wedge_body() -> Body {
|
||||
fn wedge() -> Body {
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Vec2};
|
||||
let outer = Loop2::polygon(&[Vec2::new(0., 0.), Vec2::new(40., 0.), Vec2::new(0., 20.)]);
|
||||
crate::extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0., 15.).unwrap().pop().unwrap()
|
||||
}
|
||||
|
||||
fn wedge() -> Body {
|
||||
let body = wedge_body();
|
||||
let body = crate::extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0., 15.).unwrap().pop().unwrap();
|
||||
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
|
||||
@@ -17,16 +12,6 @@ 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,6 +373,7 @@ 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};
|
||||
@@ -405,6 +406,7 @@ 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") };
|
||||
@@ -422,6 +424,7 @@ 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)
|
||||
@@ -429,6 +432,7 @@ 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);
|
||||
@@ -440,6 +444,7 @@ 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,
|
||||
@@ -450,6 +455,7 @@ 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();
|
||||
@@ -472,6 +478,7 @@ 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 };
|
||||
@@ -523,6 +530,7 @@ 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;
|
||||
@@ -554,6 +562,7 @@ 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") };
|
||||
@@ -578,6 +587,7 @@ 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,14 +1724,17 @@ fn tabbed_plate_area(da: f64, dt: f64) -> f64 {
|
||||
0.5 * (big * big * (PI + 2.0 * at) + tab) + (4.0 - dt) * (cy - yc)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
#[test]
|
||||
fn fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole() {
|
||||
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;
|
||||
Loop2 {
|
||||
let hole_y = 262.0 / 9.0;
|
||||
let outline = 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) },
|
||||
@@ -1740,44 +1743,7 @@ fn tabbed_plate_outline() -> Loop2 {
|
||||
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 closed form,
|
||||
/// 7.958672911836132 (its spandrel (1 − π/4) r² revolved about the disc's axis outside |x| < 4,
|
||||
/// solidcore#2176's test; the rim's End, `fillet_blend_corner`, builds it to 5e-8), and the
|
||||
/// corner arc's Pappus volume with flat ends, 0.15913583972191622 (the spandrel about the
|
||||
/// corner's axis at 2 − its centroid over a quarter turn), 8.117808752.
|
||||
#[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_672_911_836_132 + 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,6 +226,7 @@ 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,9 +54,22 @@ fn smlib_validity(body: &Body) -> Option<SmlibCheck> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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);
|
||||
/// 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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "solidcraft-profile"
|
||||
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"
|
||||
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"
|
||||
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: `solidcore`, `kernel`, `feature`, `command`, `import`, …
|
||||
/// What the span is: `smlib`, `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: a child of `outer`.
|
||||
record("external", "call", Duration::from_millis(2));
|
||||
// Timed by someone else (SMLib's C API): a child of `outer`.
|
||||
record("smlib", "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("external call") && t.contains("self time by category"), "{t}");
|
||||
assert!(t.contains("test outer") && t.contains("smlib call") && t.contains("self time by category"), "{t}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+25
-4
@@ -1,19 +1,40 @@
|
||||
[package]
|
||||
name = "solidcraft-smlib"
|
||||
description = "SolidCraft: the B-rep kernel, solidcore (tangency-labs/solidcore, the Rust port of NVIDIA's SMLib, Apache-2.0), behind a safe wrapper"
|
||||
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"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
links = "smlib"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
# 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 = []
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
solidcraft-geom = { workspace = true }
|
||||
solidcraft-profile = { workspace = true }
|
||||
solidcore = { workspace = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# The clocks SMLib's C library asks for in a browser build (src/ffi.rs).
|
||||
web-time = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
cc = { version = "1.2", features = ["parallel"] }
|
||||
|
||||
[dev-dependencies]
|
||||
solidcraft-profile = { workspace = true, features = ["enabled"] }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user