mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 13:01:52 +00:00
Merge origin/main into stacked #691
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011gJAwcLRBPQXYLmvXd3pWf # Conflicts: # crates/difftest/known_mismatches.toml # plan/findings.md # plan/upstream-issues.md
This commit is contained in:
+3
-2
@@ -1,4 +1,5 @@
|
||||
# Append-only logs: a local `git merge` keeps both sides' new entries instead of
|
||||
# conflicting. GitHub's own mergeability check ignores this, so merge main locally.
|
||||
# Frozen archives (new entries are one file each under plan/findings/ and
|
||||
# plan/upstream-issues/). Kept so branches that still append here merge locally;
|
||||
# GitHub's own mergeability check ignores merge=union.
|
||||
plan/findings.md merge=union
|
||||
plan/upstream-issues.md merge=union
|
||||
|
||||
+209
-28
@@ -38,6 +38,25 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# What this run must check. `code`: false only when a pull_request or merge_group change
|
||||
# touches nothing but docs (DOCS_ONLY below); those skip the build/test/difftest jobs, whose
|
||||
# required checks then report "skipped" (passing). `full`: code and not a push to main. A
|
||||
# push to main was already tested on the identical tree by the merge queue, so it runs only
|
||||
# what publishes main's artifacts (difftest reports for the next PRs' exact-base ratchet,
|
||||
# solids report); everything else is skipped there.
|
||||
changes:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
outputs:
|
||||
code: ${{ steps.c.outputs.code }}
|
||||
full: ${{ steps.c.outputs.full }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: c
|
||||
env:
|
||||
EVENT: ${{ github.event_name }}
|
||||
BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
|
||||
run: scripts/ci/changes.sh "$EVENT" "$BASE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
ci-scripts:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
@@ -50,8 +69,14 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- run: rustup show active-toolchain || rustup toolchain install
|
||||
- run: cargo fmt --all --check
|
||||
- name: No new entries in frozen archives (one file per finding / upstream issue)
|
||||
env:
|
||||
BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before }}
|
||||
run: scripts/ci/check-frozen-archives.sh "$BASE"
|
||||
|
||||
clippy:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.full == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
env:
|
||||
SOLIDCORE_CPP_DIR: ${{ github.workspace }}/.cpp
|
||||
@@ -69,6 +94,8 @@ jobs:
|
||||
run: cargo clippy --workspace --all-targets --features difftest/cpp -- -D warnings
|
||||
|
||||
test:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
env:
|
||||
SOLIDCORE_CPP_DIR: ${{ github.workspace }}/.cpp
|
||||
@@ -80,13 +107,15 @@ jobs:
|
||||
# tests/primitives_solid.rs takes minutes unoptimised, so its four tests are
|
||||
# skipped here and run once, optimised, in the next step (the difftest-profile
|
||||
# solidcore it builds is the one the runner tests below reuse).
|
||||
- run: >-
|
||||
- if: needs.changes.outputs.full == 'true'
|
||||
run: >-
|
||||
cargo test --workspace -- --exact
|
||||
--skip complete_spheres_and_tori_have_analytic_properties
|
||||
--skip primitives_are_closed_solids_with_analytic_properties
|
||||
--skip precise_sphere_properties_scale_and_translate
|
||||
--skip pole_free_sphere_tolerance_is_explicit_and_validated
|
||||
- name: Primitives solid tests (difftest profile)
|
||||
if: needs.changes.outputs.full == 'true'
|
||||
run: cargo test --profile difftest -p solidcore --test primitives_solid
|
||||
# Solids: the primitives example's per-shape results (built, blocker, checks) for
|
||||
# the site's Solids page. It reuses the difftest-profile build above and exits 1
|
||||
@@ -112,10 +141,14 @@ jobs:
|
||||
# [profile.difftest]); here rather than after the difftest run, so they no longer
|
||||
# add to its wall time.
|
||||
- uses: ./.github/actions/cpp-reference
|
||||
if: needs.changes.outputs.full == 'true'
|
||||
- name: Difftest runner tests
|
||||
if: needs.changes.outputs.full == 'true'
|
||||
run: cargo test --profile difftest -p difftest --features cpp
|
||||
|
||||
wasm:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.full == 'true'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -127,6 +160,8 @@ jobs:
|
||||
|
||||
# Same packaging check the release workflow runs before publishing (no secrets).
|
||||
publish-dry-run:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.full == 'true'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -136,6 +171,8 @@ jobs:
|
||||
- run: cargo publish -p solidcore --locked --dry-run
|
||||
|
||||
deny:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.full == 'true'
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -143,16 +180,65 @@ jobs:
|
||||
with:
|
||||
command: check licenses bans sources
|
||||
|
||||
difftest:
|
||||
# Starts with the other jobs (no `needs`): it is the critical path, and the merge queue
|
||||
# requires every job anyway. Clippy with the cpp feature and the runner tests run in
|
||||
# the clippy and test jobs, in parallel with this one.
|
||||
# The difftest gate is split so its slow parts overlap (they used to run one after
|
||||
# another: exact-base wait, sweep, perturbation). `difftest-baseline` fetches and
|
||||
# validates the exact-base artifact while `difftest-sweep` runs the green sweep and
|
||||
# `difftest-perturb` the (mandatory) perturbation check; `difftest-compare` then holds the
|
||||
# sweep's finished reports to the validated base with the same ratchet as `--baseline`;
|
||||
# `difftest` (the required check's name) fails unless every part succeeded.
|
||||
difftest-baseline:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.full == 'true'
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read # reads the exact base revision's run and report artifact
|
||||
outputs:
|
||||
baseline: ${{ steps.validate.outputs.baseline }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Fail closed: any validation error fails this step (and so the job and `difftest`).
|
||||
# Only a genuine bootstrap/diagnostic run, which the helper reports by printing
|
||||
# nothing, continues without a base.
|
||||
- name: Validate exact-base report artifact
|
||||
id: validate
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
EVENT: ${{ github.event_name }}
|
||||
BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || inputs.baseline_sha }}
|
||||
run: |
|
||||
arg=$(scripts/ci/difftest-baseline.sh "$EVENT" "$BASE" "$RUNNER_TEMP/difftest-baseline" --current-plan "$GITHUB_WORKSPACE")
|
||||
if [ -n "$arg" ]; then
|
||||
echo "baseline=yes" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "baseline=no" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Hand the validated reports and their provenance to difftest-compare.
|
||||
- name: Upload validated baseline
|
||||
if: steps.validate.outputs.baseline == 'yes'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: difftest-validated-baseline
|
||||
path: ${{ runner.temp }}/difftest-baseline
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
difftest-sweep:
|
||||
# Starts right after `changes`: it is the critical path. Clippy with the cpp feature and
|
||||
# the runner tests run in the clippy and test jobs, in parallel with this one. Also runs on
|
||||
# pushes to main, where its reports are main's published set.
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read # scripts/fetch-reports.sh reads main's report artifact
|
||||
env:
|
||||
SOLIDCORE_CPP_DIR: ${{ github.workspace }}/.cpp
|
||||
outputs:
|
||||
any: ${{ steps.green.outputs.any }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -166,15 +252,6 @@ jobs:
|
||||
echo "No current green units; exact-base retention is still required."
|
||||
fi
|
||||
|
||||
- name: Validate exact-base report artifact
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
EVENT: ${{ github.event_name }}
|
||||
BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || inputs.baseline_sha }}
|
||||
run: |
|
||||
arg=$(scripts/ci/difftest-baseline.sh "$EVENT" "$BASE" "$RUNNER_TEMP/difftest-baseline" --current-plan "$GITHUB_WORKSPACE")
|
||||
echo "DIFFTEST_BASELINE=$arg" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Upstream sources, sccache and the C++ reference
|
||||
if: steps.green.outputs.any == 'true'
|
||||
uses: ./.github/actions/cpp-reference
|
||||
@@ -198,22 +275,13 @@ jobs:
|
||||
if: steps.green.outputs.any == 'true'
|
||||
# Optimised profile (Cargo.toml [profile.difftest]): identical results, about 20x
|
||||
# faster than the debug build that used to take ~35 min here. --jobs 0: one child
|
||||
# process per unit, one per vCPU (same output and reports as a serial run).
|
||||
run: |
|
||||
baseline=()
|
||||
if [ -n "$DIFFTEST_BASELINE" ]; then
|
||||
baseline=(--baseline "$RUNNER_TEMP/difftest-baseline/reports")
|
||||
fi
|
||||
cargo run --profile difftest -p difftest --features cpp -- --green --jobs 0 "${baseline[@]}"
|
||||
# process per unit, one per vCPU (same output and reports as a serial run). The
|
||||
# exact-base ratchet is applied afterwards by difftest-compare, to
|
||||
# these reports; the per-unit status and value-share gates run here.
|
||||
run: cargo run --profile difftest -p difftest --features cpp -- --green --jobs 0
|
||||
|
||||
- name: Difftest perturbation check
|
||||
if: steps.green.outputs.any == 'true'
|
||||
run: cargo run --profile difftest -p difftest --features cpp -- --green --perturb --jobs 0 --no-report
|
||||
|
||||
# main's reports are published from here (site, PROGRESS.md case counts,
|
||||
# scripts/fetch-reports.sh), never committed.
|
||||
- name: Keep only reports of green/partial units
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.green.outputs.any == 'true' && !cancelled()
|
||||
if: steps.green.outputs.any == 'true' && !cancelled()
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import pathlib, sys
|
||||
@@ -225,6 +293,18 @@ jobs:
|
||||
p.unlink()
|
||||
PY
|
||||
|
||||
# Carries the finished reports to difftest-compare (run-internal, short-lived).
|
||||
- name: Upload sweep reports
|
||||
if: steps.green.outputs.any == 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: difftest-sweep-reports
|
||||
path: reports/difftest/*.json
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# main's reports are published from here (site, PROGRESS.md case counts,
|
||||
# scripts/fetch-reports.sh), never committed.
|
||||
- name: Upload difftest reports
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.green.outputs.any == 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -239,3 +319,104 @@ jobs:
|
||||
- name: sccache stats
|
||||
if: always() && steps.green.outputs.any == 'true'
|
||||
run: sccache --show-stats || true
|
||||
|
||||
# Mandatory on every pull_request and merge_group run, in parallel with the sweep.
|
||||
difftest-perturb:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.full == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
env:
|
||||
SOLIDCORE_CPP_DIR: ${{ github.workspace }}/.cpp
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Select optional native execution
|
||||
id: green
|
||||
run: |
|
||||
if grep -Eqs '^\s*status\s*=\s*"(green|partial)"' plan/units/*.toml; then
|
||||
echo "any=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "any=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No current green units; nothing to perturb."
|
||||
fi
|
||||
|
||||
- name: Upstream sources, sccache and the C++ reference
|
||||
if: steps.green.outputs.any == 'true'
|
||||
uses: ./.github/actions/cpp-reference
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: steps.green.outputs.any == 'true'
|
||||
with:
|
||||
workspaces: ". -> bin"
|
||||
key: difftest
|
||||
|
||||
- name: Difftest perturbation check
|
||||
if: steps.green.outputs.any == 'true'
|
||||
run: cargo run --profile difftest -p difftest --features cpp -- --green --perturb --jobs 0 --no-report
|
||||
|
||||
- name: sccache stats
|
||||
if: always() && steps.green.outputs.any == 'true'
|
||||
run: sccache --show-stats || true
|
||||
|
||||
# The exact-base ratchet. Needs both the validated base and the finished sweep: if either
|
||||
# job fails or is skipped, this job is skipped and `difftest` fails.
|
||||
difftest-compare:
|
||||
needs: [changes, difftest-baseline, difftest-sweep]
|
||||
if: needs.changes.outputs.full == 'true'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: needs.difftest-baseline.outputs.baseline == 'yes' && needs.difftest-sweep.outputs.any == 'true'
|
||||
with:
|
||||
workspaces: ". -> bin"
|
||||
key: difftest-compare
|
||||
|
||||
- name: Download validated baseline
|
||||
if: needs.difftest-baseline.outputs.baseline == 'yes'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: difftest-validated-baseline
|
||||
path: ${{ runner.temp }}/difftest-baseline
|
||||
|
||||
- name: Download sweep reports
|
||||
if: needs.difftest-sweep.outputs.any == 'true'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: difftest-sweep-reports
|
||||
path: ${{ runner.temp }}/difftest-sweep
|
||||
|
||||
# No C++ and no cases: the same ratchet as --baseline, over the finished reports.
|
||||
- name: Compare sweep reports with the exact base
|
||||
if: needs.difftest-baseline.outputs.baseline == 'yes' && needs.difftest-sweep.outputs.any == 'true'
|
||||
run: >-
|
||||
cargo run -p difftest --
|
||||
--compare-reports "$RUNNER_TEMP/difftest-sweep"
|
||||
--baseline "$RUNNER_TEMP/difftest-baseline/reports"
|
||||
|
||||
# The required check ("difftest"). Runs even when a part failed or was skipped, and
|
||||
# succeeds only if every part succeeded.
|
||||
difftest:
|
||||
needs: [changes, difftest-baseline, difftest-sweep, difftest-perturb, difftest-compare]
|
||||
if: always()
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Require every difftest part
|
||||
env:
|
||||
CHANGES: ${{ needs.changes.result }}
|
||||
FULL: ${{ needs.changes.outputs.full }}
|
||||
SWEEP: ${{ needs.difftest-sweep.result }}
|
||||
RESULTS: ${{ join(needs.*.result, ' ') }}
|
||||
run: |
|
||||
echo "changes difftest-baseline difftest-sweep difftest-perturb difftest-compare: $RESULTS"
|
||||
[ "$CHANGES" = success ] || { echo "the changes job did not succeed"; exit 1; }
|
||||
if [ "$FULL" != true ]; then
|
||||
# docs-only change (everything skipped), or a push to main (only the sweep, which
|
||||
# publishes main's reports, runs; the merge queue already ran the full gate)
|
||||
case "$SWEEP" in success|skipped) echo "not a full run (full=$FULL)"; exit 0;; esac
|
||||
echo "difftest-sweep: $SWEEP"; exit 1
|
||||
fi
|
||||
for r in $RESULTS; do
|
||||
[ "$r" = success ] || { echo "a difftest part did not succeed"; exit 1; }
|
||||
done
|
||||
|
||||
@@ -9,7 +9,7 @@ crates/solidcore the kernel (pure Rust, no unsafe, wasm-ok) owner:
|
||||
crates/solidcore-ffi extern "C" bindings to the C++ kernel (feature cpp)
|
||||
crates/difftest side-by-side Rust vs C++ runner
|
||||
harness/cpp C++ reference build (build.sh -> bin/cpp) + shim/<unit>.cpp
|
||||
plan/ units/<id>.toml (one file per unit), STATUS.md, findings.md, order.md
|
||||
plan/ units/<id>.toml, findings/<slug>.md, upstream-issues/<slug>.md (one file each), STATUS.md, order.md
|
||||
```
|
||||
|
||||
## 1. Hard rules
|
||||
@@ -51,7 +51,7 @@ anywhere (including dependencies; `deny.toml` bans copyleft crates).
|
||||
(via FFI) on the same inputs and topology is identical and every value agrees within
|
||||
`1e-9` relative (`|a-b| <= 1e-9 * max(|a|,|b|)`; an absolute floor only where a unit
|
||||
documents one per key in `abs_floors()`, recorded in its report). Never loosen a tolerance to get green. A disagreement you cannot fix
|
||||
goes in `plan/findings.md` (unit, inputs, both values, suspected cause) and the unit
|
||||
goes in a new `plan/findings/<slug>.md` (unit, inputs, both values, suspected cause) and the unit
|
||||
stays `blocked`.
|
||||
|
||||
**Git.** Work happens through pull requests to github.com/tangency-labs/solidcore. Never
|
||||
@@ -141,7 +141,11 @@ Every ported file starts with:
|
||||
|
||||
Unit list and states: `plan/units/<unit id>.toml`, one file per unit (format:
|
||||
`plan/modules.schema.md`, order: `plan/order.md`). **`plan/PROGRESS.md` is the checklist** (regenerated on `main` by CI;
|
||||
never edit it in a PR). **Any `todo` unit can be claimed.** A unit is *ready* when its `depends_on` are all
|
||||
never edit it in a PR). **Any `todo` unit or unclaimed completion task can be claimed.** A claim reserves
|
||||
its stated scope, not every future change to the unit. Missing functions, back-edge
|
||||
wiring, exact runner imports, fixtures and demonstrated parity bugs remain useful
|
||||
work in `partial` and `green` units; inspect source and reports, not status alone.
|
||||
A unit is *ready* when its `depends_on` are all
|
||||
`green` or `partial` (`soft_depends_on` never blocks; see section 4a); ready units are
|
||||
the easiest, lowest `layer` first. Claiming a unit that is not ready is fine:
|
||||
- Calls into a missing dependency are stubbed (`UNPORTED(<unit>)`, cases deferred; 4a).
|
||||
@@ -156,14 +160,44 @@ the easiest, lowest `layer` first. Claiming a unit that is not ready is fine:
|
||||
into `upstream/` (see README), then run `scripts/fetch-cpp.sh`, or
|
||||
`nice -n 10 harness/cpp/build.sh --jobs 6` to build from source.
|
||||
|
||||
1. **Claim = a draft PR.** Check nobody has it:
|
||||
`gh pr list --state open --search "<unit id> in:title"`. If none, branch from the
|
||||
latest `main` (`git fetch origin && git switch -c unit/<unit id> origin/main`),
|
||||
`git commit --allow-empty -m "claim: <unit id>"`, push, and open a draft PR titled
|
||||
exactly **`port: <unit id>`**. **The lowest-numbered open PR for a unit wins**; if
|
||||
yours lost, close it. Do not edit `plan/units/` to claim. Push WIP at least
|
||||
daily; a draft PR with no pushes for 48 h may be taken over (comment on it first).
|
||||
If the unit's `notes` mention a WIP branch on `origin`, start from it.
|
||||
1. **Claim = a scoped draft PR.** Search open PR titles and bodies for the unit,
|
||||
symbols and affected paths, including `wire:`, `fix:` and `difftest:` work; a title
|
||||
search for `port:` alone misses completion work. Branch from the latest `main`.
|
||||
Do not edit `plan/units/` to claim.
|
||||
Initial whole-unit ports keep the title **`port: <unit id>`**. For completion work,
|
||||
use a descriptive title such as **`wire: <unit id> — <function group>`** and name
|
||||
the unit in the body. An empty claim commit is sufficient to open a draft.
|
||||
|
||||
Include this scope in the PR body (update it as the work changes):
|
||||
```text
|
||||
Units:
|
||||
In scope: upstream symbols, Rust functions and intended behavior
|
||||
Expected edits: paths and shared interfaces
|
||||
Evidence: failing/deferred case IDs or missing branches; validation planned
|
||||
Related PRs / dependencies:
|
||||
Available follow-ups: independent work others can take
|
||||
Status / blockers / next step:
|
||||
```
|
||||
For overlapping whole-unit `port:` claims, the lowest-numbered open PR wins.
|
||||
For completion tasks, the earliest open claim wins only for overlapping work.
|
||||
Non-overlapping work in
|
||||
the same unit is welcome. Link the related PR and state the boundary before
|
||||
starting; independent work need not wait for a whole-unit owner to reply.
|
||||
If work changes the same function or shared interface, agree on a split in the
|
||||
existing PR first, or build on its branch and document the dependency. If the
|
||||
boundary is unclear, ask there and take a different independent task meanwhile.
|
||||
Never implement a competing version of an actively claimed change.
|
||||
|
||||
Push WIP or post a concrete progress update at least daily while implementing.
|
||||
After 48 h without either, comment with the exact scope you propose to take over
|
||||
and allow 24 h for a response; then continue from published WIP, crediting its
|
||||
author. Do not take over a PR merely because it awaits review or CI: offer review,
|
||||
conflict resolution or a coordinated follow-up instead. A blocker update should
|
||||
identify what is blocked and release independent work, rather than reserve the
|
||||
whole unit indefinitely. When pausing, release the scope and link the WIP.
|
||||
Existing PR authors retain their active scope; this is not a reset of claims.
|
||||
If the unit's notes mention a WIP branch, inspect it before starting.
|
||||
|
||||
2. **Port** the upstream functions the unit owns (listed in
|
||||
`harness/callgraph/units.json`) into its `rust_module`, plus the C++ shim
|
||||
(`harness/cpp/shim/<unit_id_with_underscores>.cpp`, functions `sc_<unit>_<fn>`, plain
|
||||
@@ -230,11 +264,17 @@ into `upstream/` (see README), then run `scripts/fetch-cpp.sh`, or
|
||||
there locally; CI regenerates them, uploads main's set as the `difftest-reports`
|
||||
artifact, and the site and `plan/PROGRESS.md` read that
|
||||
(`scripts/fetch-reports.sh` downloads it). Quote your summary lines in the PR instead.
|
||||
- `plan/findings.md` and `plan/upstream-issues.md` are append-only: add new entries,
|
||||
do not rewrite old ones. `.gitattributes` gives them `merge=union`, so a local
|
||||
`git merge origin/main` keeps both sides' entries. GitHub's own conflict check
|
||||
ignores that attribute, so when GitHub says your PR conflicts, merge main locally
|
||||
and push.
|
||||
- **Never append to a shared file.** Every finding and every upstream issue is its own
|
||||
new file: `plan/findings/<slug>.md` and `plan/upstream-issues/<slug>.md` (formats in
|
||||
each directory's `README.md`). Records are never edited after they merge (like ADRs):
|
||||
an update is an addendum file. The old shared `plan/findings.md` and
|
||||
`plan/upstream-issues.md` were split into those directories; CI fails if either comes
|
||||
back. Two PRs that each add a file never conflict.
|
||||
Known difftest mismatches likewise live in `crates/difftest/known_mismatches/<unit id>.toml`
|
||||
(one file per unit; see `crates/difftest/src/known.rs`), and SolidCraft cases in
|
||||
`fixtures/solidcraft/<issue>-<slug>.json` (one file per issue).
|
||||
The same goes for anything new you design: a registry or log is a directory of
|
||||
per-entry files that tools read in sorted order, never one file everybody appends to.
|
||||
- **Branches from before the per-unit split** (they edit `plan/modules.toml` or commit
|
||||
reports): merge main, then run the migration, which moves your `modules.toml` edits
|
||||
into `plan/units/*.toml` and untracks your reports:
|
||||
@@ -245,10 +285,33 @@ into `upstream/` (see README), then run `scripts/fetch-cpp.sh`, or
|
||||
lists any other conflicted files for you to resolve before committing.
|
||||
|
||||
**Blocked** (gate cannot be met, upstream behaviour unclear): record details in
|
||||
`plan/findings.md`, set `status = "blocked"` with a reason in `notes`, and open the PR
|
||||
`plan/findings/<slug>.md`, set `status = "blocked"` with a reason in `notes`, and open the PR
|
||||
with just that so others can see it. Never sit on a draft PR you have stopped working
|
||||
on: close it, and say where the WIP is.
|
||||
|
||||
### Completion evidence
|
||||
|
||||
For new ports and completion changes, include the [completion evidence checklist](docs/completion-evidence.md)
|
||||
in the PR description. The evidence table is required for `wire:` PRs that remove
|
||||
`UNPORTED` stubs, including when existing fixtures do not reach the continuation. Identify the owned functions and branches implemented, which
|
||||
operations actually complete successfully against C++, and which remain unreachable.
|
||||
A passing status or empty result is evidence for that case, not proof that the
|
||||
operation's successful construction or mutation path works.
|
||||
|
||||
A dependency stub replaces the missing call only. Port the upstream logic following
|
||||
it as well, preserving control flow and operation order; do not omit the continuation
|
||||
and present the function as complete. If an incremental PR deliberately leaves an
|
||||
owned body or continuation missing, list the exact symbols and remaining work.
|
||||
Report that limitation even if existing fixtures never reach it. Unit status follows
|
||||
the existing gate; it does not certify complete function or branch coverage.
|
||||
|
||||
When a dependency is wired, rerun the affected callers and add cases that reach the
|
||||
previously blocked continuations. Keep newly exposed mismatches visible. Review
|
||||
runner skips, stand-in geometry and absent-entity cases before claiming coverage.
|
||||
Record independent harness follow-ups with reproducible evidence and check existing
|
||||
PRs before implementing them. This checklist supplements the existing correctness
|
||||
gate; it does not change tolerances, outcomes or status accounting.
|
||||
|
||||
## 4a. Cycles, back-edges and the merge lock
|
||||
|
||||
The kernel core is one big call cycle, so the plan dropped ~2,300 calls as *back-edges*
|
||||
@@ -279,10 +342,13 @@ The kernel core is one big call cycle, so the plan dropped ~2,300 calls as *back
|
||||
- `plan/units/<id>.toml`: one unit's plan entry (status, claims, notes). Edit only
|
||||
your unit's file. Every tool reads them through `harness/plan/units.py` (Rust:
|
||||
`crates/difftest/src/plan.rs`).
|
||||
- `plan/findings.md`: Rust-vs-C++ disagreements, upstream bugs, nondeterminism (e.g.
|
||||
upstream `srand(time(NULL))`), anything a later agent must know. Append-only.
|
||||
- `plan/upstream-issues.md`: upstream C++ defects also get an entry here (problems only,
|
||||
with location, trigger, how found and whether the C++ build confirmed it). Append-only.
|
||||
- `plan/findings/<slug>.md`, one file per finding: Rust-vs-C++ disagreements, upstream
|
||||
bugs, nondeterminism (e.g. upstream `srand(time(NULL))`), anything a later agent must
|
||||
know. A reference to `plan/findings.md: <heading>` (older code comments): grep the
|
||||
heading under `plan/findings/`.
|
||||
- `plan/upstream-issues/<slug>.md`, one file per issue: upstream C++ defects (problems
|
||||
only, with location, trigger, how found and whether the C++ build confirmed it). The
|
||||
site reads them all. A `plan/upstream-issues.md` ID: grep it under that directory.
|
||||
- `reports/difftest/<unit>.json`: difftest output. Not committed: CI uploads main's set
|
||||
as an artifact; `scripts/fetch-reports.sh` downloads it.
|
||||
- `plan/PROGRESS.md`: the checklist (generated; never edit by hand).
|
||||
|
||||
Generated
+30
@@ -28,7 +28,9 @@ dependencies = [
|
||||
"serde_json",
|
||||
"solidcore",
|
||||
"solidcore-ffi",
|
||||
"solidcore-ssi",
|
||||
"ssi",
|
||||
"step-import",
|
||||
"toml",
|
||||
]
|
||||
|
||||
@@ -198,12 +200,40 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"serde_json",
|
||||
"step-import",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcore-ssi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"solidcore",
|
||||
"ssi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ssi"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "step-brep"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"solidcore",
|
||||
"step-import",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "step-export"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"step-import",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "step-import"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/solidcore", "crates/solidcore-ffi", "crates/difftest", "crates/nurbs-grid-mesh", "crates/ssi"]
|
||||
members = ["crates/solidcore", "crates/solidcore-ffi", "crates/difftest", "crates/nurbs-grid-mesh", "crates/ssi", "crates/solidcore-ssi", "crates/step-brep", "crates/step-export", "crates/step-import"]
|
||||
default-members = ["crates/solidcore"]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -26,12 +26,17 @@ C++ to `1e-9` relative with identical topology, verified unit by unit.
|
||||
|
||||
## Status
|
||||
|
||||
About a third ported and verified against the C++: the math, NURBS and curve layers,
|
||||
with surfaces and topology under way. **Checklist: [`plan/PROGRESS.md`](plan/PROGRESS.md)**
|
||||
(updated automatically). Notes and handoff: [`plan/STATUS.md`](plan/STATUS.md).
|
||||
The same data, browsable, is on the progress site built by
|
||||
The planned kernel modules have Rust ports, and primitive construction is exercised
|
||||
end to end. Integration remains in progress: partial units, deferred paths and known
|
||||
mismatches still limit coverage. The API is experimental (0.0.x).
|
||||
|
||||
Use the [live unit results](https://tangency-labs.github.io/solidcore/units/index.html)
|
||||
for current differential coverage and blockers, and the
|
||||
[primitive checks](https://tangency-labs.github.io/solidcore/solids.html) for whole-solid
|
||||
results. Per-unit plans and notes live in [`plan/units/`](plan/units/); the generated
|
||||
checklist is [`plan/PROGRESS.md`](plan/PROGRESS.md), with handoff notes in
|
||||
[`plan/STATUS.md`](plan/STATUS.md). The progress site is built by
|
||||
[`site/build.py`](site/README.md) (`python3 site/build.py` -> `bin/site/`).
|
||||
Nothing is usable end to end yet (no primitives, booleans or fillets).
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -96,8 +101,8 @@ Rust-only builds do not need these C++ build tools.
|
||||
git clone https://github.com/NVIDIA-Omniverse/usd-brep upstream
|
||||
git -C upstream checkout c9c5979
|
||||
scripts/fetch-cpp.sh # C++ reference -> bin/cpp (prebuilt, or built)
|
||||
cargo run -p difftest --features cpp -- --green # all units marked green
|
||||
cargo run -p difftest --features cpp -- --unit <unit id> # one unit
|
||||
cargo run --profile difftest -p difftest --features cpp -- --green # all green and partial units
|
||||
cargo run --profile difftest -p difftest --features cpp -- --unit <unit id> # one unit
|
||||
```
|
||||
|
||||
`solidcore-ffi` and `difftest` compile without the C++ build; only the `cpp` feature
|
||||
@@ -107,16 +112,29 @@ so they are cached like the Rust crates.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are pull requests, one per port unit, and AI agents are welcome:
|
||||
Contributions go through pull requests, and AI agents are welcome. Useful work includes
|
||||
connecting ported units, resolving deferred comparisons, adding operation regressions
|
||||
and fixing bugs.
|
||||
|
||||
1. Pick any unclaimed unit in [`plan/PROGRESS.md`](plan/PROGRESS.md). "Ready" ones are
|
||||
easiest; the others need stubs or co-porting of missing dependencies.
|
||||
2. Claim it by opening a draft PR titled `port: <unit id>` (lowest-numbered PR wins).
|
||||
3. Port it, prove it with difftest against the C++ reference, mark the PR ready.
|
||||
1. Check the [live unit results](https://tangency-labs.github.io/solidcore/units/index.html),
|
||||
[open issues](https://github.com/tangency-labs/solidcore/issues) and
|
||||
[open PRs](https://github.com/tangency-labs/solidcore/pulls). Choose an unclaimed
|
||||
unit or a specific completion task: a missing function, back-edge, exact import,
|
||||
coverage gap or parity bug. Green reports do not prove every branch is exercised.
|
||||
2. Search PR titles and bodies for the unit, symbols and paths. Claim the specific
|
||||
work in a draft PR; initial ports use `port: <unit id>`. A claim covers its stated
|
||||
scope, leaving independent tasks in the same unit available. Coordinate overlapping
|
||||
functions or shared interfaces in the existing PR before editing them.
|
||||
3. Describe the intended behavior, affected paths, related PRs and validation, and
|
||||
list independent follow-ups others can take. Compare against C++ and rerun affected
|
||||
units with existing tolerances and coverage. CI is the merge gate. See
|
||||
[AGENTS.md, section 4](AGENTS.md#4-porting-a-unit-pr-workflow) for the claim template,
|
||||
handoffs and inactivity rules.
|
||||
|
||||
To keep parallel PRs from conflicting: edit only your unit's `plan/units/<unit id>.toml`,
|
||||
never commit `reports/difftest/` (CI generates them), and only append to
|
||||
`plan/findings.md` and `plan/upstream-issues.md`. A branch started before the per-unit
|
||||
never commit `reports/difftest/` (CI generates them), and put each finding or upstream issue
|
||||
in its own new file under `plan/findings/` or `plan/upstream-issues/` (never append to a
|
||||
shared file). A branch started before the per-unit
|
||||
split migrates with `git fetch origin && git merge origin/main;
|
||||
scripts/units-migrate-branch.sh && git commit --no-edit`.
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ solidcore = { path = "../solidcore" }
|
||||
solidcore-ffi = { path = "../solidcore-ffi" }
|
||||
# The `ssi` engine of --ssi-corpus (a surface/surface intersection of its own, not a port).
|
||||
ssi = { path = "../ssi" }
|
||||
# The `solidcore-ssi` engine of --ssi-corpus (`ssi` behind the port's inputs and output).
|
||||
solidcore-ssi = { path = "../solidcore-ssi" }
|
||||
# The clean-room STEP importer of --step-corpus.
|
||||
step-import = { path = "../step-import" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
# float_roundtrip: parse decimals exactly (correctly rounded, as C strtod), not to within 1 ULP
|
||||
serde_json = { version = "1", features = ["float_roundtrip"] }
|
||||
|
||||
@@ -38,12 +38,14 @@ nice -n 10 cargo test -p difftest --features cpp # harness s
|
||||
| `--seeds N` | generated cases per unit (default 16) |
|
||||
| `--filter S` | only cases whose name contains `S` |
|
||||
| `--perturb` | change one item of each case's Rust output (below); passes only if **every** perturbed case is caught. No report written. |
|
||||
| `--compare-reports R` | with `--baseline D` only: run no case, apply the same ratchet to the finished reports in `R` (CI's `difftest-compare` job; needs no C++) |
|
||||
| `--baseline D` | regression ratchet: compare each unit with its earlier report `D/<unit>.json` (see [Regression gate](#regression-gate---baseline-value-share)). CI passes the exact base revision's validated report artifact |
|
||||
| `--report-dir D` / `--no-report` | report location (default `reports/difftest/`) / skip writing |
|
||||
| `--fuzz N` | with `--unit`: probe the unit's FFI guard with N mutated cases, each run in a child process (below). No report written. |
|
||||
| `--fuzz-out D` | where `--fuzz` writes failing inputs (default `bin/fuzz/`; files go to `D/<unit>/`) |
|
||||
| `--ssi-corpus [--tol T] [--jobs N] [glob]` | score the engines against the SSI corpus (see [below](#--ssi-corpus-exact-ground-truth-ssi-corpus)); not a gate; without the `cpp` feature, the `ssi` engine alone |
|
||||
| `--solidcraft-cases [glob]` | score the Rust port on SolidCraft's whole-operation regression cases (see [below](#--solidcraft-cases-solidcraft-regression-cases)); not a gate, no `cpp` feature needed |
|
||||
| `--step-corpus [--jobs N] [glob]` | import STEP Tools' AP203/AP214 sample files and build every B-rep body on both kernels from the same intermediate (see [below](#--step-corpus-real-world-step-files)); not a gate; without `cpp`, the Rust side alone |
|
||||
|
||||
Exit status: `0` no failures (deferred cases are not failures), `1` any
|
||||
failure, `2` usage or setup error. Output: one summary line per unit,
|
||||
@@ -53,6 +55,27 @@ PASS|PARTIAL|FAIL <unit> cases=… passed=… failed=… deferred=… max_rel_er
|
||||
```
|
||||
|
||||
then the first few failing cases and `reports/difftest/<unit>.json`.
|
||||
|
||||
**Native reference restrictions ([#448](https://github.com/tangency-labs/solidcore/issues/448)).**
|
||||
`geom.hash_table`, `heal.heal_data_p1`, `heal.heal_data_p2`, `heal.heal_data_p3`
|
||||
and `surface.twist_volume` require an x86-64 C++ reference. Their out-of-range
|
||||
floating-point-to-integer conversions are undefined upstream; ARM64 saturates
|
||||
where the pinned x86-64 reference wraps. Rust deliberately reproduces the latter
|
||||
on every target. On other architectures these units print `SKIP ...
|
||||
skipped(platform)` (or `FUZZ-SKIP`), before building fixtures or calling C++.
|
||||
The twist unit is included because its period reduction has the same dependency,
|
||||
even though the current committed cases do not expose it.
|
||||
|
||||
Skipped units are counted separately from passes, partials and failures. Their
|
||||
JSON reports have `status: "skipped(platform)"`, `pass: false`, a reason in
|
||||
`skipped_platform`, and no compared cases. `--status-of` prints `skipped(platform)`;
|
||||
ordinary comparison and fuzz runs exit successfully when they contain only passes,
|
||||
partials and platform skips. `--baseline` fails if any selected reference is
|
||||
unsupported, and skipped reports are invalid baselines: this cannot waive the CI
|
||||
coverage ratchet. Plan statuses and tolerances are unchanged. Rust-only
|
||||
`--digest-dir` runs remain enabled on every target. On Apple Silicon, an x86-64
|
||||
harness and difftest binary under Rosetta can compare these units; this does not
|
||||
address independent libc++ reader or libm differences.
|
||||
`with_values` / `no_values` split the passing cases into those that compared
|
||||
at least one float, integer or topology entry and those that compared nothing
|
||||
(both sides returned the same error, or empty outputs). `vacuous` counts the
|
||||
@@ -295,6 +318,17 @@ The separate value-share floor remains 75% of a unit's passing cases and at
|
||||
least one value-bearing case. Both-error and vacuous passes stay in the
|
||||
denominator. Known mismatches are not passes and cannot waive new discrepancies.
|
||||
|
||||
CI runs the gate as parallel jobs (`.github/workflows/ci.yml`): `difftest-baseline`
|
||||
fetches and validates the exact-base artifact (below) while `difftest-sweep` runs
|
||||
`--green --jobs 0` (status and value-share gates, reports uploaded within the run) and
|
||||
`difftest-perturb` runs `--green --perturb`, mandatory on every pull_request and
|
||||
merge_group run. `difftest-compare` needs both the validated base and the finished sweep and
|
||||
runs `--compare-reports <sweep reports> --baseline <validated base>`, which applies the
|
||||
`--baseline` ratchet (unit set, seeds, case identity, outcome and value retention, waivers)
|
||||
to the reports without re-running anything. The required check `difftest` is an aggregator
|
||||
(`needs` all four, `if: always()`): it succeeds only if every part succeeded, so a failed or
|
||||
skipped part, or a failed base validation, fails it. No base fallback and no second sweep.
|
||||
|
||||
CI retrieves the exact base SHA's successful push/main CI artifact through
|
||||
`scripts/ci/difftest-baseline.sh`. PRs use `pull_request.base.sha` and main pushes
|
||||
use `before`. For a merge queue, the helper snapshots the trusted repository's
|
||||
@@ -345,7 +379,7 @@ ratchet waivers once the PR has merged.
|
||||
* **Absolute floor** only where the runner documents one via
|
||||
`abs_floors()` (key + value + reason, written into the report). Do not add a
|
||||
floor to make a case pass; if the C++ and Rust genuinely disagree, record it in
|
||||
`plan/findings.md` and leave the unit non-green.
|
||||
a new `plan/findings/<slug>.md` and leave the unit non-green.
|
||||
* Key sets and array lengths must match. A case with a harness error (bad
|
||||
input, panic) fails. A unit with zero cases fails.
|
||||
|
||||
@@ -630,8 +664,12 @@ nice -n 10 cargo run -p difftest -- --solidcraft-cases '3526-*' # ids (<f
|
||||
|
||||
* **Steps** (`"op"`): `box`, `sphere`, `cylinder` (`axis`, default +Z), `union`,
|
||||
`difference` (optional `pattern`: the tool cut once per grid position, one cut after
|
||||
the other), `intersection`, `fillet` (edges by a point on each), `offset_faces` (faces
|
||||
by a point on each), `sheet_corner` (the deferred sheet-flange recipe below). Each
|
||||
the other), `intersection`, `extrude`, `revolve`, `pipe` (a circle swept along a path),
|
||||
`sweep` (a closed `profile` in `plane` at `origin` swept along one `path` arc or line in
|
||||
the XY plane, `SmApiCreateCurveSweep`), `loft` (`SmApiCreateSkinPrimitive` through
|
||||
`sections`, each one closed `profile` with its `plane` and `origin`; `degree` 1, ruled,
|
||||
by default; capped at both ends), `fillet` (edges by a point on each), `offset_faces`
|
||||
(faces by a point on each), `sheet_corner` (the deferred sheet-flange recipe below). Each
|
||||
writes the body `id`; the last writes `result`.
|
||||
* **Expectations** (`expect`): `lumps` (bounded regions; `0` = nothing left), `volume`
|
||||
(`value`, absolute `tol`, the `formula` it came from), `cylinder_faces_max`,
|
||||
@@ -656,6 +694,9 @@ nice -n 10 cargo run -p difftest -- --solidcraft-cases '3526-*' # ids (<f
|
||||
glob): per case its source links, verdict, deferral, measured values and checks.
|
||||
* `cargo test -p difftest --lib solidcraft` checks that every case file loads, every
|
||||
case runs to a verdict, and every deferral names a plan unit.
|
||||
* **Adding cases:** add a new `fixtures/solidcraft/<issue>-<slug>.json`. Files load in
|
||||
sorted order and nothing counts them, so no Rust file changes unless the case needs
|
||||
a new step kind.
|
||||
|
||||
### Sheet flange corner overlap (SolidCraft #3170, PR #3472)
|
||||
|
||||
@@ -694,6 +735,45 @@ nice -n 10 cargo run -p difftest -- --solidcraft-cases '3170-*' --no-report
|
||||
This case builds on the whole-operation harness in solidcore PR #345; it does not
|
||||
change any unit status, kernel implementation or floating-point tolerance.
|
||||
|
||||
## `--step-corpus`: real-world STEP files
|
||||
|
||||
A correctness report on real CAD data, separate from the gate like `--ssi-corpus`:
|
||||
verdicts never fail the run and it is not part of CI. The files are STEP Tools' sample
|
||||
sets (<https://steptools.com/docs/stpfiles/>, AP203 and AP214), "All Rights Reserved"
|
||||
and copied only for informational, non-commercial use: they are never committed.
|
||||
|
||||
```sh
|
||||
scripts/fetch-steptools-corpus.sh # corpus/steptools/, SHA-256 pinned
|
||||
nice -n 10 cargo run --profile difftest -p difftest --features cpp -- --step-corpus --jobs 4
|
||||
nice -n 10 cargo run --profile difftest -p difftest --features cpp -- --step-corpus 'ap203/block*'
|
||||
```
|
||||
|
||||
* **Import.** `crates/step-import` (clean room, from the public ISO 10303-21 syntax and
|
||||
the Part 41/42 entity definitions; no other STEP implementation consulted) parses
|
||||
each file and maps its `MANIFOLD_SOLID_BREP`, `BREP_WITH_VOIDS`, `FACETED_BREP` and
|
||||
`SHELL_BASED_SURFACE_MODEL` bodies to a plain-data model in millimetres. A body that
|
||||
reaches an unsupported entity is skipped with `unsupported: <ENTITY>` (listed in the
|
||||
summary). Assembly instancing (`MAPPED_ITEM`, `CONTEXT_DEPENDENT_SHAPE_REPRESENTATION`)
|
||||
is counted, not applied: each body is built once in its own coordinates.
|
||||
* **Build (both kernels, same numbers).** The body is encoded as one `f64` array
|
||||
(`step_import::flat`); the C++ shim (`harness/cpp/shim/step_import.cpp`) reads that
|
||||
array and the Rust side builds from its decoding. Per face: the surface by
|
||||
`CreateCanonical`, the edge loops' 3D curves bounded to their edges, then
|
||||
`CreateFacesWithCurves`; then `SmApiStitchIntoSolid`, counts (regions, shells, faces,
|
||||
edges, vertices), `SmApiBrepIsManifoldSolid`, `SmApiBrepComputeArea` and
|
||||
`SmApiBrepComputeVolume` (accuracy `1e-6`), `SmApiHealBrep`, and counts, manifold
|
||||
test and volume again. Face orientation is left to the stitch.
|
||||
* **Verdicts** (per body): `MATCH` (every stage agrees: statuses and counts equal,
|
||||
values within `1e-9` relative), `HEAL-DIFF` (every build stage agrees, the heal or a
|
||||
later stage differs), `DIFF` (a build stage differs), `DEFERRED` (the Rust side
|
||||
reached unported code; the unit is shown), `CRASH` (a child crashed, timed out or
|
||||
the shim rejected the input), `SKIPPED` (no B-rep body in the file).
|
||||
* Each (body, kernel) runs in its own child process (`--step-case <file> <body>
|
||||
<cpp|rust> <out.json>`). Output: one line per body, a `STEP-CORPUS` summary line, and
|
||||
`bin/step-corpus/report.json` (per body: source counts, both sides' stages, verdict).
|
||||
* `cargo run -p step-import --example survey -- corpus/steptools` lists what the
|
||||
importer maps per file; `--example dump_body -- <file> <body> [--flat]` prints one body.
|
||||
|
||||
## Notes
|
||||
|
||||
* Cases run sequentially in one process: the C++ kernel has global state. A
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
# Known Rust-vs-C++ mismatches (see crates/difftest/src/known.rs).
|
||||
#
|
||||
# A case listed here still fails its comparison; difftest reports it as
|
||||
# `known-mismatch` (summary line `known_mismatch=N`, report `"known_mismatch"`) and
|
||||
# the unit is at best `partial`, never green. A listed case that passes, defers or no
|
||||
# longer exists fails the unit: remove its entry when the port is fixed. Every entry
|
||||
# needs a plan/findings.md entry. Never list a case just to get CI green.
|
||||
#
|
||||
# [[mismatch]]
|
||||
# unit = "curve.curve_p1"
|
||||
# case = "gen0042"
|
||||
# why = "C++ succeeds with 0 points, Rust returns status_3 (plan/findings.md: ...)"
|
||||
|
||||
# PR #691: explicit known differences for this issue.
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "check_loop_mirror_Mouse_0_0_0"
|
||||
why = "WR-256 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[9]: C++ 6.95115100281493e-4, Rust 2.1819300674131145e-3; operation status unchanged. See plan/findings.md: shared-vertex gap tolerance."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "check_loop_mirror_Mouse_0_0_1"
|
||||
why = "WR-256 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[9]: C++ 6.95115100281493e-4, Rust 2.1819300674131145e-3; operation status unchanged. See plan/findings.md: shared-vertex gap tolerance."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "check_loop_mirror_Mouse_0_1_0"
|
||||
why = "WR-256 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[9]: C++ 6.912139700099413e-4, Rust 2.1819300674131145e-3; operation status unchanged. See plan/findings.md: shared-vertex gap tolerance."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "check_loop_mirror_Mouse_0_1_1"
|
||||
why = "WR-256 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[9]: C++ 6.912139700099413e-4, Rust 2.1819300674131145e-3; operation status unchanged. See plan/findings.md: shared-vertex gap tolerance."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "check_loop_mirror_Mouse_0_3_0"
|
||||
why = "WR-256 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[9]: C++ 6.95115100281493e-4, Rust 2.1819300674131145e-3; operation status unchanged. See plan/findings.md: shared-vertex gap tolerance."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "check_loop_mirror_Mouse_0_3_1"
|
||||
why = "WR-256 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[9]: C++ 6.95115100281493e-4, Rust 2.1819300674131145e-3; operation status unchanged. See plan/findings.md: shared-vertex gap tolerance."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "vertex_gap_covered_eu0"
|
||||
why = "WR-256 / #476: C++ reports closure 3 for gap 1.3000000000040757e-5 against edge sum 1.16e-5 despite shared vertex zone 2.6e-5; Rust reports closure 0. See plan/findings.md: shared-vertex gap tolerance."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "vertex_gap_covered_eu1"
|
||||
why = "WR-256 / #476: C++ reports closure 3 for gap 1.3000000000040757e-5 against edge sum 1.16e-5 despite shared vertex zone 2.6e-5; Rust reports closure 0. See plan/findings.md: shared-vertex gap tolerance."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "vertex_gap_covered_eu3"
|
||||
why = "WR-256 / #476: C++ reports closure 3 for gap 1.3000000000040757e-5 against edge sum 1.16e-5 despite shared vertex zone 2.6e-5; Rust reports closure 0. See plan/findings.md: shared-vertex gap tolerance."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "vertex_gap_too_wide_eu0"
|
||||
why = "WR-256 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[11]: C++ 1.16e-5, Rust 2.6e-5; operation status unchanged. See plan/findings.md: shared-vertex gap tolerance."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "vertex_gap_too_wide_eu1"
|
||||
why = "WR-256 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[11]: C++ 1.16e-5, Rust 2.6e-5; operation status unchanged. See plan/findings.md: shared-vertex gap tolerance."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "vertex_gap_too_wide_eu3"
|
||||
why = "WR-256 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[11]: C++ 1.16e-5, Rust 2.6e-5; operation status unchanged. See plan/findings.md: shared-vertex gap tolerance."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p3"
|
||||
case = "nurbs_off_Mouse_0.1"
|
||||
why = "TurnToNURBS tracker: upstream curves/surfaces notify their own SM_NO_PRE_EDIT/SM_NO_POST_EDIT edits, the port's geometry sends none (plan/findings.md: topology.face_p1 SetSurface SM_NO_CHANGE_GEOMETRY wired (PR #637), 'Still deferred: geometry edit notifications')"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p3"
|
||||
case = "nurbs_off_Mouse_-0.05"
|
||||
why = "TurnToNURBS tracker: upstream curves/surfaces notify their own SM_NO_PRE_EDIT/SM_NO_POST_EDIT edits, the port's geometry sends none (plan/findings.md: topology.face_p1 SetSurface SM_NO_CHANGE_GEOMETRY wired (PR #637), 'Still deferred: geometry edit notifications')"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p3"
|
||||
case = "nurbs_off_toruswvy_0.1"
|
||||
why = "TurnToNURBS tracker: upstream curves/surfaces notify their own SM_NO_PRE_EDIT/SM_NO_POST_EDIT edits, the port's geometry sends none (plan/findings.md: topology.face_p1 SetSurface SM_NO_CHANGE_GEOMETRY wired (PR #637), 'Still deferred: geometry edit notifications')"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p3"
|
||||
case = "gen0138"
|
||||
why = "TurnToNURBS tracker: upstream curves/surfaces notify their own SM_NO_PRE_EDIT/SM_NO_POST_EDIT edits, the port's geometry sends none (plan/findings.md: topology.face_p1 SetSurface SM_NO_CHANGE_GEOMETRY wired (PR #637), 'Still deferred: geometry edit notifications')"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p3"
|
||||
case = "gen0164"
|
||||
why = "TurnToNURBS tracker: upstream curves/surfaces notify their own SM_NO_PRE_EDIT/SM_NO_POST_EDIT edits, the port's geometry sends none (plan/findings.md: topology.face_p1 SetSurface SM_NO_CHANGE_GEOMETRY wired (PR #637), 'Still deferred: geometry edit notifications')"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p3"
|
||||
case = "gen0241"
|
||||
why = "TurnToNURBS tracker: upstream curves/surfaces notify their own SM_NO_PRE_EDIT/SM_NO_POST_EDIT edits, the port's geometry sends none (plan/findings.md: topology.face_p1 SetSurface SM_NO_CHANGE_GEOMETRY wired (PR #637), 'Still deferred: geometry edit notifications')"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "query.face_p4"
|
||||
case = "gen0157"
|
||||
why = "the importer's approximation of the analytic edge curves moves the loop bisector's UV line (rel 1.6e-8) across a loop at scale 1e-3 (plan/findings.md: query.face_p4: two scale-1e-3 cylinder cases move with the importer's curve approximation) [ints.classify_loop_ios[4] cpp 1 rust 0]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "query.face_p4"
|
||||
case = "gen0300"
|
||||
why = "the importer's approximation of the analytic edge curves moves the loop bisector's UV line (rel 1.6e-8) across a loop at scale 1e-3 (plan/findings.md: query.face_p4: two scale-1e-3 cylinder cases move with the importer's curve approximation) [ints.classify_loop_ios[2] cpp 2 rust 1]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "healer10_always_Defeature_inset_cube"
|
||||
why = "still differs with ClassifyLoops' loop bisector keeping its B-rep edits (the healer copies its snapshot's UV trims, tolerance growth and gap caches back); cause not traced (plan/findings.md: ClassifyLoops' loop bisector keeps its B-rep edits: 37 of 49 listed cases match, 12 topology.brep_p10 cases still differ) [ints.ints[1336] cpp 1 rust 0]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "read_Boolean_extrudeHoles1"
|
||||
why = "still differs with ClassifyLoops' loop bisector keeping its B-rep edits (the healer copies its snapshot's UV trims, tolerance growth and gap caches back); cause not traced (plan/findings.md: ClassifyLoops' loop bisector keeps its B-rep edits: 37 of 49 listed cases match, 12 topology.brep_p10 cases still differ) [values.dbls.len cpp 909 rust 921]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "read_Solids_InsetTaperedExtrude"
|
||||
why = "still differs with ClassifyLoops' loop bisector keeping its B-rep edits (the healer copies its snapshot's UV trims, tolerance growth and gap caches back); cause not traced (plan/findings.md: ClassifyLoops' loop bisector keeps its B-rep edits: 37 of 49 listed cases match, 12 topology.brep_p10 cases still differ) [ints.dbl_bits[805] cpp 4556438595713448769 rust 4556438595713597268]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "read_Solids_fillet_ts"
|
||||
why = "still differs with ClassifyLoops' loop bisector keeping its B-rep edits (the healer copies its snapshot's UV trims, tolerance growth and gap caches back); cause not traced (plan/findings.md: ClassifyLoops' loop bisector keeps its B-rep edits: 37 of 49 listed cases match, 12 topology.brep_p10 cases still differ) [values.dbls.len cpp 1659 rust 1683]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "gen0115"
|
||||
why = "still differs with ClassifyLoops' loop bisector keeping its B-rep edits (the healer copies its snapshot's UV trims, tolerance growth and gap caches back); cause not traced (plan/findings.md: ClassifyLoops' loop bisector keeps its B-rep edits: 37 of 49 listed cases match, 12 topology.brep_p10 cases still differ) [values.dbls[648] cpp 2.917160020417993e-3 rust 2.9171595358599514e-3]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "gen0117"
|
||||
why = "still differs with ClassifyLoops' loop bisector keeping its B-rep edits (the healer copies its snapshot's UV trims, tolerance growth and gap caches back); cause not traced (plan/findings.md: ClassifyLoops' loop bisector keeps its B-rep edits: 37 of 49 listed cases match, 12 topology.brep_p10 cases still differ) [ints.ints[166] cpp 1 rust 0]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "read_Fillet_bottle"
|
||||
why = "still differs with ClassifyLoops' loop bisector keeping its B-rep edits (the healer copies its snapshot's UV trims, tolerance growth and gap caches back); cause not traced (plan/findings.md: ClassifyLoops' loop bisector keeps its B-rep edits: 37 of 49 listed cases match, 12 topology.brep_p10 cases still differ) [ints.dbl_bits[2437] cpp 4533227276183232325 rust 4533227276183193973]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "read_Fillet_cyl_cyl_shell"
|
||||
why = "still differs with ClassifyLoops' loop bisector keeping its B-rep edits (the healer copies its snapshot's UV trims, tolerance growth and gap caches back); cause not traced (plan/findings.md: ClassifyLoops' loop bisector keeps its B-rep edits: 37 of 49 listed cases match, 12 topology.brep_p10 cases still differ) [ints.dbl_bits[351] cpp 4541087340294538906 rust 4541087340294541262]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "read_Fillet_fillet0227"
|
||||
why = "still differs with ClassifyLoops' loop bisector keeping its B-rep edits (the healer copies its snapshot's UV trims, tolerance growth and gap caches back); cause not traced (plan/findings.md: ClassifyLoops' loop bisector keeps its B-rep edits: 37 of 49 listed cases match, 12 topology.brep_p10 cases still differ) [ints.dbl_bits[587] cpp 4584643767240576693 rust 4584643767240344909]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "read_Solids_extrusion2"
|
||||
why = "still differs with ClassifyLoops' loop bisector keeping its B-rep edits (the healer copies its snapshot's UV trims, tolerance growth and gap caches back); cause not traced (plan/findings.md: ClassifyLoops' loop bisector keeps its B-rep edits: 37 of 49 listed cases match, 12 topology.brep_p10 cases still differ) [ints.dbl_bits[3949] cpp 4566519071875143604 rust 4566519071875138963]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "gen0040"
|
||||
why = "still differs with ClassifyLoops' loop bisector keeping its B-rep edits (the healer copies its snapshot's UV trims, tolerance growth and gap caches back); cause not traced (plan/findings.md: ClassifyLoops' loop bisector keeps its B-rep edits: 37 of 49 listed cases match, 12 topology.brep_p10 cases still differ) [ints.ints[1681] cpp 1 rust 0]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "gen0165"
|
||||
why = "still differs with ClassifyLoops' loop bisector keeping its B-rep edits (the healer copies its snapshot's UV trims, tolerance growth and gap caches back); cause not traced (plan/findings.md: ClassifyLoops' loop bisector keeps its B-rep edits: 37 of 49 listed cases match, 12 topology.brep_p10 cases still differ) [ints.ints[170] cpp 1 rust 0]"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "project_t0_Defeature_brep1"
|
||||
why = "ProjectAndTrim removes every face; C++ keeps the former solid region as the infinite region, the port keeps the original one (its SmTopology fields differ: cpp 99999999, rust 0) (plan/findings.md: 'topology.brep_p10 project_t0_Defeature_brep1: the region left after ProjectAndTrim')"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "boolean.merge_p3"
|
||||
case = "upstream_merge_breps_operation_3"
|
||||
why = "same result topology and vertex positions, different vertex list order: C++ lists one part's exclusive vertices last (plan/findings.md: 'boolean.merge_p3: vertex list order after merges that imprint edges into faces'; issue #832)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "boolean.merge_p3"
|
||||
case = "upstream_piecewise_boxes_2_keep_other"
|
||||
why = "same result topology and vertex positions, different vertex list order: C++ lists one part's exclusive vertices last (plan/findings.md: 'boolean.merge_p3: vertex list order after merges that imprint edges into faces'; issue #832)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "boolean.merge_p3"
|
||||
case = "upstream_piecewise_boxes_3_keep_other"
|
||||
why = "same result topology and vertex positions, different vertex list order: C++ lists one part's exclusive vertices last (plan/findings.md: 'boolean.merge_p3: vertex list order after merges that imprint edges into faces'; issue #832)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "boolean.merge_p3"
|
||||
case = "upstream_merge_breps_operation_0"
|
||||
why = "same result topology and vertex positions, different vertex list order; deferred on main (SmRegion::GetBrep / SmFace::Point3DClassify), compared since PR #816 (plan/findings.md: 'boolean.merge_p3: vertex list order after merges that imprint edges into faces'; 'Public boolean e2e (PR #816): cases newly compared once the context stubs were wired'; issue #832)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "boolean.merge_p3"
|
||||
case = "upstream_merge_breps_operation_1"
|
||||
why = "same result topology and vertex positions, different vertex list order; deferred on main (SmRegion::GetBrep / SmFace::Point3DClassify), compared since PR #816 (plan/findings.md: 'boolean.merge_p3: vertex list order after merges that imprint edges into faces'; 'Public boolean e2e (PR #816): cases newly compared once the context stubs were wired'; issue #832)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "boolean.merge_p3"
|
||||
case = "upstream_merge_breps_operation_2"
|
||||
why = "same result topology and vertex positions, different vertex list order; deferred on main (SmRegion::GetBrep / SmFace::Point3DClassify), compared since PR #816 (plan/findings.md: 'boolean.merge_p3: vertex list order after merges that imprint edges into faces'; 'Public boolean e2e (PR #816): cases newly compared once the context stubs were wired'; issue #832)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "boolean.merge_p3"
|
||||
case = "upstream_piecewise_boxes_0"
|
||||
why = "same result topology and vertex positions, different vertex list order; deferred on main (SmRegion::GetBrep / SmFace::Point3DClassify), compared since PR #816 (plan/findings.md: 'boolean.merge_p3: vertex list order after merges that imprint edges into faces'; 'Public boolean e2e (PR #816): cases newly compared once the context stubs were wired'; issue #832)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "boolean.merge_p3"
|
||||
case = "upstream_piecewise_boxes_1"
|
||||
why = "same result topology and vertex positions, different vertex list order; deferred on main (SmRegion::GetBrep / SmFace::Point3DClassify), compared since PR #816 (plan/findings.md: 'boolean.merge_p3: vertex list order after merges that imprint edges into faces'; 'Public boolean e2e (PR #816): cases newly compared once the context stubs were wired'; issue #832)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "blend.fillet_executive_p1"
|
||||
case = "remove_topo_edges_mouse_two_struts"
|
||||
why = "RemoveTopoEdges with topo edges 1 and 2 of Mouse.smb in sequence: after the second deletion one edge use's UV curve on the plane differs (faces, surfaces and the remaining edge agree; each edge alone matches); in DeleteEdge / DeleteTopologicalVertex / SetSurface, not located (plan/findings.md: 'blend.fillet_executive_p1, RemoveTopoEdges and SetBevelCorner')"
|
||||
@@ -0,0 +1,4 @@
|
||||
[[mismatch]]
|
||||
unit = "offset.offset_geometry_creation"
|
||||
case = "gen0350"
|
||||
why = "WR-255: the drop in ExtendTrimEdges builds the sphere face's trimmed cache, whose CreateUVTrimCurves -> sm_FixUVTrimCurves closes a 0.61 UV corner gap on the offset edge use's 5-point UV trim; upstream (c9c5979) drops the vertex onto the trim (t = 0.01177), trims there and stretches it back (Trim + EditParameterization: knots 0.0411/0.1687/0.8571 -> 0.0296/0.1588/0.8554), the port keeps the fork fix (end point only; Context::trim_uv_curves_at_corner_drops makes it pass); parked with the other WR-255 cases pending #959 (plan/findings/offset.offset_geometry_creation--addendum-2026-10-25-kept-trim-cache-and-wr-255.md)"
|
||||
@@ -0,0 +1,39 @@
|
||||
# C++ merges across a continuation that folds; the port refuses it (SolidCraft change from
|
||||
# tangency-labs/smlib 1b6f5fa, tangency-labs/solidcore#434). The case becomes a
|
||||
# `deviation(#434)` once difftest has that outcome (#989); it is a declared, deliberate
|
||||
# divergence, not an unfixed port bug.
|
||||
[[mismatch]]
|
||||
unit = "surface.surface_p3"
|
||||
case = "gen0041"
|
||||
why = "CoverCoincidentSurface refuses a cover whose continued domain folds: this surface's Du x Dv normal turns back (dot <= 0) between neighbouring samples of the 21 x 21 grid over the domain the merge would use. C++ success=true, Rust success=false. SolidCraft change, tangency-labs/smlib 1b6f5fa, tangency-labs/solidcore#434 (plan/findings/surface.surface_p3--cover-refuses-a-folded-continuation.md)."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "surface.surface_p3"
|
||||
case = "gen1448"
|
||||
why = "CoverCoincidentSurface refuses a cover whose continued domain folds: this surface's Du x Dv normal turns back (dot <= 0) between neighbouring samples of the 21 x 21 grid over the domain the merge would use. C++ success=true, Rust success=false. SolidCraft change, tangency-labs/smlib 1b6f5fa, tangency-labs/solidcore#434 (plan/findings/surface.surface_p3--cover-refuses-a-folded-continuation.md)."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "surface.surface_p3"
|
||||
case = "gen1514"
|
||||
why = "CoverCoincidentSurface refuses a cover whose continued domain folds: this surface's Du x Dv normal turns back (dot <= 0) between neighbouring samples of the 21 x 21 grid over the domain the merge would use. C++ success=true, Rust success=false. SolidCraft change, tangency-labs/smlib 1b6f5fa, tangency-labs/solidcore#434 (plan/findings/surface.surface_p3--cover-refuses-a-folded-continuation.md)."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "surface.surface_p3"
|
||||
case = "gen1567"
|
||||
why = "CoverCoincidentSurface refuses a cover whose continued domain folds: this surface's Du x Dv normal turns back (dot <= 0) between neighbouring samples of the 21 x 21 grid over the domain the merge would use. C++ success=true, Rust success=false. SolidCraft change, tangency-labs/smlib 1b6f5fa, tangency-labs/solidcore#434 (plan/findings/surface.surface_p3--cover-refuses-a-folded-continuation.md)."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "surface.surface_p3"
|
||||
case = "gen2066"
|
||||
why = "CoverCoincidentSurface refuses a cover whose continued domain folds: this surface's Du x Dv normal turns back (dot <= 0) between neighbouring samples of the 21 x 21 grid over the domain the merge would use. C++ success=true, Rust success=false. SolidCraft change, tangency-labs/smlib 1b6f5fa, tangency-labs/solidcore#434 (plan/findings/surface.surface_p3--cover-refuses-a-folded-continuation.md)."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "surface.surface_p3"
|
||||
case = "gen2303"
|
||||
why = "CoverCoincidentSurface refuses a cover whose continued domain folds: this surface's Du x Dv normal turns back (dot <= 0) between neighbouring samples of the 21 x 21 grid over the domain the merge would use. C++ success=true, Rust success=false. SolidCraft change, tangency-labs/smlib 1b6f5fa, tangency-labs/solidcore#434 (plan/findings/surface.surface_p3--cover-refuses-a-folded-continuation.md)."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "surface.surface_p3"
|
||||
case = "gen2376"
|
||||
why = "CoverCoincidentSurface refuses a cover whose continued domain folds: this surface's Du x Dv normal turns back (dot <= 0) between neighbouring samples of the 21 x 21 grid over the domain the merge would use. C++ success=true, Rust success=false. SolidCraft change, tangency-labs/smlib 1b6f5fa, tangency-labs/solidcore#434 (plan/findings/surface.surface_p3--cover-refuses-a-folded-continuation.md)."
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p10"
|
||||
case = "read_Solids_fillet_ts"
|
||||
libc = "glibc"
|
||||
why = "SmHealData::Fix_CoinVertices dangling-pointer heap reuse (UB-255): UpdateTargetLists frees both pSurvivorProps and pDeletedProps on SM_NO_MERGE_IN_BREP, Fix_CoinVertices rewrites sProbArray to the freed pSurvivorProps, and once glibc's 7-slot tcache bin fills on the 6th merge, new SmVertexProps reuses pSurvivorProps's address so C++ merges 3 extra 3-way vertex clusters (22 vertices, 1 shell vs Rust's fresh-id 25 vertices, 2 shells; plan/findings.md: 'topology.brep_p10, topology.brep_p1, sweep.api_brep: heal/notify/ProjectAndTrim fixes and `read_Solids_fillet_ts` UAF root cause'); only glibc reuses the address: with macOS libmalloc the C++ agrees with Rust, plan/findings/topology.brep_p10--read-solids-fillet-ts-mismatch-is-glibc-only.md)"
|
||||
@@ -0,0 +1,9 @@
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p3"
|
||||
case = "nurbs_off_Mouse_0.1"
|
||||
why = "TurnToNURBS tracker: C++ sm_FixUVTrimCurves trims two UV trim curves at their vertex drop points and stretches them back (Trim + EditParameterization, two SM_NO_POST_EDITs each); the port skips that step on purpose (WR-255, #570), so its tracker lacks those 4 events (plan/findings.md: topology.brep_p3: TurnToNURBS UV trim curve edits notify)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.brep_p3"
|
||||
case = "nurbs_off_Mouse_-0.05"
|
||||
why = "TurnToNURBS tracker: C++ sm_FixUVTrimCurves trims two UV trim curves at their vertex drop points and stretches them back (Trim + EditParameterization, two SM_NO_POST_EDITs each); the port skips that step on purpose (WR-255, #570), so its tracker lacks those 4 events (plan/findings.md: topology.brep_p3: TurnToNURBS UV trim curve edits notify)"
|
||||
@@ -0,0 +1,61 @@
|
||||
# PR #691: explicit known differences for this issue.
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "check_loop_mirror_Mouse_0_0_0"
|
||||
why = "WR-381 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[9]: C++ 6.95115100281493e-4, Rust 2.1819300674131145e-3; operation status unchanged. See plan/findings/topology.trimming_tools--shared-vertex-gap-tolerance-issue-476.md."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "check_loop_mirror_Mouse_0_0_1"
|
||||
why = "WR-381 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[9]: C++ 6.95115100281493e-4, Rust 2.1819300674131145e-3; operation status unchanged. See plan/findings/topology.trimming_tools--shared-vertex-gap-tolerance-issue-476.md."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "check_loop_mirror_Mouse_0_1_0"
|
||||
why = "WR-381 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[9]: C++ 6.912139700099413e-4, Rust 2.1819300674131145e-3; operation status unchanged. See plan/findings/topology.trimming_tools--shared-vertex-gap-tolerance-issue-476.md."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "check_loop_mirror_Mouse_0_1_1"
|
||||
why = "WR-381 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[9]: C++ 6.912139700099413e-4, Rust 2.1819300674131145e-3; operation status unchanged. See plan/findings/topology.trimming_tools--shared-vertex-gap-tolerance-issue-476.md."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "check_loop_mirror_Mouse_0_3_0"
|
||||
why = "WR-381 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[9]: C++ 6.95115100281493e-4, Rust 2.1819300674131145e-3; operation status unchanged. See plan/findings/topology.trimming_tools--shared-vertex-gap-tolerance-issue-476.md."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "check_loop_mirror_Mouse_0_3_1"
|
||||
why = "WR-381 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[9]: C++ 6.95115100281493e-4, Rust 2.1819300674131145e-3; operation status unchanged. See plan/findings/topology.trimming_tools--shared-vertex-gap-tolerance-issue-476.md."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "vertex_gap_covered_eu0"
|
||||
why = "WR-381 / #476: C++ reports closure 3 for gap 1.3000000000040757e-5 against edge sum 1.16e-5 despite shared vertex zone 2.6e-5; Rust reports closure 0. See plan/findings/topology.trimming_tools--shared-vertex-gap-tolerance-issue-476.md."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "vertex_gap_covered_eu1"
|
||||
why = "WR-381 / #476: C++ reports closure 3 for gap 1.3000000000040757e-5 against edge sum 1.16e-5 despite shared vertex zone 2.6e-5; Rust reports closure 0. See plan/findings/topology.trimming_tools--shared-vertex-gap-tolerance-issue-476.md."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "vertex_gap_covered_eu3"
|
||||
why = "WR-381 / #476: C++ reports closure 3 for gap 1.3000000000040757e-5 against edge sum 1.16e-5 despite shared vertex zone 2.6e-5; Rust reports closure 0. See plan/findings/topology.trimming_tools--shared-vertex-gap-tolerance-issue-476.md."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "vertex_gap_too_wide_eu0"
|
||||
why = "WR-381 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[11]: C++ 1.16e-5, Rust 2.6e-5; operation status unchanged. See plan/findings/topology.trimming_tools--shared-vertex-gap-tolerance-issue-476.md."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "vertex_gap_too_wide_eu1"
|
||||
why = "WR-381 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[11]: C++ 1.16e-5, Rust 2.6e-5; operation status unchanged. See plan/findings/topology.trimming_tools--shared-vertex-gap-tolerance-issue-476.md."
|
||||
|
||||
[[mismatch]]
|
||||
unit = "topology.trimming_tools"
|
||||
case = "vertex_gap_too_wide_eu3"
|
||||
why = "WR-381 / #476: failed gap diagnostic uses edge-only limit in C++; shared-vertex-aware Rust limit is correct. values.out[11]: C++ 1.16e-5, Rust 2.6e-5; operation status unchanged. See plan/findings/topology.trimming_tools--shared-vertex-gap-tolerance-issue-476.md."
|
||||
@@ -465,9 +465,106 @@ pub fn ratchet(base: &Baseline, res: &UnitResult) -> Vec<Violation> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Ratchet violations of a completed report `now` against the earlier report `base`
|
||||
/// (`--compare-reports`). The same rules as [`ratchet`], applied to two parsed reports
|
||||
/// instead of a live [`UnitResult`]: the sweep ran elsewhere and only its reports remain.
|
||||
pub fn ratchet_reports(base: &Baseline, now: &Baseline) -> Vec<Violation> {
|
||||
let current: BTreeMap<&str, &BaselineCase> =
|
||||
now.cases.iter().map(|c| (c.name.as_str(), c)).collect();
|
||||
let mut out = Vec::new();
|
||||
let mut push = |case: &str, detail: String| {
|
||||
out.push(Violation {
|
||||
unit: now.unit.clone(),
|
||||
check: Check::Ratchet,
|
||||
case: Some(case.to_string()),
|
||||
detail,
|
||||
});
|
||||
};
|
||||
if base.seeds != now.seeds {
|
||||
push(
|
||||
"<seeds>",
|
||||
"baseline/current seeds differ; generated coverage cannot be ratcheted".into(),
|
||||
);
|
||||
}
|
||||
for b in &base.cases {
|
||||
if b.outcome == "rejected" {
|
||||
continue;
|
||||
}
|
||||
let Some(c) = current.get(b.name.as_str()) else {
|
||||
push(
|
||||
&b.name,
|
||||
format!("case `{}` ({} in the baseline) is gone", b.name, b.outcome),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
if b.outcome == "known_mismatch" && !matches!(c.outcome.as_str(), "known_mismatch" | "pass")
|
||||
{
|
||||
push(
|
||||
&b.name,
|
||||
format!("case `{}` was a known mismatch, now {}", b.name, c.outcome),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if b.outcome == "pass" && c.outcome != "pass" {
|
||||
if c.outcome == "fail" {
|
||||
continue; // already fails the unit in the sweep
|
||||
}
|
||||
push(
|
||||
&b.name,
|
||||
format!(
|
||||
"case `{}` passed in the baseline, now {}",
|
||||
b.name, c.outcome
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if b.compared_values && !c.compared_values {
|
||||
push(
|
||||
&b.name,
|
||||
format!(
|
||||
"case `{}` compared values in the baseline, now compares nothing",
|
||||
b.name
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Units that have a report (`<unit>.json`) in `dir`, sorted. Every report is parsed.
|
||||
pub fn report_units(dir: &Path) -> Result<Vec<String>, HarnessError> {
|
||||
let entries =
|
||||
fs::read_dir(dir).map_err(|e| HarnessError::new(format!("{}: {e}", dir.display())))?;
|
||||
let mut out = Vec::new();
|
||||
for e in entries {
|
||||
let path = e
|
||||
.map_err(|e| HarnessError::new(format!("{}: {e}", dir.display())))?
|
||||
.path();
|
||||
if path.extension().and_then(|x| x.to_str()) != Some("json") {
|
||||
continue;
|
||||
}
|
||||
let unit = path
|
||||
.file_stem()
|
||||
.and_then(|x| x.to_str())
|
||||
.ok_or_else(|| HarnessError::new("invalid report filename"))?
|
||||
.to_string();
|
||||
if load_baseline(dir, &unit)?.is_none() {
|
||||
return Err(HarnessError::new(format!("{unit}: report disappeared")));
|
||||
}
|
||||
out.push(unit);
|
||||
}
|
||||
out.sort();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Value-share violation of `res`, if any. Only meaningful for a unit that does not
|
||||
/// already fail.
|
||||
/// already fail. A unit whose every case was `skipped(platform)` (e.g. a libstdc++-only
|
||||
/// reference on libc++) compared nothing and has no share to check, like a unit-level
|
||||
/// `skipped(platform)`.
|
||||
pub fn value_share(res: &UnitResult) -> Option<Violation> {
|
||||
if !res.cases.is_empty() && res.skipped_platform() == res.cases.len() {
|
||||
return None;
|
||||
}
|
||||
let passed = res.passed();
|
||||
let with_values = res.passed_with_values();
|
||||
// Exact integer arithmetic for `with_values / passed < MIN_VALUE_SHARE` (= 3/4).
|
||||
@@ -568,6 +665,59 @@ mod tests {
|
||||
use crate::output::Deferred;
|
||||
use crate::runner::CaseResult;
|
||||
|
||||
fn bcase(name: &str, outcome: &str, compared_values: bool) -> BaselineCase {
|
||||
BaselineCase {
|
||||
name: name.into(),
|
||||
outcome: outcome.into(),
|
||||
generated: false,
|
||||
compared_values,
|
||||
}
|
||||
}
|
||||
|
||||
fn report(seeds: u64, cases: Vec<BaselineCase>) -> Baseline {
|
||||
Baseline {
|
||||
unit: "a.b".into(),
|
||||
status: Some("green".into()),
|
||||
seeds: Some(seeds),
|
||||
cases,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ratchet_reports_flags_regressions() {
|
||||
let base = report(
|
||||
16,
|
||||
vec![
|
||||
bcase("p", "pass", true),
|
||||
bcase("q", "pass", true),
|
||||
bcase("r", "known_mismatch", false),
|
||||
bcase("s", "deferred", false),
|
||||
bcase("x", "rejected", false),
|
||||
bcase("f", "pass", false),
|
||||
],
|
||||
);
|
||||
assert!(ratchet_reports(&base, &base).is_empty());
|
||||
let now = report(
|
||||
16,
|
||||
vec![
|
||||
bcase("p", "deferred", false),
|
||||
bcase("q", "pass", false),
|
||||
bcase("r", "deferred", false),
|
||||
bcase("f", "fail", false),
|
||||
],
|
||||
);
|
||||
let names: Vec<String> = ratchet_reports(&base, &now)
|
||||
.into_iter()
|
||||
.filter_map(|v| v.case)
|
||||
.collect();
|
||||
// `f` failing is left to the sweep; `x` (rejected) may vanish; `s` may not.
|
||||
assert_eq!(names, ["p", "q", "r", "s"]);
|
||||
assert_eq!(
|
||||
ratchet_reports(&base, &report(8, base.cases.clone())).len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
fn case(name: &str, outcome: &str, values: usize) -> CaseResult {
|
||||
let mut c = CaseResult {
|
||||
name: name.into(),
|
||||
@@ -595,6 +745,7 @@ mod tests {
|
||||
});
|
||||
}
|
||||
"rejected" => c.rejected = Some("bad".into()),
|
||||
"skipped(platform)" => c.skipped_platform = Some("libc++".into()),
|
||||
"fail" => c.diff.mismatch_count = 1,
|
||||
"known_mismatch" => {
|
||||
c.diff.mismatch_count = 1;
|
||||
@@ -610,6 +761,7 @@ mod tests {
|
||||
unit: "a.b".into(),
|
||||
cases,
|
||||
harness_error: None,
|
||||
skipped_platform: None,
|
||||
abs_floors: Vec::new(),
|
||||
seeds: 16,
|
||||
timing: crate::runner::UnitTiming::default(),
|
||||
@@ -800,6 +952,14 @@ mod tests {
|
||||
// Deferred cases do not count either way, but some pass must compare a value.
|
||||
assert!(value_share(&unit(vec![case("a", "deferred", 0)])).is_some());
|
||||
assert!(value_share(&unit(vec![case("a", "pass", 1), case("b", "deferred", 0)])).is_none());
|
||||
// A unit whose every case is platform-skipped compared nothing: no share to
|
||||
// check. One case that ran brings the floor back.
|
||||
let skipped = |n: usize| (0..n).map(|i| case(&format!("s{i}"), "skipped(platform)", 0));
|
||||
assert!(value_share(&unit(skipped(3).collect())).is_none());
|
||||
let mut v: Vec<_> = skipped(3).collect();
|
||||
v.push(case("e", "pass", 0));
|
||||
assert!(value_share(&unit(v)).is_some());
|
||||
assert!(value_share(&unit(Vec::new())).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
#[cfg(feature = "cpp")]
|
||||
pub(crate) mod regions;
|
||||
#[cfg(feature = "cpp")]
|
||||
pub(crate) mod text_curve;
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
//! Bounded decoding of the `.smc` curve fixture files the unit runners read.
|
||||
//!
|
||||
//! Upstream text form (`N_CrvReadFromFilePtr`): `n`, `p`, rational flag, dimension,
|
||||
//! `n + 1` control points (`x y [z]`, or `wx wy [wz] w` when rational) and `n + p + 2`
|
||||
//! knots.
|
||||
//!
|
||||
//! The runners validated the whole file eagerly (a malformed tail precedes header
|
||||
//! errors) but then trusted the header: `Vec::with_capacity(n + 1)`, `n + p + 2` and the
|
||||
//! point loop all followed the declared count, so a short file with a huge `n`
|
||||
//! reserved (or overflowed) gigabytes before the truncation was ever seen.
|
||||
//!
|
||||
//! Deviations: a count must be finite, non-negative, integral and below
|
||||
//! `2^usize::BITS` (the exclusive boundary of the `f64`-to-`usize` cast), and the
|
||||
//! declared payload is checked (`checked_add`/`checked_mul`) and compared with the
|
||||
//! numbers actually present before any output grows. Parsing stays eager: every token
|
||||
//! must be a number before the headers are inspected, and numeric tails are ignored.
|
||||
|
||||
use std::vec::IntoIter;
|
||||
|
||||
use solidcore::nurbs::math::{NOW, NOZ};
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
/// A decoded `.smc` curve: control points (weight `NOW` when not rational), degree and
|
||||
/// knots.
|
||||
pub(crate) struct SmcCurve {
|
||||
/// The `n + 1` control points, `[x, y, z, w]`.
|
||||
pub(crate) cpts: Vec<[f64; 4]>,
|
||||
/// The curve degree `p`.
|
||||
pub(crate) p: usize,
|
||||
/// The `n + p + 2` knots.
|
||||
pub(crate) u: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Reads and decodes an `.smc` curve of any dimension (`z` is `NOZ` unless the
|
||||
/// dimension is 3).
|
||||
pub(crate) fn read(path: &str) -> Result<SmcCurve, HarnessError> {
|
||||
decode(&text(path)?, path)
|
||||
}
|
||||
|
||||
/// Reads and decodes a 3D `.smc` curve, rejecting any other dimension as
|
||||
/// `not a 3D curve`.
|
||||
pub(crate) fn read_3d(path: &str) -> Result<SmcCurve, HarnessError> {
|
||||
decode_3d(&text(path)?, path)
|
||||
}
|
||||
|
||||
/// Reads the file's text (`{path}: {io error}` on failure, as the runners did).
|
||||
fn text(path: &str) -> Result<String, HarnessError> {
|
||||
std::fs::read_to_string(path).map_err(|e| HarnessError::new(format!("{path}: {e}")))
|
||||
}
|
||||
|
||||
/// Decodes an `.smc` curve of any dimension (`z` is `NOZ` unless the dimension is 3).
|
||||
pub(crate) fn decode(text: &str, path: &str) -> Result<SmcCurve, HarnessError> {
|
||||
decode_impl(text, path, false)
|
||||
}
|
||||
|
||||
/// Decodes a 3D `.smc` curve, rejecting any other dimension as `not a 3D curve`.
|
||||
pub(crate) fn decode_3d(text: &str, path: &str) -> Result<SmcCurve, HarnessError> {
|
||||
decode_impl(text, path, true)
|
||||
}
|
||||
|
||||
/// One header or payload number; a file that ends early is `truncated`.
|
||||
fn number(numbers: &mut IntoIter<f64>, path: &str) -> Result<f64, HarnessError> {
|
||||
numbers
|
||||
.next()
|
||||
.ok_or_else(|| HarnessError::new(format!("{path}: truncated")))
|
||||
}
|
||||
|
||||
fn decode_impl(text: &str, path: &str, only_3d: bool) -> Result<SmcCurve, HarnessError> {
|
||||
let error = |message: String| HarnessError::new(format!("{path}: {message}"));
|
||||
// Eager lexical validation of the whole file, including ignored tails, before any
|
||||
// header is inspected (the readers' long-standing malformed-tail precedence).
|
||||
let mut numbers = text
|
||||
.split_whitespace()
|
||||
.map(|token| token.parse::<f64>().map_err(|e| error(e.to_string())))
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter();
|
||||
let count = |value: f64| {
|
||||
// `usize::MAX as f64` rounds up, so the `2^BITS` boundary is exclusive.
|
||||
if !value.is_finite()
|
||||
|| value < 0.0
|
||||
|| value.fract() != 0.0
|
||||
|| value >= 2.0_f64.powi(usize::BITS as i32)
|
||||
{
|
||||
return Err(error(format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = count(number(&mut numbers, path)?)?;
|
||||
let p = count(number(&mut numbers, path)?)?;
|
||||
let rational = number(&mut numbers, path)? != 0.0;
|
||||
let dim = count(number(&mut numbers, path)?)?;
|
||||
if only_3d && dim != 3 {
|
||||
return Err(error("not a 3D curve".to_owned()));
|
||||
}
|
||||
let overflow = || error("count overflow".to_owned());
|
||||
let points = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let knots = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let stride = 2usize
|
||||
.checked_add(usize::from(dim == 3))
|
||||
.and_then(|v| v.checked_add(usize::from(rational)))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = points
|
||||
.checked_mul(stride)
|
||||
.and_then(|v| v.checked_add(knots))
|
||||
.ok_or_else(overflow)?;
|
||||
// Preflight the numbers the file actually holds before growing any output.
|
||||
if numbers.len() < payload {
|
||||
return Err(error("truncated".to_owned()));
|
||||
}
|
||||
let mut cpts = Vec::new();
|
||||
for _ in 0..points {
|
||||
let (x, y) = (number(&mut numbers, path)?, number(&mut numbers, path)?);
|
||||
let z = if dim == 3 {
|
||||
number(&mut numbers, path)?
|
||||
} else {
|
||||
NOZ
|
||||
};
|
||||
let w = if rational {
|
||||
number(&mut numbers, path)?
|
||||
} else {
|
||||
NOW
|
||||
};
|
||||
cpts.push([x, y, z, w]);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..knots {
|
||||
u.push(number(&mut numbers, path)?);
|
||||
}
|
||||
Ok(SmcCurve { cpts, p, u })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const PATH: &str = "fixtures/example.smc";
|
||||
|
||||
fn failure(text: &str) -> String {
|
||||
let result = decode(text, PATH);
|
||||
assert!(result.is_err(), "unexpected success: {text}");
|
||||
result.err().map_or_else(String::new, |e| e.to_string())
|
||||
}
|
||||
|
||||
fn assert_error(text: &str, message: &str) {
|
||||
assert_eq!(failure(text), format!("{PATH}: {message}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_rational_flags_and_undivided_order() {
|
||||
for dim in [0, 1, 2, 3, 4, 100] {
|
||||
for flag in ["0", "-0", "1", "-1", "0.5", "-0.5", "NaN", "inf", "-inf"] {
|
||||
let rational = flag.parse::<f64>().unwrap() != 0.0;
|
||||
let mut text = format!("1 1 {flag} {dim} -0 2 ");
|
||||
if dim == 3 {
|
||||
text.push_str("3 ");
|
||||
}
|
||||
if rational {
|
||||
text.push_str("-4 ");
|
||||
}
|
||||
text.push_str("5 6 ");
|
||||
if dim == 3 {
|
||||
text.push_str("7 ");
|
||||
}
|
||||
if rational {
|
||||
text.push_str("8 ");
|
||||
}
|
||||
text.push_str("10 11 12 13");
|
||||
let c = decode(&text, PATH).unwrap();
|
||||
assert_eq!(c.p, 1);
|
||||
assert_eq!(c.u, [10.0, 11.0, 12.0, 13.0]);
|
||||
assert_eq!(
|
||||
c.cpts,
|
||||
[
|
||||
[
|
||||
-0.0,
|
||||
2.0,
|
||||
if dim == 3 { 3.0 } else { NOZ },
|
||||
if rational { -4.0 } else { NOW }
|
||||
],
|
||||
[
|
||||
5.0,
|
||||
6.0,
|
||||
if dim == 3 { 7.0 } else { NOZ },
|
||||
if rational { 8.0 } else { NOW }
|
||||
],
|
||||
]
|
||||
);
|
||||
assert_eq!(c.cpts[0][0].to_bits(), (-0.0_f64).to_bits());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_dimensional_variant_rejects_other_dimensions() {
|
||||
let c = decode_3d("0 0 0 3 1 2 3 10 11", PATH).unwrap();
|
||||
assert_eq!(c.cpts, [[1.0, 2.0, 3.0, NOW]]);
|
||||
for dim in ["0", "1", "2", "4", "100"] {
|
||||
let text = format!("0 0 0 {dim} 1 2 10 11");
|
||||
assert_eq!(
|
||||
decode_3d(&text, PATH).err().map(|e| e.to_string()),
|
||||
Some(format!("{PATH}: not a 3D curve"))
|
||||
);
|
||||
assert!(decode(&text, PATH).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_nonfinite_and_exclusive_counts() {
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32).to_string();
|
||||
for value in [
|
||||
"-1", "0.5", "NaN", "inf", "-inf", "1e300", "1e999", &exclusive,
|
||||
] {
|
||||
for index in [0, 1, 3] {
|
||||
let mut header = ["0", "0", "0", "2"];
|
||||
header[index] = value;
|
||||
let parsed = value.parse::<f64>().unwrap();
|
||||
let text = header.join(" ");
|
||||
assert_eq!(
|
||||
decode(&text, PATH).err().map(|e| e.to_string()),
|
||||
Some(format!("{PATH}: bad count {parsed}"))
|
||||
);
|
||||
assert_eq!(
|
||||
decode_3d(&text, PATH).err().map(|e| e.to_string()),
|
||||
Some(format!("{PATH}: bad count {parsed}"))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huge_headers_with_tiny_bodies_do_not_allocate() {
|
||||
let huge = 2.0_f64.powi((usize::BITS - 4) as i32).to_string();
|
||||
for index in [0, 1] {
|
||||
let mut header = ["0", "0", "0", "2"];
|
||||
header[index] = &huge;
|
||||
assert_error(&header.join(" "), "truncated");
|
||||
assert_error(&format!("{} 1 2 3", header.join(" ")), "truncated");
|
||||
}
|
||||
// An absurd dimension does not grow a point; only `== 3` adds a coordinate.
|
||||
let c = decode(&format!("0 0 0 {huge} 1 2 10 11"), PATH).unwrap();
|
||||
assert_eq!(c.cpts, [[1.0, 2.0, NOZ, NOW]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checked_knot_coordinate_and_payload_arithmetic() {
|
||||
let power = |bits| 2.0_f64.powi(bits).to_string();
|
||||
let half = power(usize::BITS as i32 - 1);
|
||||
let quarter = power(usize::BITS as i32 - 2);
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32);
|
||||
let largest = f64::from_bits(exclusive.to_bits() - 1).floor();
|
||||
let remainder = (exclusive - largest - 1.0).to_string();
|
||||
for header in [
|
||||
format!("{half} {half} 0 2"), // n + p
|
||||
format!("{largest} {remainder} 0 2"), // the final +2
|
||||
format!("{half} 0 0 2"), // points * 2
|
||||
format!("{quarter} 0 1 3"), // points * 4
|
||||
format!("{quarter} 0 0 3"), // coordinates and knots
|
||||
] {
|
||||
assert_error(&header, "count overflow");
|
||||
}
|
||||
// usize::MAX is exactly representable on 32-bit targets, unlike on 64-bit.
|
||||
if usize::BITS <= f64::MANTISSA_DIGITS {
|
||||
assert_error(&format!("{} 0 0 2", usize::MAX), "count overflow");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_header_and_payload_truncations() {
|
||||
for text in [
|
||||
"1 1 0 2 -0 2 5 6 10 11 12 13",
|
||||
"1 1 0 3 -0 2 3 5 6 7 10 11 12 13",
|
||||
"1 1 1 2 -0 2 -4 5 6 8 10 11 12 13",
|
||||
"1 1 1 3 -0 2 3 -4 5 6 7 8 10 11 12 13",
|
||||
] {
|
||||
let tokens: Vec<_> = text.split_whitespace().collect();
|
||||
for end in 0..tokens.len() {
|
||||
assert_error(&tokens[..end].join(" "), "truncated");
|
||||
}
|
||||
assert!(decode(text, PATH).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eager_bad_tail_precedes_bad_headers_overflow_and_truncation() {
|
||||
let lexical = "bad".parse::<f64>().unwrap_err().to_string();
|
||||
for prefix in ["", "-1", "0", "0 0 0 2", "0 0 0 2 1 2 10 11"] {
|
||||
assert_error(&format!("{prefix} bad"), &lexical);
|
||||
}
|
||||
let half = 2.0_f64.powi(usize::BITS as i32 - 1);
|
||||
assert_error(&format!("{half} {half} 0 2 bad"), &lexical);
|
||||
assert_error("-1", "bad count -1");
|
||||
assert_error("0 -2", "bad count -2");
|
||||
assert_error("0 0 0 -3", "bad count -3");
|
||||
assert_error("-1 -2 0 -3", "bad count -1");
|
||||
assert_error("0 0 NaN -3", "bad count -3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_tails_and_nonfinite_payload_are_preserved() {
|
||||
let c = decode("0 0 0 2 1 2 10 11 999 NaN inf -inf", PATH).unwrap();
|
||||
assert_eq!(c.cpts, [[1.0, 2.0, NOZ, NOW]]);
|
||||
assert_eq!(c.u, [10.0, 11.0]);
|
||||
let c = decode("0 0 1 3 NaN inf -inf -0 NaN inf", PATH).unwrap();
|
||||
assert!(c.cpts[0][0].is_nan());
|
||||
assert_eq!(c.cpts[0][1], f64::INFINITY);
|
||||
assert_eq!(c.cpts[0][2], f64::NEG_INFINITY);
|
||||
assert_eq!(c.cpts[0][3].to_bits(), (-0.0_f64).to_bits());
|
||||
assert!(c.u[0].is_nan());
|
||||
assert_eq!(c.u[1], f64::INFINITY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_length_curve_and_large_degree_shape() {
|
||||
let c = decode("0 0 0 2 1 2 10 11", PATH).unwrap();
|
||||
assert_eq!(c.p, 0);
|
||||
assert_eq!(c.u, [10.0, 11.0]);
|
||||
let knots = (10..19).map(|k| k.to_string()).collect::<Vec<_>>();
|
||||
let c = decode(&format!("0 7 0 2 1 2 {}", knots.join(" ")), PATH).unwrap();
|
||||
assert_eq!(c.p, 7);
|
||||
assert_eq!(c.u.len(), 9);
|
||||
}
|
||||
}
|
||||
@@ -635,6 +635,8 @@ pub struct FuzzResult {
|
||||
pub notes: Vec<Failure>,
|
||||
/// Failure to build or run the cases.
|
||||
pub harness_error: Option<String>,
|
||||
/// Reason the native reference is unsupported; no mutations or workers ran.
|
||||
pub skipped_platform: Option<String>,
|
||||
}
|
||||
|
||||
impl FuzzResult {
|
||||
@@ -643,15 +645,21 @@ impl FuzzResult {
|
||||
self.verdicts.iter().filter(|&&x| x == v).count()
|
||||
}
|
||||
|
||||
/// No failure and no harness error.
|
||||
/// At least one verdict, no failure, no harness error and no platform skip.
|
||||
pub fn pass(&self) -> bool {
|
||||
self.harness_error.is_none() && self.failures.is_empty() && !self.verdicts.is_empty()
|
||||
self.skipped_platform.is_none()
|
||||
&& self.harness_error.is_none()
|
||||
&& self.failures.is_empty()
|
||||
&& !self.verdicts.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// One-line summary, e.g. `FUZZ-PASS harness.selftest cases=300 rejected=81 malformed=40
|
||||
/// ran=179 differs=2 crashes=0 panics=0 timeouts=0` (`ran` includes `differs`).
|
||||
pub fn summary_line(r: &FuzzResult) -> String {
|
||||
if let Some(reason) = &r.skipped_platform {
|
||||
return format!("FUZZ-SKIP {} skipped(platform): {reason}", r.unit);
|
||||
}
|
||||
let mut s = format!(
|
||||
"{} {} cases={} rejected={} malformed={} ran={} differs={} crashes={} panics={} timeouts={}",
|
||||
if r.pass() { "FUZZ-PASS" } else { "FUZZ-FAIL" },
|
||||
@@ -732,8 +740,15 @@ fn clear_old(dir: &Path) {
|
||||
pub fn run_fuzz(unit: &dyn UnitDiff, ctx: &CaseContext, opts: &FuzzOptions) -> FuzzResult {
|
||||
let mut res = FuzzResult {
|
||||
unit: unit.id().to_string(),
|
||||
skipped_platform: unit
|
||||
.reference_platform()
|
||||
.and_then(|p| p.skip_reason(std::env::consts::ARCH))
|
||||
.map(str::to_string),
|
||||
..FuzzResult::default()
|
||||
};
|
||||
if res.skipped_platform.is_some() {
|
||||
return res;
|
||||
}
|
||||
if let Err(e) = fuzz_unit(unit, ctx, opts, &mut res) {
|
||||
res.harness_error = Some(e.0);
|
||||
}
|
||||
|
||||
+61
-24
@@ -161,10 +161,8 @@ pub struct UnitRun {
|
||||
pub stdout: String,
|
||||
/// The child's stderr, forwarded as is.
|
||||
pub stderr: String,
|
||||
/// The unit counts as failed.
|
||||
pub failed: bool,
|
||||
/// The unit counts as partial.
|
||||
pub partial: bool,
|
||||
/// Comparison outcome, including unsupported native references.
|
||||
pub status: crate::UnitStatus,
|
||||
/// The child hit a setup error (exit status 2), e.g. an unwritable report.
|
||||
pub setup_error: bool,
|
||||
}
|
||||
@@ -181,14 +179,20 @@ fn exit_detail(status: ExitStatus) -> String {
|
||||
format!("exit code {}", status.code().unwrap_or(-1))
|
||||
}
|
||||
|
||||
/// Parses `difftest: 1 unit(s), P passed, Q partial, F failed[...]` into `(Q, F)`.
|
||||
fn parse_summary(line: &str) -> Option<(usize, usize)> {
|
||||
/// Parses the aggregate counts `(partial, failed, skipped)` of one child.
|
||||
fn parse_summary(line: &str) -> Option<(usize, usize, usize)> {
|
||||
let rest = line.strip_prefix("difftest: 1 unit(s), ")?;
|
||||
let mut parts = rest.split(", ");
|
||||
let _passed = parts.next()?.strip_suffix(" passed")?;
|
||||
let partial = parts.next()?.strip_suffix(" partial")?.parse().ok()?;
|
||||
let failed = parts.next()?.split(' ').next()?.parse().ok()?;
|
||||
Some((partial, failed))
|
||||
let skipped = parts
|
||||
.find_map(|p| p.strip_suffix(" skipped(platform)"))
|
||||
.map(str::parse)
|
||||
.transpose()
|
||||
.ok()?
|
||||
.unwrap_or(0);
|
||||
Some((partial, failed, skipped))
|
||||
}
|
||||
|
||||
/// Turns a finished child's output into what the parent prints and counts. A child that
|
||||
@@ -197,7 +201,7 @@ pub fn interpret(id: &str, status: Option<ExitStatus>, stdout: &str, stderr: &st
|
||||
let code = status.and_then(|s| s.code());
|
||||
let body = stdout.trim_end_matches('\n');
|
||||
let (block, last) = body.rsplit_once('\n').unwrap_or(("", body));
|
||||
if let (Some(c @ 0..=2), Some((partial, failed))) = (code, parse_summary(last)) {
|
||||
if let (Some(c @ 0..=2), Some((partial, failed, skipped))) = (code, parse_summary(last)) {
|
||||
let mut out = block.to_string();
|
||||
if !out.is_empty() {
|
||||
out.push('\n');
|
||||
@@ -205,8 +209,15 @@ pub fn interpret(id: &str, status: Option<ExitStatus>, stdout: &str, stderr: &st
|
||||
return UnitRun {
|
||||
stdout: out,
|
||||
stderr: stderr.to_string(),
|
||||
failed: failed > 0,
|
||||
partial: partial > 0,
|
||||
status: if failed > 0 {
|
||||
crate::UnitStatus::Fail
|
||||
} else if partial > 0 {
|
||||
crate::UnitStatus::Partial
|
||||
} else if skipped > 0 {
|
||||
crate::UnitStatus::SkippedPlatform
|
||||
} else {
|
||||
crate::UnitStatus::Green
|
||||
},
|
||||
setup_error: c == 2,
|
||||
};
|
||||
}
|
||||
@@ -226,8 +237,7 @@ pub fn interpret(id: &str, status: Option<ExitStatus>, stdout: &str, stderr: &st
|
||||
UnitRun {
|
||||
stdout: out,
|
||||
stderr: String::new(),
|
||||
failed: true,
|
||||
partial: false,
|
||||
status: crate::UnitStatus::Fail,
|
||||
setup_error: false,
|
||||
}
|
||||
}
|
||||
@@ -317,6 +327,7 @@ pub fn run_parallel(
|
||||
let mut printed = 0usize;
|
||||
let mut pool = Pool(Vec::new());
|
||||
let (mut failed, mut partial, mut setup_error) = (0usize, 0usize, false);
|
||||
let mut skipped = 0usize;
|
||||
let stdout = std::io::stdout();
|
||||
while printed < units.len() {
|
||||
while pool.0.len() < jobs.max(1) {
|
||||
@@ -361,8 +372,9 @@ pub fn run_parallel(
|
||||
let _ = so.flush();
|
||||
drop(so);
|
||||
let _ = std::io::stderr().write_all(r.stderr.as_bytes());
|
||||
failed += usize::from(r.failed);
|
||||
partial += usize::from(r.partial && !r.failed);
|
||||
failed += usize::from(r.status == crate::UnitStatus::Fail);
|
||||
partial += usize::from(r.status == crate::UnitStatus::Partial);
|
||||
skipped += usize::from(r.status == crate::UnitStatus::SkippedPlatform);
|
||||
setup_error |= r.setup_error;
|
||||
printed += 1;
|
||||
}
|
||||
@@ -371,9 +383,8 @@ pub fn run_parallel(
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"difftest: {} unit(s), {} passed, {partial} partial, {failed} failed",
|
||||
units.len(),
|
||||
units.len() - failed - partial
|
||||
"{}",
|
||||
crate::report::run_summary(units.len(), partial, failed, skipped)
|
||||
);
|
||||
// Timing goes to stderr so stdout stays identical to a serial run.
|
||||
let sum: Duration = times.iter().sum();
|
||||
@@ -467,17 +478,42 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn summary_lines() {
|
||||
assert_eq!(
|
||||
parse_summary(
|
||||
"difftest: 1 unit(s), 0 passed, 0 partial, 0 failed, 1 skipped(platform)"
|
||||
),
|
||||
Some((0, 0, 1))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_summary("difftest: 1 unit(s), 0 passed, 1 partial, 0 failed"),
|
||||
Some((1, 0))
|
||||
Some((1, 0, 0))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_summary("difftest: 1 unit(s), 0 passed, 0 partial, 1 failed, 1 dropped"),
|
||||
Some((0, 1))
|
||||
Some((0, 1, 0))
|
||||
);
|
||||
assert_eq!(parse_summary("PASS a cases=1"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn skipped_child_is_neither_a_pass_nor_a_failure() {
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
let summary = crate::report::run_summary(1, 0, 0, 1);
|
||||
let stdout = format!("SKIP u skipped(platform): unsupported reference\n{summary}\n");
|
||||
let r = interpret("u", Some(ExitStatus::from_raw(0)), &stdout, "");
|
||||
assert_eq!(r.status, crate::UnitStatus::SkippedPlatform);
|
||||
assert!(!r.setup_error);
|
||||
assert_eq!(
|
||||
r.stdout,
|
||||
"SKIP u skipped(platform): unsupported reference\n"
|
||||
);
|
||||
assert_eq!(
|
||||
crate::report::run_summary(3, 1, 0, 1),
|
||||
"difftest: 3 unit(s), 1 passed, 1 partial, 0 failed, 1 skipped(platform)"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn interprets_children() {
|
||||
@@ -490,31 +526,32 @@ mod tests {
|
||||
"",
|
||||
);
|
||||
assert_eq!(r.stdout, "PASS u cases=1\n report: r/u.json\n");
|
||||
assert!(!r.failed && !r.partial && !r.setup_error);
|
||||
assert_eq!(r.status, crate::UnitStatus::Green);
|
||||
assert!(!r.setup_error);
|
||||
let partial = interpret(
|
||||
"u",
|
||||
Some(ok),
|
||||
"PARTIAL u\ndifftest: 1 unit(s), 0 passed, 1 partial, 0 failed\n",
|
||||
"",
|
||||
);
|
||||
assert!(partial.partial && !partial.failed);
|
||||
assert_eq!(partial.status, crate::UnitStatus::Partial);
|
||||
let fail = interpret(
|
||||
"u",
|
||||
Some(ExitStatus::from_raw(1 << 8)),
|
||||
"FAIL u\ndifftest: 1 unit(s), 0 passed, 0 partial, 1 failed\n",
|
||||
"",
|
||||
);
|
||||
assert!(fail.failed);
|
||||
assert_eq!(fail.status, crate::UnitStatus::Fail);
|
||||
// SIGSEGV
|
||||
let crash = interpret("u", Some(ExitStatus::from_raw(11)), "", "a\nboom\n");
|
||||
assert!(crash.failed);
|
||||
assert_eq!(crash.status, crate::UnitStatus::Fail);
|
||||
assert_eq!(
|
||||
crash.stdout,
|
||||
"FAIL u (worker process killed by signal 11)\n stderr: a\n stderr: boom\n"
|
||||
);
|
||||
// A clean exit without the summary line is a crash too.
|
||||
let cut = interpret("u", Some(ok), "PASS u\n", "");
|
||||
assert!(cut.failed);
|
||||
assert_eq!(cut.status, crate::UnitStatus::Fail);
|
||||
assert!(cut
|
||||
.stdout
|
||||
.starts_with("PASS u\nFAIL u (worker process exit code 0)"));
|
||||
|
||||
+140
-23
@@ -1,6 +1,10 @@
|
||||
//! Known mismatches: cases that genuinely disagree with the C++ reference and are
|
||||
//! tracked in one checked-in list, `crates/difftest/known_mismatches.toml`, until the
|
||||
//! port is fixed.
|
||||
//! tracked in checked-in lists, one file per unit,
|
||||
//! `crates/difftest/known_mismatches/<unit id>.toml`, until the port is fixed.
|
||||
//!
|
||||
//! One file per unit (never one shared list) so parallel PRs touching different units
|
||||
//! never conflict. A unit with no file has no known mismatches; delete the file when
|
||||
//! its last entry goes.
|
||||
//!
|
||||
//! A listed case that fails its comparison is classified
|
||||
//! [`CaseOutcome::KnownMismatch`](crate::runner::CaseOutcome::KnownMismatch): it is not a
|
||||
@@ -8,13 +12,20 @@
|
||||
//! printed and written to the report like a failure. A listed case that does anything
|
||||
//! else (passes, defers, or is no longer produced) fails the unit, so the list cannot go
|
||||
//! stale. Never add a case here to get a unit green; every entry needs a
|
||||
//! `plan/findings.md` entry.
|
||||
//! `plan/findings/<slug>.md` entry. Every entry's `unit` must equal the file name.
|
||||
//!
|
||||
//! An entry whose disagreement comes from the C library the C++ reference runs on (e.g.
|
||||
//! glibc's allocator reusing a freed address under an upstream use-after-free) names it
|
||||
//! in `libc`. On any other C library the entry does not apply: the case runs as an
|
||||
//! ordinary case and must pass.
|
||||
//!
|
||||
//! ```toml
|
||||
//! # crates/difftest/known_mismatches/curve.curve_p1.toml
|
||||
//! [[mismatch]]
|
||||
//! unit = "curve.curve_p1"
|
||||
//! case = "gen0042"
|
||||
//! why = "C++ succeeds with 0 points, Rust returns status_3 (plan/findings.md: ...)"
|
||||
//! why = "C++ succeeds with 0 points, Rust returns status_3 (plan/findings/...)"
|
||||
//! # libc = "glibc" # optional: only where the C++ reference runs on glibc
|
||||
//! ```
|
||||
|
||||
use std::path::Path;
|
||||
@@ -23,19 +34,54 @@ use serde::Deserialize;
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
/// Path of the list, relative to the repository root.
|
||||
pub const KNOWN_MISMATCHES: &str = "crates/difftest/known_mismatches.toml";
|
||||
/// Directory of the per-unit lists, relative to the repository root.
|
||||
pub const KNOWN_MISMATCHES_DIR: &str = "crates/difftest/known_mismatches";
|
||||
|
||||
/// The former single shared list; it must not come back (one file per unit instead).
|
||||
pub const RETIRED_SHARED_LIST: &str = "crates/difftest/known_mismatches.toml";
|
||||
|
||||
/// Path of `unit`'s list, relative to the repository root.
|
||||
pub fn path_for(unit: &str) -> String {
|
||||
format!("{KNOWN_MISMATCHES_DIR}/{unit}.toml")
|
||||
}
|
||||
|
||||
/// One listed case.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct KnownMismatch {
|
||||
/// Unit id.
|
||||
/// Unit id (must equal the file name).
|
||||
pub unit: String,
|
||||
/// Case name within the unit.
|
||||
pub case: String,
|
||||
/// Why it disagrees (C++ vs Rust status) and where it is recorded.
|
||||
pub why: String,
|
||||
/// The C library the disagreement depends on; `None` applies everywhere.
|
||||
#[serde(default)]
|
||||
pub libc: Option<Libc>,
|
||||
}
|
||||
|
||||
impl KnownMismatch {
|
||||
/// Whether the entry applies to this build of the C++ reference.
|
||||
pub fn applies(&self) -> bool {
|
||||
self.libc.is_none_or(Libc::is_current)
|
||||
}
|
||||
}
|
||||
|
||||
/// A C library a known mismatch can be restricted to ([`KnownMismatch::libc`]).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Libc {
|
||||
/// GNU libc (Linux `*-gnu` targets), whose allocator the CI reference uses.
|
||||
Glibc,
|
||||
}
|
||||
|
||||
impl Libc {
|
||||
/// Whether the C++ reference in this process runs on this C library.
|
||||
pub fn is_current(self) -> bool {
|
||||
match self {
|
||||
Self::Glibc => cfg!(all(target_os = "linux", target_env = "gnu")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
@@ -45,26 +91,36 @@ struct File {
|
||||
mismatch: Vec<KnownMismatch>,
|
||||
}
|
||||
|
||||
/// Parses the list's text.
|
||||
pub fn parse(text: &str) -> Result<Vec<KnownMismatch>, HarnessError> {
|
||||
let f: File =
|
||||
toml::from_str(text).map_err(|e| HarnessError::new(format!("{KNOWN_MISMATCHES}: {e}")))?;
|
||||
/// Parses the text of `unit`'s list, rejecting entries for any other unit.
|
||||
pub fn parse(text: &str, unit: &str) -> Result<Vec<KnownMismatch>, HarnessError> {
|
||||
let path = path_for(unit);
|
||||
let f: File = toml::from_str(text).map_err(|e| HarnessError::new(format!("{path}: {e}")))?;
|
||||
if let Some(m) = f.mismatch.iter().find(|m| m.unit != unit) {
|
||||
return Err(HarnessError::new(format!(
|
||||
"{path}: entry for case `{}` names unit `{}`; it belongs in {}",
|
||||
m.case,
|
||||
m.unit,
|
||||
path_for(&m.unit)
|
||||
)));
|
||||
}
|
||||
Ok(f.mismatch)
|
||||
}
|
||||
|
||||
/// The entries of `unit` from `<root>/crates/difftest/known_mismatches.toml` (empty if
|
||||
/// the file does not exist).
|
||||
/// The entries of `unit` from `<root>/crates/difftest/known_mismatches/<unit>.toml`
|
||||
/// (empty if the file does not exist).
|
||||
pub fn for_unit(root: &Path, unit: &str) -> Result<Vec<KnownMismatch>, HarnessError> {
|
||||
let path = root.join(KNOWN_MISMATCHES);
|
||||
if unit.is_empty() || unit.contains(['/', '\\']) || unit.starts_with('.') {
|
||||
return Err(HarnessError::new(format!(
|
||||
"{KNOWN_MISMATCHES_DIR}: `{unit}` is not a unit id"
|
||||
)));
|
||||
}
|
||||
let path = root.join(path_for(unit));
|
||||
let text = match std::fs::read_to_string(&path) {
|
||||
Ok(t) => t,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
|
||||
Err(e) => return Err(HarnessError::new(format!("{}: {e}", path.display()))),
|
||||
};
|
||||
Ok(parse(&text)?
|
||||
.into_iter()
|
||||
.filter(|m| m.unit == unit)
|
||||
.collect())
|
||||
parse(&text, unit)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -72,17 +128,78 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_entries_and_rejects_typos() {
|
||||
let v = parse("[[mismatch]]\nunit = \"a.b\"\ncase = \"c\"\nwhy = \"w\"\n").unwrap();
|
||||
fn parses_entries_and_rejects_typos_and_foreign_units() {
|
||||
let v = parse(
|
||||
"[[mismatch]]\nunit = \"a.b\"\ncase = \"c\"\nwhy = \"w\"\n",
|
||||
"a.b",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(v.len(), 1);
|
||||
assert_eq!((v[0].unit.as_str(), v[0].case.as_str()), ("a.b", "c"));
|
||||
assert!(parse("").unwrap().is_empty());
|
||||
assert!(parse("[[mismatch]]\nunit = \"a\"\ncase = \"c\"\nwhy = \"w\"\nx = 1\n").is_err());
|
||||
assert!(parse("", "a.b").unwrap().is_empty());
|
||||
assert!(parse(
|
||||
"[[mismatch]]\nunit = \"a.b\"\ncase = \"c\"\nwhy = \"w\"\nx = 1\n",
|
||||
"a.b"
|
||||
)
|
||||
.is_err());
|
||||
let e = parse(
|
||||
"[[mismatch]]\nunit = \"a.c\"\ncase = \"c\"\nwhy = \"w\"\n",
|
||||
"a.b",
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(e.0.contains("known_mismatches/a.c.toml"), "{}", e.0);
|
||||
assert!(for_unit(Path::new("."), "../x").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checked_in_list_parses() {
|
||||
fn libc_restricted_entries_apply_only_on_that_libc() {
|
||||
let entry = |libc: &str| {
|
||||
parse(
|
||||
&format!("[[mismatch]]\nunit = \"a.b\"\ncase = \"c\"\nwhy = \"w\"\n{libc}"),
|
||||
"a.b",
|
||||
)
|
||||
};
|
||||
let any = entry("").unwrap();
|
||||
assert_eq!(any[0].libc, None);
|
||||
assert!(any[0].applies());
|
||||
let glibc = entry("libc = \"glibc\"\n").unwrap();
|
||||
assert_eq!(glibc[0].libc, Some(Libc::Glibc));
|
||||
assert_eq!(
|
||||
glibc[0].applies(),
|
||||
cfg!(all(target_os = "linux", target_env = "gnu"))
|
||||
);
|
||||
assert!(entry("libc = \"musl\"\n").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checked_in_lists_parse_and_match_their_file_names() {
|
||||
let root = crate::case::repo_root();
|
||||
assert!(for_unit(&root, "no.such.unit").unwrap().is_empty());
|
||||
assert!(
|
||||
!root.join(RETIRED_SHARED_LIST).exists(),
|
||||
"{RETIRED_SHARED_LIST} is retired: put entries in {KNOWN_MISMATCHES_DIR}/<unit id>.toml"
|
||||
);
|
||||
let units: Vec<String> = crate::plan::load(&root)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|u| u.id)
|
||||
.collect();
|
||||
let mut files: Vec<_> = std::fs::read_dir(root.join(KNOWN_MISMATCHES_DIR))
|
||||
.unwrap()
|
||||
.map(|e| e.unwrap().path())
|
||||
.collect();
|
||||
files.sort();
|
||||
assert!(!files.is_empty());
|
||||
for p in files {
|
||||
assert_eq!(
|
||||
p.extension().and_then(|e| e.to_str()),
|
||||
Some("toml"),
|
||||
"{p:?}"
|
||||
);
|
||||
let unit = p.file_stem().and_then(|s| s.to_str()).unwrap();
|
||||
assert!(units.iter().any(|u| u == unit), "{p:?}: no such unit");
|
||||
let v = for_unit(&root, unit).unwrap();
|
||||
assert!(!v.is_empty(), "{p:?}: empty list; delete the file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,13 @@ pub mod json;
|
||||
pub mod known;
|
||||
pub mod output;
|
||||
pub mod plan;
|
||||
pub mod platform;
|
||||
pub mod report;
|
||||
pub mod rng;
|
||||
pub mod runner;
|
||||
pub mod solidcraft;
|
||||
pub mod ssi;
|
||||
pub mod step;
|
||||
pub mod util;
|
||||
|
||||
pub use case::{Case, CaseContext, CaseSource};
|
||||
|
||||
+175
-26
@@ -12,7 +12,7 @@
|
||||
//! --jobs N (-j; default 1, 0 = all cores) # units in N child processes (`difftest::jobs`)
|
||||
//! cargo run -p difftest --features cpp -- --unit <id> --fuzz N [--fuzz-out <dir>] # FFI guard probing
|
||||
//! cargo run -p difftest --features cpp -- --ssi-corpus [--tol 1e-6|default] [--jobs N] [glob]
|
||||
//! cargo run -p difftest -- --ssi-corpus [glob] # the `ssi` engine only; no `cpp` feature needed
|
||||
//! cargo run -p difftest -- --ssi-corpus [glob] # the `ssi` and `solidcore-ssi` engines; no `cpp` needed
|
||||
//! cargo run -p difftest -- --solidcraft-cases [glob] # Rust only; no `cpp` feature needed
|
||||
//! cargo run -p difftest --features cpp -- --green --digest-dir <dir> [--digest-debug] # Rust side
|
||||
//! only, one `<dir>/<unit>.digest` per unit (native vs wasm32: scripts/wasm-digest.sh)
|
||||
@@ -30,10 +30,11 @@ use difftest::{fuzz, plan, registry, report, run_unit, CaseContext, RunOptions,
|
||||
|
||||
const USAGE: &str = "usage: difftest [--unit <id>]... [--green] [--all] [--list]
|
||||
[--status-of <id>] [--seeds N] [--filter <substr>] [--perturb] [--report-dir <dir>] [--no-report] [--jobs N]
|
||||
[--baseline <dir>] [--digest-dir <dir> [--digest-debug]]
|
||||
[--baseline <dir>] [--compare-reports <dir>] [--digest-dir <dir> [--digest-debug]]
|
||||
difftest --unit <id>... --fuzz N [--fuzz-out <dir>] [--seeds N] [--filter <substr>]
|
||||
difftest --ssi-corpus [--tol 1e-6|default] [--jobs N] [--report-dir <dir>] [--no-report] [case-glob]
|
||||
difftest --solidcraft-cases [--report-dir <dir>] [--no-report] [case-glob]";
|
||||
difftest --solidcraft-cases [--report-dir <dir>] [--no-report] [case-glob]
|
||||
difftest --step-corpus [--jobs N] [--report-dir <dir>] [--no-report] [file-glob]";
|
||||
|
||||
/// Default child processes of `--ssi-corpus`.
|
||||
const DEFAULT_SSI_JOBS: usize = 2;
|
||||
@@ -62,8 +63,10 @@ struct Args {
|
||||
report_dir: Option<PathBuf>,
|
||||
no_report: bool,
|
||||
baseline: Option<PathBuf>,
|
||||
compare_reports: Option<PathBuf>,
|
||||
ssi_corpus: bool,
|
||||
solidcraft_cases: bool,
|
||||
step_corpus: bool,
|
||||
tol: Option<String>,
|
||||
jobs: Option<usize>,
|
||||
glob: Option<String>,
|
||||
@@ -94,8 +97,12 @@ fn parse_args() -> Result<Args, String> {
|
||||
"--report-dir" => a.report_dir = Some(PathBuf::from(val("--report-dir")?)),
|
||||
"--no-report" => a.no_report = true,
|
||||
"--baseline" => a.baseline = Some(PathBuf::from(val("--baseline")?)),
|
||||
"--compare-reports" => {
|
||||
a.compare_reports = Some(PathBuf::from(val("--compare-reports")?));
|
||||
}
|
||||
"--ssi-corpus" => a.ssi_corpus = true,
|
||||
"--solidcraft-cases" => a.solidcraft_cases = true,
|
||||
"--step-corpus" => a.step_corpus = true,
|
||||
"--tol" => a.tol = Some(val("--tol")?),
|
||||
"--jobs" | "-j" => {
|
||||
let v = val("--jobs")?;
|
||||
@@ -114,7 +121,7 @@ fn parse_args() -> Result<Args, String> {
|
||||
}
|
||||
"-h" | "--help" => return Err(String::new()),
|
||||
other
|
||||
if (a.ssi_corpus || a.solidcraft_cases)
|
||||
if (a.ssi_corpus || a.solidcraft_cases || a.step_corpus)
|
||||
&& !other.starts_with('-')
|
||||
&& a.glob.is_none() =>
|
||||
{
|
||||
@@ -135,6 +142,10 @@ fn main() -> ExitCode {
|
||||
if raw.first().map(String::as_str) == Some("--ssi-case") {
|
||||
return difftest::ssi::run_child(raw.get(1..).unwrap_or(&[]));
|
||||
}
|
||||
// Child of `--step-corpus`: one kernel on one body (see `difftest::step::run_child`).
|
||||
if raw.first().map(String::as_str) == Some("--step-case") {
|
||||
return difftest::step::run_child(raw.get(1..).unwrap_or(&[]));
|
||||
}
|
||||
// Child of `--fuzz`: one batch of fuzz cases (see `difftest::fuzz::run_child`).
|
||||
if raw.first().map(String::as_str) == Some("--fuzz-exec") {
|
||||
return fuzz::run_child(raw.get(1..).unwrap_or(&[]));
|
||||
@@ -165,12 +176,24 @@ fn main() -> ExitCode {
|
||||
|| args.digest_dir.is_some()
|
||||
|| args.fuzz.is_some()
|
||||
|| args.ssi_corpus
|
||||
|| args.solidcraft_cases)
|
||||
|| args.solidcraft_cases
|
||||
|| args.step_corpus)
|
||||
{
|
||||
eprintln!("difftest: --baseline requires an unfiltered comparison run");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
|
||||
// Without the C++ kernel it builds the Rust side alone.
|
||||
if args.step_corpus {
|
||||
let opts = difftest::step::CorpusOptions {
|
||||
glob: args.glob.clone(),
|
||||
jobs: args.jobs.unwrap_or(DEFAULT_SSI_JOBS).max(1),
|
||||
report_dir: args.report_dir.clone(),
|
||||
no_report: args.no_report,
|
||||
};
|
||||
return difftest::step::run_corpus(&root, &opts);
|
||||
}
|
||||
|
||||
if args.solidcraft_cases {
|
||||
let opts = difftest::solidcraft::CasesOptions {
|
||||
glob: args.glob.clone(),
|
||||
@@ -195,6 +218,17 @@ fn main() -> ExitCode {
|
||||
return difftest::ssi::run_corpus(&root, &opts);
|
||||
}
|
||||
|
||||
// Reports only: no C++ kernel needed.
|
||||
if let Some(current) = &args.compare_reports {
|
||||
return match Waivers::load(&root) {
|
||||
Ok(waivers) => compare_reports_main(&args, current, &waivers),
|
||||
Err(e) => {
|
||||
eprintln!("difftest: {e}");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if !solidcore_ffi::LINKED {
|
||||
eprintln!(
|
||||
"difftest: built without the C++ kernel; run with \
|
||||
@@ -252,9 +286,7 @@ fn main() -> ExitCode {
|
||||
let res = run_unit(unit.as_ref(), &ctx, &opts);
|
||||
// The value-share floor is part of the gate (`--green` enforces it too).
|
||||
match baseline::value_share(&res) {
|
||||
Some(v)
|
||||
if res.status() != UnitStatus::Fail && waivers.covering(&v).is_none() =>
|
||||
{
|
||||
Some(v) if res.pass() && waivers.covering(&v).is_none() => {
|
||||
eprintln!("difftest: {id}: {}", v.detail);
|
||||
UnitStatus::Fail
|
||||
}
|
||||
@@ -380,6 +412,7 @@ fn main() -> ExitCode {
|
||||
|
||||
let mut failed = 0usize;
|
||||
let mut partial = 0usize;
|
||||
let mut skipped = 0usize;
|
||||
for id in &wanted {
|
||||
let Some(unit) = reg.iter().find(|u| u.id() == id.as_str()) else {
|
||||
println!("FAIL {id} (no difftest runner registered for this unit)");
|
||||
@@ -387,7 +420,7 @@ fn main() -> ExitCode {
|
||||
continue;
|
||||
};
|
||||
let res = run_unit(unit.as_ref(), &ctx, &opts);
|
||||
if args.perturb {
|
||||
if args.perturb && res.status() != UnitStatus::SkippedPlatform {
|
||||
let (n, caught) = res.perturb_stats();
|
||||
let ok = n > 0 && n == caught;
|
||||
let unperturbed = res
|
||||
@@ -455,18 +488,12 @@ fn main() -> ExitCode {
|
||||
.iter()
|
||||
.find(|p| p.id == id.as_str())
|
||||
.map(|p| p.status.as_str());
|
||||
let mut unit_failed = match res.status() {
|
||||
UnitStatus::Fail => true,
|
||||
UnitStatus::Partial if plan_status == Some("green") => {
|
||||
// Green units cannot contain deferred or known mismatches.
|
||||
println!(
|
||||
" FAIL {id}: plan status is green but cases are deferred or known mismatches"
|
||||
);
|
||||
true
|
||||
}
|
||||
UnitStatus::Partial | UnitStatus::Green => false,
|
||||
};
|
||||
if !unit_failed {
|
||||
let gate_error = selection_gate(res.status(), plan_status, args.baseline.is_some());
|
||||
if let Some(reason) = gate_error {
|
||||
println!(" FAIL {id}: {reason}");
|
||||
}
|
||||
let mut unit_failed = res.status() == UnitStatus::Fail || gate_error.is_some();
|
||||
if !unit_failed && res.status() != UnitStatus::SkippedPlatform {
|
||||
let mut violations: Vec<Violation> = baseline::value_share(&res).into_iter().collect();
|
||||
if let (Some(dir), None) = (&args.baseline, &args.filter) {
|
||||
match baseline::load_baseline(dir, id) {
|
||||
@@ -484,8 +511,10 @@ fn main() -> ExitCode {
|
||||
failed += 1;
|
||||
} else if res.status() == UnitStatus::Partial {
|
||||
partial += 1;
|
||||
} else if res.status() == UnitStatus::SkippedPlatform {
|
||||
skipped += 1;
|
||||
}
|
||||
if !args.no_report {
|
||||
if !args.no_report && !args.perturb {
|
||||
match report::write_unit_report(&report_dir, &res) {
|
||||
Ok(p) => println!(
|
||||
" report: {}",
|
||||
@@ -504,9 +533,8 @@ fn main() -> ExitCode {
|
||||
String::new()
|
||||
};
|
||||
println!(
|
||||
"difftest: {} unit(s), {} passed, {partial} partial, {failed} failed{dropped_note}",
|
||||
wanted.len(),
|
||||
wanted.len() - failed - partial
|
||||
"{}{dropped_note}",
|
||||
report::run_summary(wanted.len(), partial, failed, skipped)
|
||||
);
|
||||
if setup_error {
|
||||
ExitCode::from(2)
|
||||
@@ -517,7 +545,100 @@ fn main() -> ExitCode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Selection constraints are not case-level waivers. An unsupported oracle cannot
|
||||
/// satisfy an exact-base run, including when that baseline has no earlier unit report.
|
||||
fn selection_gate(
|
||||
status: UnitStatus,
|
||||
plan_status: Option<&str>,
|
||||
has_baseline: bool,
|
||||
) -> Option<&'static str> {
|
||||
match status {
|
||||
UnitStatus::Partial if plan_status == Some("green") => {
|
||||
Some("plan status is green but cases are deferred or known mismatches")
|
||||
}
|
||||
UnitStatus::SkippedPlatform if has_baseline => {
|
||||
Some("skipped reference cannot validate an exact-base comparison")
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints gate violations (waived ones with their reason); true if any is not waived.
|
||||
/// `--compare-reports DIR --baseline BASE`: the regression ratchet applied to the finished
|
||||
/// reports of a sweep run elsewhere (CI runs the sweep while the exact-base artifact is
|
||||
/// still being validated). Runs no case and needs no C++. The sweep itself already enforced
|
||||
/// per-unit status and the value-share floor; this adds exactly the `--baseline` checks:
|
||||
/// every green/partial baseline unit still has a report, seeds match, and no case is lost.
|
||||
fn compare_reports_main(args: &Args, current: &Path, waivers: &Waivers) -> ExitCode {
|
||||
let Some(base_dir) = &args.baseline else {
|
||||
eprintln!("difftest: --compare-reports requires --baseline");
|
||||
return ExitCode::from(2);
|
||||
};
|
||||
if args.green
|
||||
|| args.all
|
||||
|| args.perturb
|
||||
|| args.filter.is_some()
|
||||
|| args.seeds.is_some()
|
||||
|| !args.units.is_empty()
|
||||
{
|
||||
eprintln!("difftest: --compare-reports takes only --baseline");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
let setup = |e: difftest::HarnessError| {
|
||||
eprintln!("difftest: {e}");
|
||||
ExitCode::from(2)
|
||||
};
|
||||
let base_units = match baseline::baseline_units(base_dir) {
|
||||
Ok(u) => u,
|
||||
Err(e) => return setup(e),
|
||||
};
|
||||
let now_units = match baseline::report_units(current) {
|
||||
Ok(u) => u,
|
||||
Err(e) => return setup(e),
|
||||
};
|
||||
let mut failed = 0usize;
|
||||
for u in base_units.iter().filter(|u| !now_units.contains(u)) {
|
||||
let v = Violation {
|
||||
unit: u.clone(),
|
||||
check: Check::Ratchet,
|
||||
case: None,
|
||||
detail: format!("unit `{u}` passed in the baseline but the sweep produced no report"),
|
||||
};
|
||||
println!("{u}:");
|
||||
if print_violations(&[v], waivers) {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
for unit in &now_units {
|
||||
let (b, n) = match (
|
||||
baseline::load_baseline(base_dir, unit),
|
||||
baseline::load_baseline(current, unit),
|
||||
) {
|
||||
(Ok(Some(b)), Ok(Some(n))) => (b, n),
|
||||
(Ok(None), Ok(Some(_))) => {
|
||||
println!("{unit}: no earlier report (new unit)");
|
||||
continue;
|
||||
}
|
||||
(Err(e), _) | (_, Err(e)) => return setup(e),
|
||||
(_, Ok(None)) => return setup(difftest::HarnessError::new("report disappeared")),
|
||||
};
|
||||
let violations = baseline::ratchet_reports(&b, &n);
|
||||
if violations.is_empty() {
|
||||
println!("OK {unit} ({} baseline cases retained)", b.cases.len());
|
||||
} else {
|
||||
println!("{unit}:");
|
||||
if print_violations(&violations, waivers) {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"difftest: compared {} reports against the baseline: {failed} unit(s) regressed",
|
||||
now_units.len()
|
||||
);
|
||||
ExitCode::from(u8::from(failed > 0))
|
||||
}
|
||||
|
||||
fn print_violations(violations: &[Violation], waivers: &Waivers) -> bool {
|
||||
let (waived, open): (Vec<&Violation>, Vec<&Violation>) = violations
|
||||
.iter()
|
||||
@@ -634,6 +755,7 @@ fn fuzz_main(args: &Args, ctx: &CaseContext, root: &Path) -> ExitCode {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
wanted.retain(|w| seen.insert(w.clone()));
|
||||
let mut failed = 0usize;
|
||||
let mut skipped = 0usize;
|
||||
for id in &wanted {
|
||||
let Some(unit) = reg.iter().find(|u| u.id() == id.as_str()) else {
|
||||
println!("FUZZ-FAIL {id} (no difftest runner registered for this unit)");
|
||||
@@ -642,6 +764,10 @@ fn fuzz_main(args: &Args, ctx: &CaseContext, root: &Path) -> ExitCode {
|
||||
};
|
||||
let res = fuzz::run_fuzz(unit.as_ref(), ctx, &opts);
|
||||
println!("{}", fuzz::summary_line(&res));
|
||||
if res.skipped_platform.is_some() {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
for f in res.failures.iter().take(MAX_FUZZ_LINES) {
|
||||
let file = f.file.as_deref().map_or_else(String::new, |p| {
|
||||
format!(" -> {}", p.strip_prefix(root).unwrap_or(p).display())
|
||||
@@ -667,8 +793,13 @@ fn fuzz_main(args: &Args, ctx: &CaseContext, root: &Path) -> ExitCode {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
let skipped_note = if skipped > 0 {
|
||||
format!(", {skipped} skipped(platform)")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
println!(
|
||||
"difftest --fuzz: {} unit(s), {failed} failed, {:.1} s",
|
||||
"difftest --fuzz: {} unit(s), {failed} failed, {:.1} s{skipped_note}",
|
||||
wanted.len(),
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
@@ -678,3 +809,21 @@ fn fuzz_main(args: &Args, ctx: &CaseContext, root: &Path) -> ExitCode {
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn platform_skips_are_not_exact_base_comparisons() {
|
||||
for plan in [None, Some("green"), Some("partial")] {
|
||||
assert!(selection_gate(UnitStatus::SkippedPlatform, plan, false).is_none());
|
||||
assert!(selection_gate(UnitStatus::SkippedPlatform, plan, true).is_some());
|
||||
assert!(selection_gate(UnitStatus::Green, plan, true).is_none());
|
||||
}
|
||||
// A real deferral in a green unit must still fail, on every platform.
|
||||
assert!(selection_gate(UnitStatus::Partial, Some("green"), false).is_some());
|
||||
assert!(selection_gate(UnitStatus::Partial, Some("green"), true).is_some());
|
||||
assert!(selection_gate(UnitStatus::Partial, Some("partial"), true).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,10 +62,15 @@ pub fn parse_unit(text: &str) -> Result<PlanUnit, String> {
|
||||
.get("id")
|
||||
.and_then(toml::Value::as_str)
|
||||
.ok_or_else(|| "missing id".to_string())?;
|
||||
let status = v
|
||||
.get("status")
|
||||
.and_then(toml::Value::as_str)
|
||||
.unwrap_or("todo");
|
||||
let status = match v.get("status") {
|
||||
None => "todo",
|
||||
Some(value) => value.as_str().ok_or_else(|| {
|
||||
format!(
|
||||
"unit {id}: status must be a string, found {}",
|
||||
value.type_str()
|
||||
)
|
||||
})?,
|
||||
};
|
||||
if !STATUSES.contains(&status) {
|
||||
return Err(format!(
|
||||
"unit {id}: unknown status `{status}` (expected one of {})",
|
||||
@@ -110,6 +115,54 @@ mod tests {
|
||||
assert!(parse_unit("status = \"green\"\n").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_string_statuses() {
|
||||
for (status, kind) in [
|
||||
("[\"green\"]", "array"),
|
||||
("1", "integer"),
|
||||
("true", "boolean"),
|
||||
("{ value = \"green\" }", "table"),
|
||||
] {
|
||||
let err =
|
||||
parse_unit(&format!("id = \"malformed.unit\"\nstatus = {status}\n")).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
format!("unit malformed.unit: status must be a string, found {kind}")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_all_string_statuses() {
|
||||
for status in STATUSES {
|
||||
let unit = parse_unit(&format!("id = \"x\"\nstatus = \"{status}\"\n")).unwrap();
|
||||
assert_eq!(unit.id, "x");
|
||||
assert_eq!(unit.status, *status);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_status_defaults_to_todo() {
|
||||
let unit = parse_unit("id = \"x\"\n").unwrap();
|
||||
assert_eq!(unit.status, "todo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_rejects_non_string_status_instead_of_omitting_unit() {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"difftest-plan-malformed-status-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let units = dir.join("plan").join("units");
|
||||
fs::create_dir_all(&units).unwrap();
|
||||
fs::write(units.join("a.toml"), "id = \"a\"\nstatus = \"green\"\n").unwrap();
|
||||
fs::write(units.join("b.toml"), "id = \"b\"\nstatus = [\"green\"]\n").unwrap();
|
||||
let err = load(&dir).unwrap_err().to_string();
|
||||
assert!(err.contains("b.toml"));
|
||||
assert!(err.contains("unit b: status must be a string, found array"));
|
||||
fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_unit_dir_and_checks_file_names() {
|
||||
let dir = std::env::temp_dir().join(format!("difftest-plan-{}", std::process::id()));
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
//! Platform requirements of the native reference, independent of Rust portability.
|
||||
//!
|
||||
//! Some pinned C++ operations convert out-of-range floats to integers (undefined
|
||||
//! behavior). The Rust port deliberately reproduces the x86-64 reference on every
|
||||
//! target. A different native instruction cannot act as its parity oracle (#448).
|
||||
|
||||
/// A documented architecture requirement of a unit's C++ reference.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ReferencePlatform {
|
||||
/// Architecture name, using Rust's `std::env::consts::ARCH` spelling.
|
||||
pub arch: &'static str,
|
||||
/// Why other architectures cannot supply this unit's reference results.
|
||||
pub reason: &'static str,
|
||||
}
|
||||
|
||||
impl ReferencePlatform {
|
||||
/// Reason to skip comparison on `arch`; `None` when it is supported.
|
||||
pub fn skip_reason(self, arch: &str) -> Option<&'static str> {
|
||||
(arch != self.arch).then_some(self.reason)
|
||||
}
|
||||
}
|
||||
|
||||
/// Native voxel mapping and edge-length histograms depend on x86-64 float casts.
|
||||
pub const X86_64_HASH_CASTS: ReferencePlatform = ReferencePlatform {
|
||||
arch: "x86_64",
|
||||
reason: "C++ float-to-ULONG conversions require x86-64 (UB-35/UB-194, #448)",
|
||||
};
|
||||
|
||||
/// Native twist-period reduction uses an out-of-range float-to-LONG conversion.
|
||||
pub const X86_64_TWIST_CASTS: ReferencePlatform = ReferencePlatform {
|
||||
arch: "x86_64",
|
||||
reason: "C++ float-to-LONG conversion requires x86-64 (TwistVolume::EvaluateBoundingBoxSimple, #448)",
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::cell::Cell;
|
||||
|
||||
use crate::{Case, CaseContext, CaseSource, HarnessError, Output, RunOptions, UnitDiff};
|
||||
|
||||
#[test]
|
||||
fn reference_requirements_are_architecture_specific() {
|
||||
for platform in [X86_64_HASH_CASTS, X86_64_TWIST_CASTS] {
|
||||
assert_eq!(platform.skip_reason("x86_64"), None);
|
||||
for arch in ["aarch64", "arm", "x86", "wasm32"] {
|
||||
assert_eq!(platform.skip_reason(arch), Some(platform.reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Restricted {
|
||||
cases: Cell<usize>,
|
||||
rust: Cell<usize>,
|
||||
}
|
||||
|
||||
impl UnitDiff for Restricted {
|
||||
fn id(&self) -> &'static str {
|
||||
"test.platform"
|
||||
}
|
||||
|
||||
fn reference_platform(&self) -> Option<ReferencePlatform> {
|
||||
Some(ReferencePlatform {
|
||||
arch: "unsupported-test-architecture",
|
||||
reason: "test reference is unavailable",
|
||||
})
|
||||
}
|
||||
|
||||
fn cases(&self, _: &CaseContext) -> Result<Vec<Case>, HarnessError> {
|
||||
self.cases.set(self.cases.get() + 1);
|
||||
Ok(vec![Case {
|
||||
unit: self.id().into(),
|
||||
name: "value".into(),
|
||||
source: CaseSource::Inline,
|
||||
inputs: serde_json::Value::Null,
|
||||
}])
|
||||
}
|
||||
|
||||
fn run_cpp(&self, _: &Case) -> Result<Output, HarnessError> {
|
||||
panic!("unsupported C++ must never run")
|
||||
}
|
||||
|
||||
fn run_rust(&self, _: &Case) -> Result<Output, HarnessError> {
|
||||
self.rust.set(self.rust.get() + 1);
|
||||
Ok(Output::new().scalar("value", 42.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_comparisons_skip_before_fixture_construction_and_never_pass() {
|
||||
let unit = Restricted::default();
|
||||
let ctx = CaseContext {
|
||||
root: crate::case::repo_root(),
|
||||
seeds: 16,
|
||||
};
|
||||
for perturb in [false, true] {
|
||||
let r = crate::run_unit(
|
||||
&unit,
|
||||
&ctx,
|
||||
&RunOptions {
|
||||
perturb,
|
||||
..RunOptions::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(r.status(), crate::UnitStatus::SkippedPlatform);
|
||||
assert!(!r.pass());
|
||||
assert!(r.cases.is_empty());
|
||||
assert_eq!((unit.cases.get(), unit.rust.get()), (0, 0));
|
||||
let report = crate::report::unit_report(&r);
|
||||
assert_eq!(report["status"], "skipped(platform)");
|
||||
assert_eq!(report["pass"], false);
|
||||
assert_eq!(report["cases_passed"], 0);
|
||||
assert_eq!(report["skipped_platform"], "test reference is unavailable");
|
||||
assert!(crate::report::summary_line(&r)
|
||||
.starts_with("SKIP test.platform skipped(platform):"));
|
||||
// A platform-only report must never replace a trusted CI baseline.
|
||||
assert!(crate::baseline::parse_baseline(&report.to_string()).is_err());
|
||||
let baseline = crate::baseline::Baseline {
|
||||
unit: unit.id().into(),
|
||||
status: Some("green".into()),
|
||||
seeds: Some(16),
|
||||
cases: vec![crate::baseline::BaselineCase {
|
||||
name: "value".into(),
|
||||
outcome: "pass".into(),
|
||||
generated: false,
|
||||
compared_values: true,
|
||||
}],
|
||||
};
|
||||
assert!(!crate::baseline::ratchet(&baseline, &r).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzz_skips_without_workers_but_rust_digests_still_run() {
|
||||
let unit = Restricted::default();
|
||||
let ctx = CaseContext {
|
||||
root: crate::case::repo_root(),
|
||||
seeds: 16,
|
||||
};
|
||||
let r = crate::fuzz::run_fuzz(
|
||||
&unit,
|
||||
&ctx,
|
||||
&crate::fuzz::FuzzOptions {
|
||||
cases: 10,
|
||||
out_dir: ctx.root.join("bin/platform-test-unused"),
|
||||
filter: None,
|
||||
},
|
||||
);
|
||||
assert!(r.skipped_platform.is_some());
|
||||
assert!(!r.pass());
|
||||
assert!(r.verdicts.is_empty());
|
||||
assert_eq!((unit.cases.get(), unit.rust.get()), (0, 0));
|
||||
assert!(crate::fuzz::summary_line(&r).starts_with("FUZZ-SKIP test.platform"));
|
||||
|
||||
let digest = crate::runner::digest_unit(&unit, &ctx, &RunOptions::default(), false);
|
||||
assert!(digest.harness_error.is_none());
|
||||
assert_eq!(digest.lines.len(), 1);
|
||||
assert_eq!((unit.cases.get(), unit.rust.get()), (1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "cpp")]
|
||||
fn registry_marks_only_the_documented_units() {
|
||||
let marked: Vec<_> = crate::registry()
|
||||
.iter()
|
||||
.filter_map(|u| {
|
||||
u.reference_platform().map(|p| {
|
||||
assert_eq!(p.arch, "x86_64");
|
||||
u.id()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
marked,
|
||||
[
|
||||
"geom.hash_table",
|
||||
"heal.heal_data_p1",
|
||||
"heal.heal_data_p2",
|
||||
"heal.heal_data_p3",
|
||||
"surface.twist_volume"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,9 @@ pub fn unit_report(r: &UnitResult) -> Value {
|
||||
m.insert("unit".into(), json!(r.unit));
|
||||
m.insert("pass".into(), json!(r.pass()));
|
||||
m.insert("status".into(), json!(r.status().as_str()));
|
||||
if let Some(reason) = &r.skipped_platform {
|
||||
m.insert("skipped_platform".into(), json!(reason));
|
||||
}
|
||||
m.insert("rel_tol".into(), num(REL_TOL));
|
||||
m.insert("abs_floors".into(), Value::Array(floors));
|
||||
m.insert("seeds".into(), json!(r.seeds));
|
||||
@@ -134,7 +137,7 @@ pub fn write_unit_report(dir: &Path, r: &UnitResult) -> Result<PathBuf, HarnessE
|
||||
/// Aggregate counts recorded in a report (including vacuous and known outcomes).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReportCounts {
|
||||
/// `green`, `partial` or `fail`.
|
||||
/// `green`, `partial`, `fail` or `skipped(platform)`.
|
||||
pub status: String,
|
||||
/// All cases.
|
||||
pub total: u64,
|
||||
@@ -200,14 +203,35 @@ impl ReportCounts {
|
||||
}
|
||||
}
|
||||
|
||||
/// Aggregate CLI summary. Skipped units never contribute to the passed count.
|
||||
/// The optional suffix preserves existing output when all references are supported.
|
||||
pub fn run_summary(total: usize, partial: usize, failed: usize, skipped: usize) -> String {
|
||||
let mut s = format!(
|
||||
"difftest: {total} unit(s), {} passed, {partial} partial, {failed} failed",
|
||||
total.saturating_sub(partial + failed + skipped)
|
||||
);
|
||||
if skipped > 0 {
|
||||
let _ = write!(s, ", {skipped} skipped(platform)");
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// One-line summary, e.g.
|
||||
/// `PASS harness.selftest cases=40 passed=40 failed=0 deferred=0 max_rel_err=1.1e-16 with_values=36 no_values=4 vacuous=1`.
|
||||
/// The status word is `PASS` (green), `PARTIAL` or `FAIL`; `with_values` / `no_values`
|
||||
/// The status word is `PASS` (green), `PARTIAL`, `FAIL` or `SKIP`; a skip prints
|
||||
/// only the unit and its platform reason. Otherwise, `with_values` / `no_values`
|
||||
/// split the passing cases into those that compared at least one value and those that
|
||||
/// compared nothing (the same error on both sides, or empty outputs); `vacuous` counts
|
||||
/// the `no_values` cases where both sides succeeded, so only the status agreed; with deferred
|
||||
/// cases the line ends with ` blocked_by=<unit>(<cases>),...`.
|
||||
pub fn summary_line(r: &UnitResult) -> String {
|
||||
if r.status() == UnitStatus::SkippedPlatform {
|
||||
return format!(
|
||||
"SKIP {} skipped(platform): {}",
|
||||
r.unit,
|
||||
r.skipped_platform.as_deref().unwrap_or_default()
|
||||
);
|
||||
}
|
||||
let with_values = r.passed_with_values();
|
||||
let mut s = format!(
|
||||
"{} {} cases={} passed={} failed={} deferred={} max_rel_err={:e} with_values={with_values} no_values={} vacuous={}",
|
||||
@@ -215,6 +239,7 @@ pub fn summary_line(r: &UnitResult) -> String {
|
||||
UnitStatus::Green => "PASS",
|
||||
UnitStatus::Partial => "PARTIAL",
|
||||
UnitStatus::Fail => "FAIL",
|
||||
UnitStatus::SkippedPlatform => "SKIP",
|
||||
},
|
||||
r.unit,
|
||||
r.cases.len(),
|
||||
@@ -262,6 +287,7 @@ mod tests {
|
||||
unit: "a.b".into(),
|
||||
cases: Vec::new(),
|
||||
harness_error: None,
|
||||
skipped_platform: None,
|
||||
abs_floors: Vec::new(),
|
||||
seeds: 16,
|
||||
timing: crate::runner::UnitTiming::default(),
|
||||
|
||||
@@ -18,6 +18,13 @@ pub trait UnitDiff {
|
||||
/// Unit id as in `plan/units/<id>.toml` (e.g. `nlib.knots`).
|
||||
fn id(&self) -> &'static str;
|
||||
|
||||
/// Architecture restriction of the C++ oracle, if any. This does not restrict
|
||||
/// Rust-only digest runs. Unsupported comparisons are skipped before fixtures
|
||||
/// are constructed and never count as parity passes.
|
||||
fn reference_platform(&self) -> Option<crate::platform::ReferencePlatform> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Runs the upstream C++ (via `solidcore_ffi::units::*`).
|
||||
/// Return `Ok(Output::err(..))` when the *operation* fails and `Err` only
|
||||
/// for harness problems (bad inputs); `Err` always fails the case. Native fixture
|
||||
@@ -115,7 +122,7 @@ pub struct CaseResult {
|
||||
pub perturbed: Option<&'static str>,
|
||||
/// The Rust side's [`Output::diagnostics`] (reported, never compared).
|
||||
pub diagnostics: BTreeMap<String, i64>,
|
||||
/// The case is listed in `known_mismatches.toml` (the entry's `why`).
|
||||
/// The case is listed in `known_mismatches/<unit>.toml` (the entry's `why`).
|
||||
pub known_mismatch: Option<String>,
|
||||
}
|
||||
|
||||
@@ -133,7 +140,7 @@ pub enum CaseOutcome {
|
||||
Deferred,
|
||||
/// Skipped because the platform's C++ standard library is not libstdc++.
|
||||
SkippedPlatform,
|
||||
/// A mismatch listed in `known_mismatches.toml` ([`crate::known`]): not a pass,
|
||||
/// A mismatch listed in `known_mismatches/<unit>.toml` ([`crate::known`]): not a pass,
|
||||
/// keeps the unit from being green, reported like a failure.
|
||||
KnownMismatch,
|
||||
}
|
||||
@@ -224,15 +231,18 @@ pub enum UnitStatus {
|
||||
Partial,
|
||||
/// Any failing case, a harness error, no cases, or only rejected fixtures.
|
||||
Fail,
|
||||
/// The C++ reference is unsupported on this architecture; no cases ran.
|
||||
SkippedPlatform,
|
||||
}
|
||||
|
||||
impl UnitStatus {
|
||||
/// `green`, `partial` or `fail`.
|
||||
/// `green`, `partial`, `fail` or `skipped(platform)`.
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Green => "green",
|
||||
Self::Partial => "partial",
|
||||
Self::Fail => "fail",
|
||||
Self::SkippedPlatform => "skipped(platform)",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,6 +256,8 @@ pub struct UnitResult {
|
||||
pub cases: Vec<CaseResult>,
|
||||
/// Failure to build the case list.
|
||||
pub harness_error: Option<String>,
|
||||
/// Reason the C++ reference cannot run on this architecture (no cases ran).
|
||||
pub skipped_platform: Option<String>,
|
||||
/// Documented absolute floors used.
|
||||
pub abs_floors: Vec<AbsFloor>,
|
||||
/// `--seeds` value used.
|
||||
@@ -307,13 +319,13 @@ impl UnitTiming {
|
||||
}
|
||||
|
||||
impl UnitResult {
|
||||
/// Green, partial or fail (see [`UnitStatus`]).
|
||||
/// Comparison status, including an unsupported reference (see [`UnitStatus`]).
|
||||
pub fn status(&self) -> UnitStatus {
|
||||
if self.harness_error.is_some()
|
||||
|| self.cases.is_empty()
|
||||
|| self.failed() > 0
|
||||
|| self.cases.len() == self.rejected()
|
||||
{
|
||||
if self.harness_error.is_some() || self.failed() > 0 {
|
||||
UnitStatus::Fail
|
||||
} else if self.skipped_platform.is_some() {
|
||||
UnitStatus::SkippedPlatform
|
||||
} else if self.cases.is_empty() || self.cases.len() == self.rejected() {
|
||||
UnitStatus::Fail
|
||||
} else if self.deferred() > 0 || self.known_mismatches() > 0 {
|
||||
UnitStatus::Partial
|
||||
@@ -324,9 +336,9 @@ impl UnitResult {
|
||||
}
|
||||
}
|
||||
|
||||
/// No failure: the unit is green or partial (the exit status is 0).
|
||||
/// Parity was checked: the unit is green or partial. Skipped units do not pass.
|
||||
pub fn pass(&self) -> bool {
|
||||
self.status() != UnitStatus::Fail
|
||||
matches!(self.status(), UnitStatus::Green | UnitStatus::Partial)
|
||||
}
|
||||
|
||||
/// Number of passing cases.
|
||||
@@ -489,10 +501,17 @@ pub fn run_unit(unit: &dyn UnitDiff, ctx: &CaseContext, opts: &RunOptions) -> Un
|
||||
unit: unit.id().to_string(),
|
||||
cases: Vec::new(),
|
||||
harness_error: None,
|
||||
skipped_platform: unit
|
||||
.reference_platform()
|
||||
.and_then(|p| p.skip_reason(std::env::consts::ARCH))
|
||||
.map(str::to_string),
|
||||
abs_floors: unit.abs_floors().to_vec(),
|
||||
seeds: ctx.seeds,
|
||||
timing: UnitTiming::default(),
|
||||
};
|
||||
if res.skipped_platform.is_some() {
|
||||
return res;
|
||||
}
|
||||
let digest = std::env::var_os("DIFFTEST_RUST_DIGEST").is_some();
|
||||
let t0 = Instant::now();
|
||||
let cases = unit.cases(ctx);
|
||||
@@ -520,12 +539,18 @@ pub fn run_unit(unit: &dyn UnitDiff, ctx: &CaseContext, opts: &RunOptions) -> Un
|
||||
if !missing.is_empty() {
|
||||
res.harness_error = Some(format!(
|
||||
"{}: no such case(s): {}",
|
||||
crate::known::KNOWN_MISMATCHES,
|
||||
crate::known::path_for(unit.id()),
|
||||
missing.join(", ")
|
||||
));
|
||||
return res;
|
||||
}
|
||||
}
|
||||
// Entries for another C library are checked for existence above but do not apply
|
||||
// here: their cases run as ordinary cases.
|
||||
let known: Vec<_> = known
|
||||
.into_iter()
|
||||
.filter(crate::known::KnownMismatch::applies)
|
||||
.collect();
|
||||
for case in cases {
|
||||
if let Some(f) = &opts.filter {
|
||||
if !case.name.contains(f.as_str()) {
|
||||
@@ -655,7 +680,7 @@ pub fn run_unit(unit: &dyn UnitDiff, ctx: &CaseContext, opts: &RunOptions) -> Un
|
||||
{
|
||||
cr.harness_error = Some(format!(
|
||||
"listed in {} but the case is now {}: remove the entry",
|
||||
crate::known::KNOWN_MISMATCHES,
|
||||
crate::known::path_for(unit.id()),
|
||||
cr.outcome().as_str()
|
||||
));
|
||||
}
|
||||
@@ -799,6 +824,7 @@ mod tests {
|
||||
unit: "u".into(),
|
||||
cases: vec![case(None, true), case(d(), true)],
|
||||
harness_error: None,
|
||||
skipped_platform: None,
|
||||
abs_floors: Vec::new(),
|
||||
seeds: 0,
|
||||
timing: UnitTiming::default(),
|
||||
@@ -1012,9 +1038,9 @@ mod tests {
|
||||
fn known_root(tag: &str, list: &str) -> CaseContext {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("difftest-known-{tag}-{}", std::process::id()));
|
||||
let dir = root.join("crates/difftest");
|
||||
let dir = root.join(crate::known::KNOWN_MISMATCHES_DIR);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("known_mismatches.toml"), list).unwrap();
|
||||
std::fs::write(dir.join("empty.unit.toml"), list).unwrap();
|
||||
CaseContext { root, seeds: 0 }
|
||||
}
|
||||
|
||||
|
||||
@@ -24,14 +24,19 @@ use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use solidcore::boolean::api_brep as boolean;
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::geom::object::ObjectRef;
|
||||
use solidcore::geom::vector3d::{Point3, Vec3};
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::mass::integrator::properties::{brep_volume, PropertyAttributes};
|
||||
use solidcore::primitive::api_curves as curves;
|
||||
use solidcore::primitive::api_primitives as primitives;
|
||||
use solidcore::primitive::primitive_creation::SweepOptions;
|
||||
use solidcore::surface::surface::p2::SurfaceKind;
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::sweep::api_brep as sweeps;
|
||||
use solidcore::sweep::api_surfaces::create_skin_primitive;
|
||||
use solidcore::topology::brep::p1::regions;
|
||||
use solidcore::topology::brep::p2::ArenaNotifyState;
|
||||
use solidcore::topology::brep::p3;
|
||||
@@ -159,6 +164,103 @@ pub enum Step {
|
||||
/// Tool.
|
||||
b: String,
|
||||
},
|
||||
/// `SmApiCreateLinearSweep`: the closed `profile` (built in `plane`, moved by
|
||||
/// `origin`) swept `distance` along the plane's normal, capped at both ends.
|
||||
Extrude {
|
||||
/// Body written.
|
||||
id: String,
|
||||
/// Closed loops of segments, in the plane's (u, v) coordinates.
|
||||
profile: Vec<Seg>,
|
||||
/// Plane of the profile (default XY).
|
||||
#[serde(default)]
|
||||
plane: ProfilePlane,
|
||||
/// Where the plane's origin sits.
|
||||
#[serde(default)]
|
||||
origin: P3,
|
||||
/// Sweep length along the normal.
|
||||
distance: f64,
|
||||
},
|
||||
/// `SmApiCreateRotationalSweep`: the closed `profile` (built in `plane`, moved by
|
||||
/// `origin`) turned `angle_deg` about the axis through `axis_point` along
|
||||
/// `axis_dir`, capped at both ends.
|
||||
Revolve {
|
||||
/// Body written.
|
||||
id: String,
|
||||
/// Closed loops of segments, in the plane's (u, v) coordinates.
|
||||
profile: Vec<Seg>,
|
||||
/// Plane of the profile (default XY).
|
||||
#[serde(default)]
|
||||
plane: ProfilePlane,
|
||||
/// Where the plane's origin sits.
|
||||
#[serde(default)]
|
||||
origin: P3,
|
||||
/// A point on the axis (global coordinates).
|
||||
axis_point: P3,
|
||||
/// Axis direction (global coordinates).
|
||||
axis_dir: P3,
|
||||
/// Turn in degrees.
|
||||
angle_deg: f64,
|
||||
},
|
||||
/// A circle of `radius` swept along `path` (built in `plane`, moved by `origin`),
|
||||
/// capped at both ends: `SmApiCreatePipeSweep` for one segment,
|
||||
/// `SmApiCreateSweepAlongPlanarPath` of a circle normal to the path start for more.
|
||||
Pipe {
|
||||
/// Body written.
|
||||
id: String,
|
||||
/// Pipe radius.
|
||||
radius: f64,
|
||||
/// Lines and arcs joined end to end, in the plane's (u, v) coordinates.
|
||||
path: Vec<Seg>,
|
||||
/// Plane of the path (default XY).
|
||||
#[serde(default)]
|
||||
plane: ProfilePlane,
|
||||
/// Where the plane's origin sits.
|
||||
#[serde(default)]
|
||||
origin: P3,
|
||||
},
|
||||
/// `SmApiCreateSkinPrimitive`: a solid lofted through `sections` in order, each one
|
||||
/// closed profile, of `degree` in the loft direction (1, ruled, by default), capped
|
||||
/// at the first and last section.
|
||||
Loft {
|
||||
/// Body written.
|
||||
id: String,
|
||||
/// The sections, at least two.
|
||||
sections: Vec<LoftSection>,
|
||||
/// Degree in the loft direction: 1 linear, 2 quadratic, 3 cubic.
|
||||
#[serde(default = "ruled")]
|
||||
degree: usize,
|
||||
},
|
||||
/// `SmApiCreateCurveSweep`: the closed `profile` (built in `plane`, moved by
|
||||
/// `origin`, so its plane is normal to the path's start tangent) swept along the
|
||||
/// single `path` arc or line (in the XY plane at the world origin), capped at both
|
||||
/// ends. The profile keeps its place relative to the path's start.
|
||||
Sweep {
|
||||
/// Body written.
|
||||
id: String,
|
||||
/// Closed loops of segments, in the plane's (u, v) coordinates.
|
||||
profile: Vec<Seg>,
|
||||
/// Plane of the profile (default XY).
|
||||
#[serde(default)]
|
||||
plane: ProfilePlane,
|
||||
/// Where the plane's origin sits.
|
||||
#[serde(default)]
|
||||
origin: P3,
|
||||
/// The path: one arc or line in the plane's (u, v) coordinates.
|
||||
path: Seg,
|
||||
},
|
||||
/// Reflection of `body` in the plane `normal = at`; the original is not changed, join
|
||||
/// the two with a `union` step. `SM_API` has no mirror, so this defers (see `step_rust`).
|
||||
Mirror {
|
||||
/// Body written.
|
||||
id: String,
|
||||
/// Body reflected.
|
||||
body: String,
|
||||
/// Axis the mirror plane is perpendicular to.
|
||||
normal: MirrorAxis,
|
||||
/// Where the plane cuts that axis.
|
||||
#[serde(default)]
|
||||
at: f64,
|
||||
},
|
||||
/// `SmApiFilletEdges`, circular section.
|
||||
Fillet {
|
||||
/// Body written.
|
||||
@@ -213,6 +315,85 @@ pub enum Step {
|
||||
},
|
||||
}
|
||||
|
||||
/// One piece of a profile, in the profile plane's (u, v) coordinates. Pieces that share
|
||||
/// end points form closed loops; a profile may hold several (outer boundary and holes).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Seg {
|
||||
/// `SmApiCreateLineSegment`.
|
||||
Line {
|
||||
/// Start.
|
||||
from: [f64; 2],
|
||||
/// End.
|
||||
to: [f64; 2],
|
||||
},
|
||||
/// A closed polygon: one line segment per consecutive pair, the last back to the
|
||||
/// first.
|
||||
Polygon {
|
||||
/// Corners, at least three.
|
||||
points: Vec<[f64; 2]>,
|
||||
},
|
||||
/// `SmApiCreateArc`, counter-clockwise from `start_deg` to `end_deg`.
|
||||
Arc {
|
||||
/// Centre.
|
||||
center: [f64; 2],
|
||||
/// Radius.
|
||||
radius: f64,
|
||||
/// Start angle from +u.
|
||||
start_deg: f64,
|
||||
/// End angle from +u.
|
||||
end_deg: f64,
|
||||
},
|
||||
/// `SmApiCreateCircle`, analytic.
|
||||
Circle {
|
||||
/// Centre.
|
||||
center: [f64; 2],
|
||||
/// Radius.
|
||||
radius: f64,
|
||||
},
|
||||
}
|
||||
|
||||
/// One section of a `loft`: a closed profile built in `plane` and moved by `origin`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LoftSection {
|
||||
/// One closed loop of segments, in the plane's (u, v) coordinates.
|
||||
pub profile: Vec<Seg>,
|
||||
/// Plane of the profile (default XY).
|
||||
#[serde(default)]
|
||||
pub plane: ProfilePlane,
|
||||
/// Where the plane's origin sits.
|
||||
#[serde(default)]
|
||||
pub origin: P3,
|
||||
}
|
||||
|
||||
/// The plane a profile is drawn in, as a right-handed rotation of XY.
|
||||
#[derive(Debug, Clone, Copy, Default, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProfilePlane {
|
||||
/// u along X, v along Y, normal +Z.
|
||||
#[default]
|
||||
Xy,
|
||||
/// u along Y, v along Z, normal +X.
|
||||
Yz,
|
||||
/// u along Z, v along X, normal +Y.
|
||||
Zx,
|
||||
/// u along X, v along Z, normal -Y (XY turned +90 degrees about X).
|
||||
Xz,
|
||||
}
|
||||
|
||||
/// The axis a mirror plane is perpendicular to.
|
||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MirrorAxis {
|
||||
/// The plane `x = at`.
|
||||
X,
|
||||
/// The plane `y = at`.
|
||||
Y,
|
||||
/// The plane `z = at`.
|
||||
Z,
|
||||
}
|
||||
|
||||
/// A grid of positions: `pitch[0] * i + pitch[1] * j` for `i < count[0]`,
|
||||
/// `j < count[1]`, `i` varying fastest.
|
||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||
@@ -274,6 +455,10 @@ fn z_axis() -> P3 {
|
||||
[0.0, 0.0, 1.0]
|
||||
}
|
||||
|
||||
fn ruled() -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
/// A loaded case: `<file stem>/<name>` and its file.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Loaded {
|
||||
@@ -348,6 +533,12 @@ impl Step {
|
||||
| Self::Union { id, .. }
|
||||
| Self::Difference { id, .. }
|
||||
| Self::Intersection { id, .. }
|
||||
| Self::Extrude { id, .. }
|
||||
| Self::Revolve { id, .. }
|
||||
| Self::Pipe { id, .. }
|
||||
| Self::Loft { id, .. }
|
||||
| Self::Sweep { id, .. }
|
||||
| Self::Mirror { id, .. }
|
||||
| Self::Fillet { id, .. }
|
||||
| Self::OffsetFaces { id, .. }
|
||||
| Self::SheetCorner { id, .. } => id,
|
||||
@@ -360,15 +551,46 @@ impl Step {
|
||||
Self::Box { .. }
|
||||
| Self::Sphere { .. }
|
||||
| Self::Cylinder { .. }
|
||||
| Self::Extrude { .. }
|
||||
| Self::Revolve { .. }
|
||||
| Self::Pipe { .. }
|
||||
| Self::Loft { .. }
|
||||
| Self::Sweep { .. }
|
||||
| Self::SheetCorner { .. } => Vec::new(),
|
||||
Self::Union { a, b, .. }
|
||||
| Self::Difference { a, b, .. }
|
||||
| Self::Intersection { a, b, .. } => vec![a, b],
|
||||
Self::Fillet { body, .. } | Self::OffsetFaces { body, .. } => vec![body],
|
||||
Self::Fillet { body, .. }
|
||||
| Self::OffsetFaces { body, .. }
|
||||
| Self::Mirror { body, .. } => vec![body],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A profile has pieces, and every number in them is finite.
|
||||
fn valid_profile(profile: &[Seg]) -> bool {
|
||||
let pt = |p: &[f64; 2]| p.iter().all(|x| x.is_finite());
|
||||
!profile.is_empty()
|
||||
&& profile.iter().all(|s| match s {
|
||||
Seg::Line { from, to } => pt(from) && pt(to),
|
||||
Seg::Polygon { points } => points.len() >= 3 && points.iter().all(pt),
|
||||
Seg::Arc {
|
||||
center,
|
||||
radius,
|
||||
start_deg,
|
||||
end_deg,
|
||||
} => pt(center) && radius.is_finite() && start_deg.is_finite() && end_deg.is_finite(),
|
||||
Seg::Circle { center, radius } => pt(center) && radius.is_finite(),
|
||||
})
|
||||
}
|
||||
|
||||
/// A sweep path has only lines and arcs (and its numbers are finite, checked by
|
||||
/// [`valid_profile`]).
|
||||
fn valid_path_segments(path: &[Seg]) -> bool {
|
||||
path.iter()
|
||||
.all(|s| matches!(s, Seg::Line { .. } | Seg::Arc { .. }))
|
||||
}
|
||||
|
||||
/// Every step reads only bodies written before it, the last step writes [`RESULT`],
|
||||
/// and every number is finite.
|
||||
///
|
||||
@@ -402,6 +624,60 @@ pub fn validate(case: &Case) -> Result<(), String> {
|
||||
Step::Difference {
|
||||
pattern: Some(p), ..
|
||||
} => p.pitch.iter().flatten().all(|x| x.is_finite()),
|
||||
Step::Extrude {
|
||||
profile,
|
||||
origin,
|
||||
distance,
|
||||
..
|
||||
} => {
|
||||
origin.iter().all(|x| x.is_finite())
|
||||
&& distance.is_finite()
|
||||
&& valid_profile(profile)
|
||||
}
|
||||
Step::Revolve {
|
||||
profile,
|
||||
origin,
|
||||
axis_point,
|
||||
axis_dir,
|
||||
angle_deg,
|
||||
..
|
||||
} => {
|
||||
origin
|
||||
.iter()
|
||||
.chain(axis_point)
|
||||
.chain(axis_dir)
|
||||
.all(|x| x.is_finite())
|
||||
&& angle_deg.is_finite()
|
||||
&& valid_profile(profile)
|
||||
}
|
||||
Step::Pipe {
|
||||
radius,
|
||||
path,
|
||||
origin,
|
||||
..
|
||||
} => {
|
||||
radius.is_finite()
|
||||
&& origin.iter().all(|x| x.is_finite())
|
||||
&& valid_path_segments(path)
|
||||
&& valid_profile(path)
|
||||
}
|
||||
Step::Loft { sections, .. } => {
|
||||
sections.len() >= 2
|
||||
&& sections.iter().all(|s| {
|
||||
s.origin.iter().all(|x| x.is_finite()) && valid_profile(&s.profile)
|
||||
})
|
||||
}
|
||||
Step::Mirror { at, .. } => at.is_finite(),
|
||||
Step::Sweep {
|
||||
profile,
|
||||
origin,
|
||||
path,
|
||||
..
|
||||
} => {
|
||||
origin.iter().all(|x| x.is_finite())
|
||||
&& valid_profile(profile)
|
||||
&& valid_path_segments(std::slice::from_ref(path))
|
||||
}
|
||||
Step::Fillet { edges, radius, .. } => {
|
||||
edges.iter().flatten().all(|x| x.is_finite()) && radius.is_finite()
|
||||
}
|
||||
@@ -578,6 +854,71 @@ fn cylinder(k: &mut Kernel, origin: P3, axis: P3, radius: f64, height: f64) -> R
|
||||
Ok(b)
|
||||
}
|
||||
|
||||
/// The curves of `profile`, drawn in XY, turned onto `plane` about the origin and moved
|
||||
/// by `origin`.
|
||||
fn profile_curves(
|
||||
k: &mut Kernel,
|
||||
profile: &[Seg],
|
||||
plane: ProfilePlane,
|
||||
origin: P3,
|
||||
) -> Result<Vec<Curve>, Run> {
|
||||
let at = |[u, v]: [f64; 2]| Vec3::new(u, v, 0.0);
|
||||
let mut out: Vec<Curve> = Vec::new();
|
||||
for seg in profile {
|
||||
match seg {
|
||||
Seg::Line { from, to } => {
|
||||
let l = curves::create_line_segment(at(*from), at(*to)).map_err(|e| solid(&e))?;
|
||||
out.push(Curve::from(l));
|
||||
}
|
||||
Seg::Polygon { points } => {
|
||||
// Each point to the next, the last back to the first.
|
||||
for (from, to) in points.iter().zip(points.iter().cycle().skip(1)) {
|
||||
let l =
|
||||
curves::create_line_segment(at(*from), at(*to)).map_err(|e| solid(&e))?;
|
||||
out.push(Curve::from(l));
|
||||
}
|
||||
}
|
||||
Seg::Arc {
|
||||
center,
|
||||
radius,
|
||||
start_deg,
|
||||
end_deg,
|
||||
} => {
|
||||
let a = curves::create_arc(at(*center), *radius, *start_deg, *end_deg)
|
||||
.map_err(|e| solid(&e))?;
|
||||
out.push(Curve::from(a));
|
||||
}
|
||||
Seg::Circle { center, radius } => {
|
||||
let c = curves::create_analytic_circle(k, at(*center), *radius)
|
||||
.map_err(|e| solid(&e))?;
|
||||
out.push(Curve::from(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
// XY -> YZ is the turn x -> y -> z -> x: 120 degrees about (1, 1, 1); ZX is 240.
|
||||
// XZ is XY turned 90 degrees about X, so u is x and v is z.
|
||||
let turn = match plane {
|
||||
ProfilePlane::Xy => None,
|
||||
ProfilePlane::Yz => Some((Vec3::new(1.0, 1.0, 1.0), 120.0)),
|
||||
ProfilePlane::Zx => Some((Vec3::new(1.0, 1.0, 1.0), 240.0)),
|
||||
ProfilePlane::Xz => Some((Vec3::X, 90.0)),
|
||||
};
|
||||
for c in &mut out {
|
||||
if let Some((axis, deg)) = turn {
|
||||
api_rotate(
|
||||
k,
|
||||
Some(TransformObject::Curve(c)),
|
||||
Vec3::new(0.0, 0.0, 0.0),
|
||||
axis,
|
||||
deg,
|
||||
)
|
||||
.map_err(|e| solid(&e))?;
|
||||
}
|
||||
api_translate(k, Some(TransformObject::Curve(c)), p3(origin)).map_err(|e| solid(&e))?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn step_rust(k: &mut Kernel, bodies: &mut BTreeMap<String, Brep>, step: &Step) -> Result<(), Run> {
|
||||
let body = match step {
|
||||
Step::Box { origin, size, .. } => {
|
||||
@@ -635,6 +976,156 @@ fn step_rust(k: &mut Kernel, bodies: &mut BTreeMap<String, Brep>, step: &Step) -
|
||||
}
|
||||
target
|
||||
}
|
||||
Step::Extrude {
|
||||
profile,
|
||||
plane,
|
||||
origin,
|
||||
distance,
|
||||
..
|
||||
} => {
|
||||
let cs = profile_curves(k, profile, *plane, *origin)?;
|
||||
let refs: Vec<Option<&Curve>> = cs.iter().map(Some).collect();
|
||||
let normal = match plane {
|
||||
ProfilePlane::Xy => Vec3::new(0.0, 0.0, 1.0),
|
||||
ProfilePlane::Yz => Vec3::new(1.0, 0.0, 0.0),
|
||||
ProfilePlane::Zx => Vec3::new(0.0, 1.0, 0.0),
|
||||
ProfilePlane::Xz => Vec3::new(0.0, -1.0, 0.0),
|
||||
};
|
||||
sweeps::create_linear_sweep(k, &refs, normal, *distance, true).map_err(|e| solid(&e))?
|
||||
}
|
||||
Step::Revolve {
|
||||
profile,
|
||||
plane,
|
||||
origin,
|
||||
axis_point,
|
||||
axis_dir,
|
||||
angle_deg,
|
||||
..
|
||||
} => {
|
||||
let cs = profile_curves(k, profile, *plane, *origin)?;
|
||||
let refs: Vec<Option<&Curve>> = cs.iter().map(Some).collect();
|
||||
sweeps::create_rotational_sweep(
|
||||
k,
|
||||
&refs,
|
||||
p3(*axis_point),
|
||||
p3(*axis_dir),
|
||||
*angle_deg,
|
||||
true,
|
||||
)
|
||||
.map_err(|e| solid(&e))?
|
||||
}
|
||||
Step::Pipe {
|
||||
radius,
|
||||
path,
|
||||
plane,
|
||||
origin,
|
||||
..
|
||||
} => {
|
||||
let cs = profile_curves(k, path, *plane, *origin)?;
|
||||
if let [single] = cs.as_slice() {
|
||||
sweeps::create_pipe_sweep(k, *radius, Some(single), true).map_err(|e| solid(&e))?
|
||||
} else {
|
||||
// The profile: a circle at the origin turned from +Z onto the path's start
|
||||
// tangent; the sweep moves its centre to the path start.
|
||||
let (u, v) = match path.first() {
|
||||
Some(Seg::Line { from, to }) => (to[0] - from[0], to[1] - from[1]),
|
||||
Some(Seg::Arc { start_deg, .. }) => {
|
||||
let (s, c) = start_deg.to_radians().sin_cos();
|
||||
(-s, c)
|
||||
}
|
||||
_ => return Err(Run::Error("pipe path starts with no line or arc".into())),
|
||||
};
|
||||
let t = match plane {
|
||||
ProfilePlane::Xy => Vec3::new(u, v, 0.0),
|
||||
ProfilePlane::Yz => Vec3::new(0.0, u, v),
|
||||
ProfilePlane::Zx => Vec3::new(v, 0.0, u),
|
||||
ProfilePlane::Xz => Vec3::new(u, 0.0, v),
|
||||
}
|
||||
.unitize()
|
||||
.map_err(|e| solid(&e))?;
|
||||
let mut circle = Curve::from(
|
||||
curves::create_analytic_circle(k, Vec3::new(0.0, 0.0, 0.0), *radius)
|
||||
.map_err(|e| solid(&e))?,
|
||||
);
|
||||
let z = Vec3::new(0.0, 0.0, 1.0);
|
||||
let cos = t.dot(z).clamp(-1.0, 1.0);
|
||||
if cos < 1.0 {
|
||||
let axis = if cos > -1.0 {
|
||||
z.cross(t)
|
||||
} else {
|
||||
Vec3::new(1.0, 0.0, 0.0)
|
||||
};
|
||||
api_rotate(
|
||||
k,
|
||||
Some(TransformObject::Curve(&mut circle)),
|
||||
Vec3::new(0.0, 0.0, 0.0),
|
||||
axis,
|
||||
cos.acos().to_degrees(),
|
||||
)
|
||||
.map_err(|e| solid(&e))?;
|
||||
}
|
||||
let refs: Vec<&Curve> = cs.iter().collect();
|
||||
sweeps::create_sweep_along_planar_path(k, &[&circle], &refs, true, 3)
|
||||
.map_err(|e| solid(&e))?
|
||||
}
|
||||
}
|
||||
Step::Loft {
|
||||
sections, degree, ..
|
||||
} => {
|
||||
let mut counts = Vec::new();
|
||||
let mut cs = Vec::new();
|
||||
for s in sections {
|
||||
let section = profile_curves(k, &s.profile, s.plane, s.origin)?;
|
||||
counts.push(section.len());
|
||||
cs.extend(section);
|
||||
}
|
||||
let refs: Vec<Option<&Curve>> = cs.iter().map(Some).collect();
|
||||
create_skin_primitive(k, &counts, &refs, *degree, true)
|
||||
.map_err(|e| solid(&e))?
|
||||
.brep
|
||||
}
|
||||
Step::Sweep {
|
||||
profile,
|
||||
plane,
|
||||
origin,
|
||||
path,
|
||||
..
|
||||
} => {
|
||||
let cs = profile_curves(k, profile, *plane, *origin)?;
|
||||
// The path is one arc or line in the XY plane at the world origin (no plane
|
||||
// turn, no translation), so the profile's plane is normal to its start tangent.
|
||||
let path = profile_curves(k, std::slice::from_ref(path), ProfilePlane::Xy, [0.0; 3])?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Run::Error("sweep path has no curve".into()))?;
|
||||
let refs: Vec<Option<&Curve>> = cs.iter().map(Some).collect();
|
||||
let options = SweepOptions {
|
||||
cap_ends: true,
|
||||
// `SMLIB_SWEEP_FOLLOW`: the profile turns with the path's frame.
|
||||
translational_sweep: false,
|
||||
move_profile_center_to_path: false,
|
||||
orient_profile_perpendicular_to_path: false,
|
||||
move_to_profile: false,
|
||||
};
|
||||
sweeps::create_curve_sweep(k, &refs, Some(&path), None, None, Some(&options))
|
||||
.map_err(|e| solid(&e))?
|
||||
.brep
|
||||
}
|
||||
// SM_API has no mirror and no matrix transform: SmBrep::Mirror and
|
||||
// SmBrep::Transform(double[4][4]) are SMLib-level and not ported. The nearest
|
||||
// SM_API route, SmApiScaleByPt with a reflecting scale, is refused for any body
|
||||
// with a circular edge (SmEllipse::Transform takes uniform scales only), and a
|
||||
// uniform -1 scale plus a half turn leaves bodies built by booleans inside out
|
||||
// (negative volume): SmBrep::Transform skips the FindAndSetInfiniteRegion and
|
||||
// UV-trim rebuild that SmBrep::Mirror does. Neither is run as a mirror.
|
||||
Step::Mirror { .. } => {
|
||||
return Err(Run::Deferred(Deferral {
|
||||
blocked_by: "no SM_API entry point".into(),
|
||||
symbol: "mirror of a body (SmBrep::Mirror is not in SM_API; SmApiScaleByPt \
|
||||
with a reflecting scale fails or inverts boolean results)"
|
||||
.into(),
|
||||
}))
|
||||
}
|
||||
// No Rust `SmApiFilletEdges` yet: it belongs to `blend.fillet_executive_p1`.
|
||||
Step::Fillet { .. } => {
|
||||
return Err(Run::Deferred(Deferral {
|
||||
@@ -661,7 +1152,49 @@ fn step_rust(k: &mut Kernel, bodies: &mut BTreeMap<String, Brep>, step: &Step) -
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Makes the NURB of every analytic surface that the operation left without one (a
|
||||
/// boolean result's swept faces carry an `SmSurfOfExtrusion` whose base has none).
|
||||
/// Upstream's first `SmBSplineSurface::CalculateBoundingBox` makes it through the virtual
|
||||
/// `GetOrCreateGwNurbPointer`; the port's bounding box is a `const` method and returns
|
||||
/// `SM_ERR` instead, so the volume of such a body would fail here (recorded in
|
||||
/// plan/findings/query.brep--boolean-results-need-their-analytic-nurbs.md).
|
||||
fn make_missing_nurbs(k: &Kernel, b: &mut Brep) -> Result<(), SolidError> {
|
||||
for f in b.face_list().to_vec() {
|
||||
let Ok(face) = b.face_mut(f) else { continue };
|
||||
let Some(surface) = face.surface.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let missing = match surface.kind() {
|
||||
SurfaceKind::Plane(s) => s.base().gw_nurb().is_none(),
|
||||
SurfaceKind::Cone(s) => s.rev().base().gw_nurb().is_none(),
|
||||
SurfaceKind::Sphere(s) => s.base().base().gw_nurb().is_none(),
|
||||
SurfaceKind::Torus(s) => s.base().base().gw_nurb().is_none(),
|
||||
SurfaceKind::SurfOfRevolution(s) => s.base().gw_nurb().is_none(),
|
||||
SurfaceKind::SurfOfExtrusion(s) => s.base().gw_nurb().is_none(),
|
||||
SurfaceKind::BSpline(_)
|
||||
| SurfaceKind::CurveBounded(_)
|
||||
| SurfaceKind::Step(_)
|
||||
| SurfaceKind::OffsetSurface(_)
|
||||
| SurfaceKind::SrfInVolume(_) => false,
|
||||
};
|
||||
if !missing {
|
||||
continue;
|
||||
}
|
||||
match surface.kind_mut() {
|
||||
SurfaceKind::Plane(s) => s.make_nurb()?,
|
||||
SurfaceKind::Cone(s) => s.make_nurb(k)?,
|
||||
SurfaceKind::Sphere(s) => s.make_nurb(k)?,
|
||||
SurfaceKind::Torus(s) => s.make_nurb(k)?,
|
||||
SurfaceKind::SurfOfRevolution(s) => s.make_nurb(k)?,
|
||||
SurfaceKind::SurfOfExtrusion(s) => s.make_nurb(k)?,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn measure(k: &Kernel, b: &mut Brep) -> Result<Measured, SolidError> {
|
||||
make_missing_nurbs(k, b)?;
|
||||
let store = AttributeStore::default();
|
||||
let attrs = PropertyAttributes {
|
||||
store: &store,
|
||||
@@ -947,16 +1480,33 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_case_file_loads() {
|
||||
// No hard-coded total (every new case file would edit it and conflict): every
|
||||
// file in the directory is a case file, and every case of every file loads.
|
||||
let dir = cases_dir(&crate::case::repo_root());
|
||||
let mut expected = 0;
|
||||
for e in std::fs::read_dir(&dir).unwrap() {
|
||||
let p = e.unwrap().path();
|
||||
assert_eq!(
|
||||
p.extension().and_then(|x| x.to_str()),
|
||||
Some("json"),
|
||||
"{p:?}"
|
||||
);
|
||||
let v: Value = serde_json::from_str(&std::fs::read_to_string(&p).unwrap()).unwrap();
|
||||
let n = v["cases"].as_array().map_or(0, Vec::len);
|
||||
assert!(n > 0, "{p:?}: no cases");
|
||||
expected += n;
|
||||
}
|
||||
let cases = all();
|
||||
assert_eq!(
|
||||
cases.len(),
|
||||
61,
|
||||
expected,
|
||||
"{:?}",
|
||||
cases.iter().map(|c| &c.id).collect::<Vec<_>>()
|
||||
);
|
||||
for l in &cases {
|
||||
assert!(
|
||||
l.file.issue.contains("/issues/") && l.file.pr.contains("/pull/"),
|
||||
l.id.starts_with("a52-")
|
||||
|| (l.file.issue.contains("/issues/") && l.file.pr.contains("/pull/")),
|
||||
"{}",
|
||||
l.id
|
||||
);
|
||||
@@ -1188,5 +1738,38 @@ mod tests {
|
||||
)
|
||||
.is_err());
|
||||
assert!(serde_json::from_str::<Step>(r#"{"op": "chamfer", "id": "a"}"#).is_err());
|
||||
let ext = r#"{"op": "extrude", "id": "result", "distance": 1, "profile": [{"kind": "polygon", "points": [[0, 0], [1, 0], [1, 1]]}]}"#;
|
||||
assert!(validate(&case(vec![step(ext)])).is_ok());
|
||||
let flat = ext.replace("[[0, 0], [1, 0], [1, 1]]", "[[0, 0], [1, 0]]");
|
||||
assert!(validate(&case(vec![step(&flat)])).is_err());
|
||||
let empty = ext.replace(
|
||||
r#"[{"kind": "polygon", "points": [[0, 0], [1, 0], [1, 1]]}]"#,
|
||||
"[]",
|
||||
);
|
||||
assert!(validate(&case(vec![step(&empty)])).is_err());
|
||||
let mirror = r#"{"op": "mirror", "id": "result", "body": "nope", "normal": "y"}"#;
|
||||
assert!(validate(&case(vec![step(mirror)])).is_err());
|
||||
assert!(serde_json::from_str::<Step>(
|
||||
r#"{"op": "mirror", "id": "a", "body": "b", "normal": "w"}"#
|
||||
)
|
||||
.is_err());
|
||||
let pipe = r#"{"op": "pipe", "id": "result", "radius": 1, "path": [{"kind": "line", "from": [0, 0], "to": [5, 0]}]}"#;
|
||||
assert!(validate(&case(vec![step(pipe)])).is_ok());
|
||||
let no_path = pipe.replace(r#"[{"kind": "line", "from": [0, 0], "to": [5, 0]}]"#, "[]");
|
||||
assert!(validate(&case(vec![step(&no_path)])).is_err());
|
||||
let circle_path = pipe.replace(
|
||||
r#"{"kind": "line", "from": [0, 0], "to": [5, 0]}"#,
|
||||
r#"{"kind": "circle", "center": [0, 0], "radius": 5}"#,
|
||||
);
|
||||
assert!(validate(&case(vec![step(&circle_path)])).is_err());
|
||||
let loft = r#"{"op": "loft", "id": "result", "sections": [{"profile": [{"kind": "circle", "center": [0, 0], "radius": 2}]}, {"profile": [{"kind": "circle", "center": [0, 0], "radius": 1}], "origin": [0, 0, 5]}]}"#;
|
||||
assert!(validate(&case(vec![step(loft)])).is_ok());
|
||||
let Step::Loft { degree, .. } = step(loft) else {
|
||||
panic!("not a loft")
|
||||
};
|
||||
assert_eq!(degree, 1);
|
||||
let one = r#"{"op": "loft", "id": "result", "sections": [{"profile": [{"kind": "circle", "center": [0, 0], "radius": 2}]}]}"#;
|
||||
assert!(validate(&case(vec![step(one)])).is_err());
|
||||
assert!(serde_json::from_str::<Step>(&loft.replace("\"origin\"", "\"orign\"")).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+26
-16
@@ -1,5 +1,6 @@
|
||||
//! `--ssi-corpus`: scores the C++ reference, the Rust port and the `ssi` crate against
|
||||
//! jyviko's exact ground-truth surface/surface intersection corpus
|
||||
//! `--ssi-corpus`: scores the C++ reference, the Rust port, the `ssi` crate and the `ssi`
|
||||
//! crate through `solidcore-ssi` against jyviko's exact ground-truth surface/surface
|
||||
//! intersection corpus
|
||||
//! (<https://github.com/jyviko/ssi-corpus>, commit `b04c8a6`, MIT OR Apache-2.0; fetched
|
||||
//! into `corpus/ssi/` by `scripts/fetch-ssi-corpus.sh`, never committed).
|
||||
//!
|
||||
@@ -9,16 +10,20 @@
|
||||
//! ([`score`]); where the C++ and the port both answered, their outputs are also
|
||||
//! compared with the difftest rule (topology identical, values within
|
||||
//! [`crate::REL_TOL`]). The `ssi` engine ([`ssi_engine`]) is a method of its own, never
|
||||
//! compared for parity, and the only one a build without the `cpp` feature runs. This
|
||||
//! compared for parity; the `solidcore-ssi` engine ([`solidcore_ssi_engine`]) runs it
|
||||
//! from solidcore's own surfaces into the port's output form, sampled as the port's is
|
||||
//! ([`kernel`]). Those two are the engines a build without the `cpp` feature runs. This
|
||||
//! is a correctness report: it never changes a unit's difftest status.
|
||||
|
||||
pub mod case;
|
||||
#[cfg(feature = "cpp")]
|
||||
pub mod engine;
|
||||
pub mod geom;
|
||||
pub mod kernel;
|
||||
pub mod polyset;
|
||||
pub mod raw;
|
||||
pub mod score;
|
||||
pub mod solidcore_ssi_engine;
|
||||
pub mod ssi_engine;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
@@ -43,12 +48,16 @@ pub const TIMEOUT: Duration = Duration::from_secs(300);
|
||||
/// The default approximation tolerance.
|
||||
pub const DEFAULT_TOL: &str = "1e-6";
|
||||
|
||||
/// The engines, in report order: upstream SMLib (C++), the Rust port, the `ssi` crate.
|
||||
pub const ENGINES: [&str; 3] = ["cpp", "rust", "ssi"];
|
||||
/// How many engines there are.
|
||||
pub const N_ENGINES: usize = 4;
|
||||
|
||||
/// The engines, in report order: upstream SMLib (C++), the Rust port, the `ssi` crate,
|
||||
/// and the `ssi` crate through `solidcore-ssi` on solidcore's own surfaces.
|
||||
pub const ENGINES: [&str; N_ENGINES] = ["cpp", "rust", "ssi", "solidcore-ssi"];
|
||||
|
||||
/// Whether this build runs each of [`ENGINES`]: `cpp` and `rust` need the C++ kernel
|
||||
/// (feature `cpp`), `ssi` nothing.
|
||||
pub const BUILT: [bool; 3] = [cfg!(feature = "cpp"), cfg!(feature = "cpp"), true];
|
||||
/// (feature `cpp`), `ssi` and `solidcore-ssi` nothing.
|
||||
pub const BUILT: [bool; N_ENGINES] = [cfg!(feature = "cpp"), cfg!(feature = "cpp"), true, true];
|
||||
|
||||
/// Whether the C++ and the port both run, so their outputs are compared for parity.
|
||||
const PARITY: bool = BUILT[0] && BUILT[1];
|
||||
@@ -147,13 +156,13 @@ pub fn case_files(root: &Path, glob: Option<&str>) -> Result<Vec<PathBuf>, Harne
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// `--ssi-case <case.json> <tol|default> <cpp|rust|ssi> <out.json>`: one engine on one
|
||||
/// `--ssi-case <case.json> <tol|default> <cpp|rust|ssi|solidcore-ssi> <out.json>`: one engine on one
|
||||
/// case, writing the corpus runner's JSON. Exit `0` on success (including a deferred Rust
|
||||
/// run and a non-success status), `3` when a surface cannot be built, `2` otherwise.
|
||||
/// `cpp` and `rust` need the `cpp` feature.
|
||||
pub fn run_child(args: &[String]) -> ExitCode {
|
||||
let [case_path, tol, engine_name, out_path] = args else {
|
||||
eprintln!("usage: difftest --ssi-case <case.json> <tol|default> <cpp|rust|ssi> <out.json>");
|
||||
eprintln!("usage: difftest --ssi-case <case.json> <tol|default> <cpp|rust|ssi|solidcore-ssi> <out.json>");
|
||||
return ExitCode::from(2);
|
||||
};
|
||||
let tol = match parse_tol(tol) {
|
||||
@@ -176,6 +185,7 @@ pub fn run_child(args: &[String]) -> ExitCode {
|
||||
#[cfg(feature = "cpp")]
|
||||
"rust" => engine::run_rust(&case, tol),
|
||||
"ssi" => ssi_engine::run_ssi(&case, tol),
|
||||
"solidcore-ssi" => solidcore_ssi_engine::run_solidcore_ssi(&case, tol),
|
||||
other if ENGINES.contains(&other) => {
|
||||
eprintln!("difftest: engine `{other}` needs feature `cpp`");
|
||||
return ExitCode::from(2);
|
||||
@@ -240,12 +250,12 @@ fn read_raw(path: &Path) -> RunResult {
|
||||
fn run_children(
|
||||
files: &[PathBuf],
|
||||
tol: &str,
|
||||
rawdirs: &[PathBuf; 3],
|
||||
rawdirs: &[PathBuf; N_ENGINES],
|
||||
jobs: usize,
|
||||
) -> Result<Vec<[RunResult; 3]>, HarnessError> {
|
||||
) -> Result<Vec<[RunResult; N_ENGINES]>, HarnessError> {
|
||||
let exe =
|
||||
std::env::current_exe().map_err(|e| HarnessError::new(format!("current_exe: {e}")))?;
|
||||
let mut results: Vec<[RunResult; 3]> = files
|
||||
let mut results: Vec<[RunResult; N_ENGINES]> = files
|
||||
.iter()
|
||||
.map(|_| std::array::from_fn(|_| Err("not run".to_string())))
|
||||
.collect();
|
||||
@@ -374,13 +384,13 @@ fn truncate(s: &str, n: usize) -> String {
|
||||
/// timing.
|
||||
fn score_all(
|
||||
cases: &[SsiCase],
|
||||
results: &[[RunResult; 3]],
|
||||
results: &[[RunResult; N_ENGINES]],
|
||||
acc_tol: Option<f64>,
|
||||
cov_floor: f64,
|
||||
names: &BTreeMap<i64, String>,
|
||||
jobs: usize,
|
||||
) -> Vec<Vec<Option<Score>>> {
|
||||
let grade = |case: &SsiCase, res: &[RunResult; 3]| -> Vec<Option<Score>> {
|
||||
let grade = |case: &SsiCase, res: &[RunResult; N_ENGINES]| -> Vec<Option<Score>> {
|
||||
res.iter()
|
||||
.zip(BUILT)
|
||||
.map(|(r, run)| match r {
|
||||
@@ -502,7 +512,7 @@ pub fn run_corpus(root: &Path, opts: &CorpusOptions) -> ExitCode {
|
||||
let names = status_names(root);
|
||||
let acc_tol = tol.map(|t| 10.0 * t);
|
||||
let cov_floor = if tol.is_none() { 5e-3 } else { 0.0 };
|
||||
let mut counts: [BTreeMap<String, usize>; 3] = Default::default();
|
||||
let mut counts: [BTreeMap<String, usize>; N_ENGINES] = Default::default();
|
||||
let (mut compared, mut identical, mut same_verdict) = (0usize, 0usize, 0usize);
|
||||
let mut entries = Vec::new();
|
||||
let all_scores = score_all(&cases, &results, acc_tol, cov_floor, &names, opts.jobs);
|
||||
@@ -532,7 +542,7 @@ pub fn run_corpus(root: &Path, opts: &CorpusOptions) -> ExitCode {
|
||||
}
|
||||
// Rust vs C++ parity where both answered
|
||||
let mut parity = "-".to_string();
|
||||
if let [Ok(c), Ok(r), _] = res {
|
||||
if let [Ok(c), Ok(r), ..] = res {
|
||||
if r.deferred.is_none() {
|
||||
compared += 1;
|
||||
let d = compare(&c.parity_output(), &r.parity_output(), &[]);
|
||||
|
||||
@@ -8,29 +8,18 @@
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::extent2d::Extent2d;
|
||||
use solidcore::geom::tol::approx_tol3d;
|
||||
use solidcore::geom::vector2d::{Point2, Vec2};
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::nurbs::b_spline_surface::BSplineSurface;
|
||||
use solidcore::nurbs::math::CPoint;
|
||||
use solidcore::nurbs::srf_basic::Surface as NurbSurface;
|
||||
use solidcore::surface::cone::Cone;
|
||||
use solidcore::surface::plane::Plane;
|
||||
use solidcore::surface::sphere::{Sphere, SphereOptions};
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::surface::torus::{Torus, TorusOptions};
|
||||
use solidcore::SolidError;
|
||||
use solidcore_ffi::units::ssi_corpus::{self as ffi, SsiSurface, SsiSurfaceIn};
|
||||
use solidcore_ffi::FfiError;
|
||||
|
||||
use super::case::{SsiCase, SurfaceSpec};
|
||||
use super::geom::P3;
|
||||
use super::kernel::{deferred_output, rust_surface, write_intersection};
|
||||
use super::raw::{type_name, RawCurve, RawOutput, N_SAMPLES, SM_SUCCESS};
|
||||
use crate::Deferred;
|
||||
|
||||
/// `SM_ERR`.
|
||||
const SM_ERR: i64 = 1001;
|
||||
@@ -249,251 +238,6 @@ fn decode(
|
||||
})
|
||||
}
|
||||
|
||||
fn v3(p: P3) -> Vec3 {
|
||||
Vec3::new(p[0], p[1], p[2])
|
||||
}
|
||||
|
||||
fn deferred_output(case: &SsiCase, tol: Option<f64>, unit: &str, symbol: &str) -> RawOutput {
|
||||
RawOutput {
|
||||
case: case.id.clone(),
|
||||
engine: "rust".into(),
|
||||
status: 0,
|
||||
approx_tol: tol,
|
||||
deferred: Some(Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: symbol.into(),
|
||||
}),
|
||||
..RawOutput::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of making an analytic surface's NURB in place, as the C++ runner's
|
||||
/// `GetNaturalUVDomain()` does (`GetOrCreateGwNurbPointer` through a const cast): an
|
||||
/// unported step defers the case.
|
||||
fn nurb_made(r: Result<(), SolidError>, what: &str) -> Result<Result<(), Deferred>, String> {
|
||||
match r {
|
||||
Ok(()) => Ok(Ok(())),
|
||||
Err(SolidError::Unported { upstream, unit }) => Ok(Err(Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: upstream.into(),
|
||||
})),
|
||||
Err(e) => Err(format!("{what}: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The runner's `unit_cross` (same operation order): the unit vector along `a x b`.
|
||||
fn unit_cross(a: Vec3, b: Vec3) -> Vec3 {
|
||||
let c = [
|
||||
a.y * b.z - a.z * b.y,
|
||||
a.z * b.x - a.x * b.z,
|
||||
a.x * b.y - a.y * b.x,
|
||||
];
|
||||
let l = (c[0] * c[0] + c[1] * c[1] + c[2] * c[2]).sqrt();
|
||||
Vec3::new(c[0] / l, c[1] / l, c[2] / l)
|
||||
}
|
||||
|
||||
/// Builds a corpus surface with solidcore's types, or names the unit that must land first.
|
||||
fn rust_surface(k: &Kernel, spec: &SurfaceSpec) -> Result<Result<Surface, Deferred>, String> {
|
||||
match spec {
|
||||
SurfaceSpec::Plane {
|
||||
origin,
|
||||
xaxis,
|
||||
yaxis,
|
||||
u,
|
||||
v,
|
||||
} => {
|
||||
let dom = Extent2d::from_bounds(u[0], v[0], u[1], v[1]);
|
||||
let mut plane = Plane::new(
|
||||
v3(*origin),
|
||||
v3(*xaxis),
|
||||
v3(*yaxis),
|
||||
Vec2::new(1.0, 1.0),
|
||||
&dom,
|
||||
);
|
||||
// The C++ runner's `GetNaturalUVDomain()` makes the plane's NURB in place
|
||||
// (`GetOrCreateGwNurbPointer` through a const cast); the port's query works on a
|
||||
// copy, so make it here to start GlobalSurfaceIntersect from the same state
|
||||
// (`IntersectWithPlane`'s parallel branch tests for the NURB).
|
||||
match plane.make_nurb() {
|
||||
Ok(()) => {}
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(Err(Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: upstream.into(),
|
||||
}))
|
||||
}
|
||||
Err(e) => return Err(format!("plane: {e}")),
|
||||
}
|
||||
Ok(Ok(Surface::from(plane)))
|
||||
}
|
||||
SurfaceSpec::Nurbs {
|
||||
degree,
|
||||
knots_u,
|
||||
knots_v,
|
||||
nu,
|
||||
nv,
|
||||
points,
|
||||
weights,
|
||||
} => {
|
||||
if *nu == 0 || *nv == 0 || points.len() != nu * nv || weights.len() != nu * nv {
|
||||
return Err("nurbs: bad sizes".into());
|
||||
}
|
||||
if knots_u.len() != nu + degree[0] + 1 || knots_v.len() != nv + degree[1] + 1 {
|
||||
return Err("nurbs: bad knots".into());
|
||||
}
|
||||
// NLib control points are homogeneous: Cartesian = (x/w, y/w, z/w)
|
||||
let net: Vec<Vec<CPoint>> = points
|
||||
.chunks(*nv)
|
||||
.zip(weights.chunks(*nv))
|
||||
.map(|(row, w)| {
|
||||
row.iter()
|
||||
.zip(w)
|
||||
.map(|(p, &w)| CPoint::new(p[0] * w, p[1] * w, p[2] * w, w))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let sur = NurbSurface::new(net, degree[0], degree[1], knots_u.clone(), knots_v.clone())
|
||||
.map_err(|e| format!("nurbs: {e}"))?;
|
||||
Ok(Ok(Surface::from(BSplineSurface::from_nurb(sur, false))))
|
||||
}
|
||||
SurfaceSpec::Cylinder {
|
||||
origin,
|
||||
xaxis,
|
||||
yaxis,
|
||||
radius,
|
||||
angle,
|
||||
height,
|
||||
} => {
|
||||
let z = unit_cross(v3(*xaxis), v3(*yaxis));
|
||||
let o = v3(*origin) + z * height[0];
|
||||
let mut cyl = Cone::new_cylinder_bounded(
|
||||
o,
|
||||
v3(*xaxis),
|
||||
v3(*yaxis),
|
||||
*radius,
|
||||
angle[0],
|
||||
angle[1],
|
||||
height[1] - height[0],
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
Ok(nurb_made(cyl.make_nurb(k), "cylinder")?.map(|()| Surface::from(cyl)))
|
||||
}
|
||||
SurfaceSpec::Cone {
|
||||
apex,
|
||||
xaxis,
|
||||
yaxis,
|
||||
half_angle_deg,
|
||||
height,
|
||||
} => {
|
||||
// apex-based description: radius(h) = h * tan(half_angle), h in [h0, h1]
|
||||
// along z, full 360 degree rotation
|
||||
let z = unit_cross(v3(*xaxis), v3(*yaxis));
|
||||
let t = (half_angle_deg * std::f64::consts::PI / 180.0).tan();
|
||||
let o = v3(*apex) + z * height[0];
|
||||
let pl = Axis2Placement::new(o, v3(*xaxis), v3(*yaxis));
|
||||
match Cone::create_canonical(
|
||||
k,
|
||||
&pl,
|
||||
height[0] * t,
|
||||
height[1] * t,
|
||||
height[1] - height[0],
|
||||
false,
|
||||
false,
|
||||
) {
|
||||
Ok(cone) => Ok(Ok(Surface::from(cone))),
|
||||
Err(SolidError::Unported { upstream, unit }) => Ok(Err(Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: upstream.into(),
|
||||
})),
|
||||
Err(e) => Err(format!("cone: {e}")),
|
||||
}
|
||||
}
|
||||
SurfaceSpec::Sphere {
|
||||
center,
|
||||
xaxis,
|
||||
yaxis,
|
||||
radius,
|
||||
lon,
|
||||
lat,
|
||||
} => {
|
||||
let anal = Extent2d::from_bounds(lon[0], lat[0], lon[1], lat[1]);
|
||||
let mut sphere = match Sphere::new(
|
||||
k,
|
||||
v3(*center),
|
||||
v3(*xaxis),
|
||||
v3(*yaxis),
|
||||
anal,
|
||||
*radius,
|
||||
SphereOptions::default(),
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(Err(Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: upstream.into(),
|
||||
}))
|
||||
}
|
||||
Err(e) => return Err(format!("sphere: {e}")),
|
||||
};
|
||||
Ok(nurb_made(sphere.make_nurb(k), "sphere")?.map(|()| Surface::from(sphere)))
|
||||
}
|
||||
SurfaceSpec::Torus {
|
||||
center,
|
||||
xaxis,
|
||||
yaxis,
|
||||
major_radius,
|
||||
minor_radius,
|
||||
major,
|
||||
minor,
|
||||
} => {
|
||||
let anal = Extent2d::from_bounds(major[0], minor[0], major[1], minor[1]);
|
||||
let mut torus = match Torus::new(
|
||||
k,
|
||||
v3(*center),
|
||||
v3(*xaxis),
|
||||
v3(*yaxis),
|
||||
anal,
|
||||
*major_radius,
|
||||
*minor_radius,
|
||||
TorusOptions::default(),
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(Err(Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: upstream.into(),
|
||||
}))
|
||||
}
|
||||
Err(e) => return Err(format!("torus: {e}")),
|
||||
};
|
||||
Ok(nurb_made(torus.make_nurb(k), "torus")?.map(|()| Surface::from(torus)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Samples a curve as the runner's `sample_curve` (failed evaluations skipped); an
|
||||
/// unported evaluation defers the case.
|
||||
fn sample(k: &Kernel, c: &Curve, n: i32) -> Result<(Vec<Vec3>, [f64; 2]), SolidError> {
|
||||
let ivl = c.natural_interval();
|
||||
let (lo, hi) = (ivl.min(), ivl.max());
|
||||
let mut out = Vec::new();
|
||||
for i in 0..n {
|
||||
let t = lo + (hi - lo) * (f64::from(i) / f64::from(n - 1));
|
||||
match c.evaluate_point(k, t) {
|
||||
Ok(p) => out.push(p),
|
||||
Err(e @ SolidError::Unported { .. }) => return Err(e),
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Ok((out, [lo, hi]))
|
||||
}
|
||||
|
||||
fn to_p3(v: Vec3) -> P3 {
|
||||
[v.x, v.y, v.z]
|
||||
}
|
||||
|
||||
fn rust_status(e: &SolidError) -> (i64, Option<String>) {
|
||||
match e {
|
||||
SolidError::Upstream { code, .. } => (i64::from(*code), None),
|
||||
@@ -523,7 +267,7 @@ pub fn run_rust(case: &SsiCase, tol: Option<f64>) -> Result<RawOutput, EngineFai
|
||||
};
|
||||
built.push((s, d));
|
||||
}
|
||||
Err(d) => return Ok(deferred_output(case, tol, &d.unit, &d.symbol)),
|
||||
Err(d) => return Ok(deferred_output(case, tol, "rust", &d.unit, &d.symbol)),
|
||||
}
|
||||
}
|
||||
let [(s1, d1), (s2, d2)] = <[(Surface, Extent2d); 2]>::try_from(built)
|
||||
@@ -546,7 +290,7 @@ pub fn run_rust(case: &SsiCase, tol: Option<f64>) -> Result<RawOutput, EngineFai
|
||||
let x = match res {
|
||||
Ok(x) => x,
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(deferred_output(case, tol, unit, upstream))
|
||||
return Ok(deferred_output(case, tol, "rust", unit, upstream))
|
||||
}
|
||||
Err(e) => {
|
||||
let (status, name) = rust_status(&e);
|
||||
@@ -555,57 +299,8 @@ pub fn run_rust(case: &SsiCase, tol: Option<f64>) -> Result<RawOutput, EngineFai
|
||||
return Ok(out);
|
||||
}
|
||||
};
|
||||
let surfaces = [&s1, &s2];
|
||||
for (i, c3) in x.curves_3d.iter().enumerate() {
|
||||
let ty = x.curve_types.get(i).map_or(-1, |t| *t as i64);
|
||||
let deviation = x.deviations.get(i).copied().unwrap_or(f64::NAN);
|
||||
let (pts, ivl) = match sample(&k, c3, N_SAMPLES) {
|
||||
Ok(r) => r,
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(deferred_output(case, tol, unit, upstream))
|
||||
}
|
||||
Err(e) => return Err(EngineFailure::Other(e.to_string())),
|
||||
};
|
||||
let mut curve = RawCurve {
|
||||
ty: type_name(ty).to_string(),
|
||||
deviation,
|
||||
interval: Some(ivl),
|
||||
class_type: Some(c3.type_code()),
|
||||
xyz: pts.into_iter().map(to_p3).collect(),
|
||||
..RawCurve::default()
|
||||
};
|
||||
let uvs = [&x.surface1_uv_curves, &x.surface2_uv_curves];
|
||||
for (kk, (uvlist, surf)) in uvs.iter().zip(surfaces).enumerate() {
|
||||
let Some(Some(uvc)) = uvlist.get(i) else {
|
||||
continue;
|
||||
};
|
||||
let (uv, _) = match sample(&k, uvc, N_SAMPLES) {
|
||||
Ok(r) => r,
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(deferred_output(case, tol, unit, upstream))
|
||||
}
|
||||
Err(e) => return Err(EngineFailure::Other(e.to_string())),
|
||||
};
|
||||
let mut at = Vec::with_capacity(uv.len());
|
||||
for p in &uv {
|
||||
match surf.evaluate_point(&k, Point2::new(p.x, p.y)) {
|
||||
Ok(q) => at.push(to_p3(q)),
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(deferred_output(case, tol, unit, upstream))
|
||||
}
|
||||
Err(_) => at.push([f64::NAN; 3]),
|
||||
}
|
||||
}
|
||||
let uv2: Vec<[f64; 2]> = uv.iter().map(|p| [p.x, p.y]).collect();
|
||||
if kk == 0 {
|
||||
curve.uv1 = uv2;
|
||||
curve.s1_at_uv = at;
|
||||
} else {
|
||||
curve.uv2 = uv2;
|
||||
curve.s2_at_uv = at;
|
||||
}
|
||||
}
|
||||
out.curves.push(curve);
|
||||
match write_intersection(&k, &x, [&s1, &s2], &mut out)? {
|
||||
Ok(()) => Ok(out),
|
||||
Err(d) => Ok(deferred_output(case, tol, "rust", &d.unit, &d.symbol)),
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
//! What the Rust engines of `--ssi-corpus` share: a corpus surface built with
|
||||
//! solidcore's own types ([`rust_surface`]), and an intersection in the port's form
|
||||
//! written out sampled the way the C++ runner samples it ([`write_intersection`]). The
|
||||
//! port (`rust`, needs the `cpp` feature for its parity checks) and `solidcore-ssi` (the
|
||||
//! `solidcore-ssi` crate on the same surfaces, no C++) both use them.
|
||||
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::extent2d::Extent2d;
|
||||
use solidcore::geom::vector2d::{Point2, Vec2};
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::nurbs::b_spline_surface::BSplineSurface;
|
||||
use solidcore::nurbs::math::CPoint;
|
||||
use solidcore::nurbs::srf_basic::Surface as NurbSurface;
|
||||
use solidcore::surface::cone::Cone;
|
||||
use solidcore::surface::plane::Plane;
|
||||
use solidcore::surface::sphere::{Sphere, SphereOptions};
|
||||
use solidcore::surface::surface::p2::SurfaceIntersection;
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::surface::torus::{Torus, TorusOptions};
|
||||
use solidcore::SolidError;
|
||||
|
||||
use super::case::{SsiCase, SurfaceSpec};
|
||||
use super::geom::P3;
|
||||
use super::raw::{type_name, RawCurve, RawOutput, N_SAMPLES};
|
||||
use super::EngineFailure;
|
||||
use crate::Deferred;
|
||||
|
||||
fn deferred(unit: &str, upstream: &str) -> Deferred {
|
||||
Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: upstream.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn v3(p: P3) -> Vec3 {
|
||||
Vec3::new(p[0], p[1], p[2])
|
||||
}
|
||||
|
||||
/// The output of a case deferred for `engine` until `unit` is ported.
|
||||
pub(crate) fn deferred_output(
|
||||
case: &SsiCase,
|
||||
tol: Option<f64>,
|
||||
engine: &str,
|
||||
unit: &str,
|
||||
symbol: &str,
|
||||
) -> RawOutput {
|
||||
RawOutput {
|
||||
case: case.id.clone(),
|
||||
engine: engine.into(),
|
||||
status: 0,
|
||||
approx_tol: tol,
|
||||
deferred: Some(Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: symbol.into(),
|
||||
}),
|
||||
..RawOutput::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of making an analytic surface's NURB in place, as the C++ runner's
|
||||
/// `GetNaturalUVDomain()` does (`GetOrCreateGwNurbPointer` through a const cast): an
|
||||
/// unported step defers the case.
|
||||
fn nurb_made(r: Result<(), SolidError>, what: &str) -> Result<Result<(), Deferred>, String> {
|
||||
match r {
|
||||
Ok(()) => Ok(Ok(())),
|
||||
Err(SolidError::Unported { upstream, unit }) => Ok(Err(Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: upstream.into(),
|
||||
})),
|
||||
Err(e) => Err(format!("{what}: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The runner's `unit_cross` (same operation order): the unit vector along `a x b`.
|
||||
fn unit_cross(a: Vec3, b: Vec3) -> Vec3 {
|
||||
let c = [
|
||||
a.y * b.z - a.z * b.y,
|
||||
a.z * b.x - a.x * b.z,
|
||||
a.x * b.y - a.y * b.x,
|
||||
];
|
||||
let l = (c[0] * c[0] + c[1] * c[1] + c[2] * c[2]).sqrt();
|
||||
Vec3::new(c[0] / l, c[1] / l, c[2] / l)
|
||||
}
|
||||
|
||||
/// Builds a corpus surface with solidcore's types, or names the unit that must land first.
|
||||
pub(crate) fn rust_surface(
|
||||
k: &Kernel,
|
||||
spec: &SurfaceSpec,
|
||||
) -> Result<Result<Surface, Deferred>, String> {
|
||||
match spec {
|
||||
SurfaceSpec::Plane {
|
||||
origin,
|
||||
xaxis,
|
||||
yaxis,
|
||||
u,
|
||||
v,
|
||||
} => {
|
||||
let dom = Extent2d::from_bounds(u[0], v[0], u[1], v[1]);
|
||||
let mut plane = Plane::new(
|
||||
v3(*origin),
|
||||
v3(*xaxis),
|
||||
v3(*yaxis),
|
||||
Vec2::new(1.0, 1.0),
|
||||
&dom,
|
||||
);
|
||||
// The C++ runner's `GetNaturalUVDomain()` makes the plane's NURB in place
|
||||
// (`GetOrCreateGwNurbPointer` through a const cast); the port's query works on a
|
||||
// copy, so make it here to start GlobalSurfaceIntersect from the same state
|
||||
// (`IntersectWithPlane`'s parallel branch tests for the NURB).
|
||||
match plane.make_nurb() {
|
||||
Ok(()) => {}
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(Err(Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: upstream.into(),
|
||||
}))
|
||||
}
|
||||
Err(e) => return Err(format!("plane: {e}")),
|
||||
}
|
||||
Ok(Ok(Surface::from(plane)))
|
||||
}
|
||||
SurfaceSpec::Nurbs {
|
||||
degree,
|
||||
knots_u,
|
||||
knots_v,
|
||||
nu,
|
||||
nv,
|
||||
points,
|
||||
weights,
|
||||
} => {
|
||||
if *nu == 0 || *nv == 0 || points.len() != nu * nv || weights.len() != nu * nv {
|
||||
return Err("nurbs: bad sizes".into());
|
||||
}
|
||||
if knots_u.len() != nu + degree[0] + 1 || knots_v.len() != nv + degree[1] + 1 {
|
||||
return Err("nurbs: bad knots".into());
|
||||
}
|
||||
// NLib control points are homogeneous: Cartesian = (x/w, y/w, z/w)
|
||||
let net: Vec<Vec<CPoint>> = points
|
||||
.chunks(*nv)
|
||||
.zip(weights.chunks(*nv))
|
||||
.map(|(row, w)| {
|
||||
row.iter()
|
||||
.zip(w)
|
||||
.map(|(p, &w)| CPoint::new(p[0] * w, p[1] * w, p[2] * w, w))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let sur = NurbSurface::new(net, degree[0], degree[1], knots_u.clone(), knots_v.clone())
|
||||
.map_err(|e| format!("nurbs: {e}"))?;
|
||||
Ok(Ok(Surface::from(BSplineSurface::from_nurb(sur, false))))
|
||||
}
|
||||
SurfaceSpec::Cylinder {
|
||||
origin,
|
||||
xaxis,
|
||||
yaxis,
|
||||
radius,
|
||||
angle,
|
||||
height,
|
||||
} => {
|
||||
let z = unit_cross(v3(*xaxis), v3(*yaxis));
|
||||
let o = v3(*origin) + z * height[0];
|
||||
let mut cyl = Cone::new_cylinder_bounded(
|
||||
o,
|
||||
v3(*xaxis),
|
||||
v3(*yaxis),
|
||||
*radius,
|
||||
angle[0],
|
||||
angle[1],
|
||||
height[1] - height[0],
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
Ok(nurb_made(cyl.make_nurb(k), "cylinder")?.map(|()| Surface::from(cyl)))
|
||||
}
|
||||
SurfaceSpec::Cone {
|
||||
apex,
|
||||
xaxis,
|
||||
yaxis,
|
||||
half_angle_deg,
|
||||
height,
|
||||
} => {
|
||||
// apex-based description: radius(h) = h * tan(half_angle), h in [h0, h1]
|
||||
// along z, full 360 degree rotation
|
||||
let z = unit_cross(v3(*xaxis), v3(*yaxis));
|
||||
let t = (half_angle_deg * std::f64::consts::PI / 180.0).tan();
|
||||
let o = v3(*apex) + z * height[0];
|
||||
let pl = Axis2Placement::new(o, v3(*xaxis), v3(*yaxis));
|
||||
match Cone::create_canonical(
|
||||
k,
|
||||
&pl,
|
||||
height[0] * t,
|
||||
height[1] * t,
|
||||
height[1] - height[0],
|
||||
false,
|
||||
false,
|
||||
) {
|
||||
Ok(cone) => Ok(Ok(Surface::from(cone))),
|
||||
Err(SolidError::Unported { upstream, unit }) => Ok(Err(Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: upstream.into(),
|
||||
})),
|
||||
Err(e) => Err(format!("cone: {e}")),
|
||||
}
|
||||
}
|
||||
SurfaceSpec::Sphere {
|
||||
center,
|
||||
xaxis,
|
||||
yaxis,
|
||||
radius,
|
||||
lon,
|
||||
lat,
|
||||
} => {
|
||||
let anal = Extent2d::from_bounds(lon[0], lat[0], lon[1], lat[1]);
|
||||
let mut sphere = match Sphere::new(
|
||||
k,
|
||||
v3(*center),
|
||||
v3(*xaxis),
|
||||
v3(*yaxis),
|
||||
anal,
|
||||
*radius,
|
||||
SphereOptions::default(),
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(Err(Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: upstream.into(),
|
||||
}))
|
||||
}
|
||||
Err(e) => return Err(format!("sphere: {e}")),
|
||||
};
|
||||
Ok(nurb_made(sphere.make_nurb(k), "sphere")?.map(|()| Surface::from(sphere)))
|
||||
}
|
||||
SurfaceSpec::Torus {
|
||||
center,
|
||||
xaxis,
|
||||
yaxis,
|
||||
major_radius,
|
||||
minor_radius,
|
||||
major,
|
||||
minor,
|
||||
} => {
|
||||
let anal = Extent2d::from_bounds(major[0], minor[0], major[1], minor[1]);
|
||||
let mut torus = match Torus::new(
|
||||
k,
|
||||
v3(*center),
|
||||
v3(*xaxis),
|
||||
v3(*yaxis),
|
||||
anal,
|
||||
*major_radius,
|
||||
*minor_radius,
|
||||
TorusOptions::default(),
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(Err(Deferred {
|
||||
unit: unit.into(),
|
||||
symbol: upstream.into(),
|
||||
}))
|
||||
}
|
||||
Err(e) => return Err(format!("torus: {e}")),
|
||||
};
|
||||
Ok(nurb_made(torus.make_nurb(k), "torus")?.map(|()| Surface::from(torus)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Samples a curve as the runner's `sample_curve` (failed evaluations skipped); an
|
||||
/// unported evaluation defers the case.
|
||||
pub(crate) fn sample(k: &Kernel, c: &Curve, n: i32) -> Result<(Vec<Vec3>, [f64; 2]), SolidError> {
|
||||
let ivl = c.natural_interval();
|
||||
let (lo, hi) = (ivl.min(), ivl.max());
|
||||
let mut out = Vec::new();
|
||||
for i in 0..n {
|
||||
let t = lo + (hi - lo) * (f64::from(i) / f64::from(n - 1));
|
||||
match c.evaluate_point(k, t) {
|
||||
Ok(p) => out.push(p),
|
||||
Err(e @ SolidError::Unported { .. }) => return Err(e),
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Ok((out, [lo, hi]))
|
||||
}
|
||||
|
||||
pub(crate) fn to_p3(v: Vec3) -> P3 {
|
||||
[v.x, v.y, v.z]
|
||||
}
|
||||
|
||||
/// Writes the intersection `x` of `surfaces` into `out`, sampled as the C++ runner
|
||||
/// samples it: each curve at [`N_SAMPLES`] parameters over its natural interval, each
|
||||
/// UV curve the same, and each surface at its UV samples. `Ok(Err(_))` names the unit
|
||||
/// an unported evaluation on the way needs.
|
||||
pub(crate) fn write_intersection(
|
||||
k: &Kernel,
|
||||
x: &SurfaceIntersection,
|
||||
surfaces: [&Surface; 2],
|
||||
out: &mut RawOutput,
|
||||
) -> Result<Result<(), Deferred>, EngineFailure> {
|
||||
for (i, c3) in x.curves_3d.iter().enumerate() {
|
||||
let ty = x.curve_types.get(i).map_or(-1, |t| *t as i64);
|
||||
let deviation = x.deviations.get(i).copied().unwrap_or(f64::NAN);
|
||||
let (pts, ivl) = match sample(k, c3, N_SAMPLES) {
|
||||
Ok(r) => r,
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(Err(deferred(unit, upstream)))
|
||||
}
|
||||
Err(e) => return Err(EngineFailure::Other(e.to_string())),
|
||||
};
|
||||
let mut curve = RawCurve {
|
||||
ty: type_name(ty).to_string(),
|
||||
deviation,
|
||||
interval: Some(ivl),
|
||||
class_type: Some(c3.type_code()),
|
||||
xyz: pts.into_iter().map(to_p3).collect(),
|
||||
..RawCurve::default()
|
||||
};
|
||||
let uvs = [&x.surface1_uv_curves, &x.surface2_uv_curves];
|
||||
for (kk, (uvlist, surf)) in uvs.iter().zip(surfaces).enumerate() {
|
||||
let Some(Some(uvc)) = uvlist.get(i) else {
|
||||
continue;
|
||||
};
|
||||
let (uv, _) = match sample(k, uvc, N_SAMPLES) {
|
||||
Ok(r) => r,
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(Err(deferred(unit, upstream)))
|
||||
}
|
||||
Err(e) => return Err(EngineFailure::Other(e.to_string())),
|
||||
};
|
||||
let mut at = Vec::with_capacity(uv.len());
|
||||
for p in &uv {
|
||||
match surf.evaluate_point(k, Point2::new(p.x, p.y)) {
|
||||
Ok(q) => at.push(to_p3(q)),
|
||||
Err(SolidError::Unported { upstream, unit }) => {
|
||||
return Ok(Err(deferred(unit, upstream)))
|
||||
}
|
||||
Err(_) => at.push([f64::NAN; 3]),
|
||||
}
|
||||
}
|
||||
let uv2: Vec<[f64; 2]> = uv.iter().map(|p| [p.x, p.y]).collect();
|
||||
if kk == 0 {
|
||||
curve.uv1 = uv2;
|
||||
curve.s1_at_uv = at;
|
||||
} else {
|
||||
curve.uv2 = uv2;
|
||||
curve.s2_at_uv = at;
|
||||
}
|
||||
}
|
||||
out.curves.push(curve);
|
||||
}
|
||||
Ok(Ok(()))
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//! The `solidcore-ssi` engine: the case's surfaces built with solidcore's own types, as
|
||||
//! the `rust` engine builds them ([`rust_surface`]), intersected by the `solidcore-ssi`
|
||||
//! crate (the `ssi` crate behind the port's inputs and output), and written as the
|
||||
//! `rust` engine writes the port's output ([`write_intersection`]). So it grades the
|
||||
//! path a kernel caller would take, the adapters into and out of `ssi` included, by the
|
||||
//! port's own sampling: its curves are B-spline curves sampled at 4001 parameters, not
|
||||
//! the marched points the `ssi` engine writes. Its points are the singular points the
|
||||
//! crate returns beside the curves (the `ssi` engine writes the same). The timer covers
|
||||
//! the whole call (patches in, `ssi`, filling in, curves out). No C++ is involved.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use solidcore::geom::extent2d::Extent2d;
|
||||
use solidcore::geom::tol::approx_tol3d;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::SolidError;
|
||||
use solidcore_ssi::Error;
|
||||
|
||||
use super::case::SsiCase;
|
||||
use super::kernel::{deferred_output, rust_surface, write_intersection};
|
||||
use super::raw::{RawOutput, SM_SUCCESS};
|
||||
use super::EngineFailure;
|
||||
|
||||
/// The engine's name in reports.
|
||||
pub const ENGINE: &str = "solidcore-ssi";
|
||||
|
||||
/// `SM_ERR`: the status of a refusal or failure, whose code and detail are in
|
||||
/// `status_name`.
|
||||
const SM_ERR: i64 = 1001;
|
||||
|
||||
/// The `solidcore-ssi` crate's intersection of the case's two surfaces. A refusal (a
|
||||
/// contact `ssi` has not built, `BOOLEAN_TANGENT`), a surface it does not take, or a
|
||||
/// failure is status `SM_ERR` with its code and detail as `status_name`; an unported
|
||||
/// step on the way defers the case, as for the port.
|
||||
pub fn run_solidcore_ssi(case: &SsiCase, tol: Option<f64>) -> Result<RawOutput, EngineFailure> {
|
||||
let k = Kernel::default();
|
||||
let [(a, da), (b, db)] = &case.surfaces;
|
||||
let mut built = Vec::new();
|
||||
for (spec, dom) in [(a, da), (b, db)] {
|
||||
match rust_surface(&k, spec).map_err(EngineFailure::Build)? {
|
||||
Ok(s) => {
|
||||
let d = match dom {
|
||||
Some(d) => Extent2d::from_bounds(d[0], d[2], d[1], d[3]),
|
||||
None => s.natural_uv_domain(),
|
||||
};
|
||||
built.push((s, d));
|
||||
}
|
||||
Err(d) => return Ok(deferred_output(case, tol, ENGINE, &d.unit, &d.symbol)),
|
||||
}
|
||||
}
|
||||
let [(s1, d1), (s2, d2)] = <[(Surface, Extent2d); 2]>::try_from(built)
|
||||
.map_err(|_| EngineFailure::Other("two surfaces expected".into()))?;
|
||||
let dom4 = |d: &Extent2d| [d.min().x, d.max().x, d.min().y, d.max().y];
|
||||
let approx = tol.map(|t| approx_tol3d(&k, t));
|
||||
let t0 = Instant::now();
|
||||
let res = solidcore_ssi::global_surface_intersect(&k, &s1, &d1, &s2, &d2, approx, None);
|
||||
let time_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||
let mut out = RawOutput {
|
||||
case: case.id.clone(),
|
||||
engine: ENGINE.into(),
|
||||
status: SM_SUCCESS,
|
||||
domain1: dom4(&d1),
|
||||
domain2: dom4(&d2),
|
||||
approx_tol: tol,
|
||||
time_ms,
|
||||
..RawOutput::default()
|
||||
};
|
||||
let x = match res {
|
||||
Ok(x) => x,
|
||||
Err(Error::Kernel(SolidError::Unported { upstream, unit })) => {
|
||||
return Ok(deferred_output(case, tol, ENGINE, unit, upstream))
|
||||
}
|
||||
Err(e) => {
|
||||
out.status = SM_ERR;
|
||||
out.status_name = Some(match &e {
|
||||
Error::Ssi(_) => e.to_string(),
|
||||
Error::Unsupported(_) => format!("UNSUPPORTED ({e})"),
|
||||
Error::Kernel(_) => format!("SM_ERR ({e})"),
|
||||
});
|
||||
return Ok(out);
|
||||
}
|
||||
};
|
||||
// the singular points on the curves, as the `ssi` engine writes them
|
||||
out.points = x.vertices.iter().map(|v| [v.x, v.y, v.z]).collect();
|
||||
match write_intersection(&k, &x.curves, [&s1, &s2], &mut out)? {
|
||||
Ok(()) => Ok(out),
|
||||
Err(d) => Ok(deferred_output(case, tol, ENGINE, &d.unit, &d.symbol)),
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2065,6 +2065,11 @@ fn corner_rust(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
corner_rec(&fc, &rm, &mut rec)?;
|
||||
if !insert {
|
||||
super::blend_fillet_corner_p3::put_fillet_brep(&mut rec, &new_brep)?;
|
||||
super::blend_fillet_corner_p3::put_error_info(
|
||||
&mut rec,
|
||||
&exec.error,
|
||||
fc.filleted_vertex(),
|
||||
);
|
||||
}
|
||||
squeeze_output(&rec)
|
||||
}
|
||||
|
||||
@@ -46,10 +46,13 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use solidcore::blend::fillet_corner::p3::{
|
||||
CornerEnv, CornerFilletBrep, FilletCorner, FilletCornerKind, FilletExecutiveData,
|
||||
CornerEnv, CornerFilletBrep, ExecutiveErrorObject, FilletCorner, FilletCornerKind,
|
||||
FilletExecutiveData,
|
||||
};
|
||||
use solidcore::blend::fillet_custom_solver::{OffsetSurface, SurfaceId};
|
||||
use solidcore::blend::fillet_executive::p3::ErrorInfo;
|
||||
use solidcore::blend::fillet_geom::p1::{FilletCornerId, FilletCornerType, FilletSolverId};
|
||||
use solidcore::blend::fillet_solver::p1::{FilletSolver, FilletSurfaceGenerator};
|
||||
use solidcore::blend::fillet_solver::p1::{FilletSolver, FilletSurfaceGenerator, OffsetSurfaceSlot};
|
||||
use solidcore::intersect::surface_intersector::p1::SrfIndex;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::math::EFF_ZERO_RAD;
|
||||
@@ -189,6 +192,50 @@ impl CornerOp {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct BigRadCornerIn {
|
||||
kind: usize,
|
||||
v_edge: i64,
|
||||
#[serde(default)]
|
||||
v_sub: usize,
|
||||
#[serde(default)]
|
||||
adjusted: bool,
|
||||
tol: f64,
|
||||
tan: f64,
|
||||
#[serde(default)]
|
||||
edges: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct BigRadRailsIn {
|
||||
f0: usize,
|
||||
eu0: usize,
|
||||
d0: f64,
|
||||
f1: usize,
|
||||
eu1: usize,
|
||||
d1: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct BigRadSolverIn {
|
||||
tol: f64,
|
||||
ang: f64,
|
||||
tang: f64,
|
||||
#[serde(default)]
|
||||
rails: Option<BigRadRailsIn>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct BigRadIn {
|
||||
model: Model,
|
||||
fg: usize,
|
||||
corners: Vec<BigRadCornerIn>,
|
||||
fillet_corners: Vec<usize>,
|
||||
solvers: Vec<BigRadSolverIn>,
|
||||
geom_solvers: Vec<i64>,
|
||||
fillet_solvers: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "case", rename_all = "snake_case")]
|
||||
enum Inputs {
|
||||
@@ -212,6 +259,7 @@ enum Inputs {
|
||||
model: Model,
|
||||
corner: CornerIn,
|
||||
},
|
||||
BigRad(BigRadIn),
|
||||
}
|
||||
|
||||
const KINDS: [FilletCornerKind; 11] = [
|
||||
@@ -853,8 +901,8 @@ pub(super) fn corner_rust_status(
|
||||
let faces = rm.target_faces.clone();
|
||||
// m_pFilletBrep: `new (ctx) SmBrep()` (its constructor makes the infinite region)
|
||||
let mut fillet_brep = Brep::new_with_infinite_region().map_err(h)?;
|
||||
let mut exec = FilletExecutiveData::default();
|
||||
let r = {
|
||||
let mut exec = FilletExecutiveData::default();
|
||||
let mut records = BTreeMap::new();
|
||||
let mut attributes = solidcore::geom::attribute::AttributeStore::default();
|
||||
let mut env = CornerEnv {
|
||||
@@ -900,6 +948,7 @@ pub(super) fn corner_rust_status(
|
||||
rm.put(&mut rec)?;
|
||||
put_corner_surfaces(&mut rec, &c.surfaces)?;
|
||||
put_fillet_brep(&mut rec, &fillet_brep)?;
|
||||
put_error_info(&mut rec, &exec.error, c.filleted_vertex());
|
||||
corner_output(&rec)
|
||||
}
|
||||
|
||||
@@ -988,6 +1037,532 @@ pub(super) fn put_fillet_brep(o: &mut Vec<f64>, b: &Brep) -> Result<(), HarnessE
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Executive error info as `p3_put_error_info` writes it.
|
||||
pub(super) fn put_error_info(
|
||||
o: &mut Vec<f64>,
|
||||
info: &ErrorInfo<ExecutiveErrorObject>,
|
||||
corner_vert: Option<VertexId>,
|
||||
) {
|
||||
o.push(f64::from(info.code));
|
||||
o.push(info.corner_type as i64 as f64);
|
||||
let vtag = match info.vertex {
|
||||
None => -1.0,
|
||||
Some(ExecutiveErrorObject::TargetVertex(v)) if Some(v) == corner_vert => 1.0,
|
||||
Some(_) => 0.0,
|
||||
};
|
||||
o.push(vtag);
|
||||
let nb = info.entity_name.as_bytes();
|
||||
o.push(nb.len() as f64);
|
||||
for &b in nb {
|
||||
o.push(f64::from(b));
|
||||
}
|
||||
let cb = info.comment.as_bytes();
|
||||
o.push(cb.len() as f64);
|
||||
for &b in cb {
|
||||
o.push(f64::from(b));
|
||||
}
|
||||
}
|
||||
|
||||
fn big_rad_input(b: &BigRadIn) -> Result<Vec<f64>, HarnessError> {
|
||||
let mut o = Vec::new();
|
||||
push_model(&mut o, &b.model)?;
|
||||
o.push(b.fg as f64);
|
||||
o.push(b.corners.len() as f64);
|
||||
for c in &b.corners {
|
||||
o.extend([
|
||||
c.kind as f64,
|
||||
c.v_edge as f64,
|
||||
c.v_sub as f64,
|
||||
flag(c.adjusted),
|
||||
c.tol,
|
||||
c.tan,
|
||||
c.edges.len() as f64,
|
||||
]);
|
||||
for &e in &c.edges {
|
||||
o.push(e as f64);
|
||||
}
|
||||
}
|
||||
o.push(b.fillet_corners.len() as f64);
|
||||
for &ci in &b.fillet_corners {
|
||||
o.push(ci as f64);
|
||||
}
|
||||
o.push(b.solvers.len() as f64);
|
||||
for s in &b.solvers {
|
||||
o.extend([s.tol, s.ang, s.tang, flag(s.rails.is_some())]);
|
||||
if let Some(r) = &s.rails {
|
||||
o.extend([
|
||||
r.f0 as f64,
|
||||
r.eu0 as f64,
|
||||
r.d0,
|
||||
r.f1 as f64,
|
||||
r.eu1 as f64,
|
||||
r.d1,
|
||||
]);
|
||||
}
|
||||
o.push(0.0); // ngo = 0 so M.geoms matches rm.geoms
|
||||
}
|
||||
for &si in &b.geom_solvers {
|
||||
o.push(si as f64);
|
||||
}
|
||||
o.push(b.fillet_solvers.len() as f64);
|
||||
for &si in &b.fillet_solvers {
|
||||
o.push(si as f64);
|
||||
}
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
fn big_rad_output(rec: &[f64]) -> Result<Output, HarnessError> {
|
||||
let first = rec
|
||||
.first()
|
||||
.copied()
|
||||
.ok_or_else(|| harness("empty big_rad record"))?;
|
||||
if first == REFUSED {
|
||||
return Ok(Output::rejected("upstream would dereference null"));
|
||||
}
|
||||
let converted = rec
|
||||
.get(1)
|
||||
.copied()
|
||||
.ok_or_else(|| harness("short big_rad record"))?;
|
||||
let tail = rec.get(2..).unwrap_or_default().to_vec();
|
||||
Ok(Output::new()
|
||||
.int("status", first as i64)
|
||||
.int("converted", converted as i64)
|
||||
.values("state", tail))
|
||||
}
|
||||
|
||||
fn big_rad_cpp(b: &BigRadIn) -> Result<Output, HarnessError> {
|
||||
let input = big_rad_input(b)?;
|
||||
let flat = ffi::corner(Op::BigRad, &input).map_err(|e| harness(e.to_string()))?;
|
||||
big_rad_output(&flat)
|
||||
}
|
||||
|
||||
fn big_rad_rust(k: &Kernel, b: &BigRadIn) -> Result<Output, HarnessError> {
|
||||
ffi::validate_corner_op(Op::BigRad, &big_rad_input(b)?)
|
||||
.map_err(|e| harness(e.to_string()))?;
|
||||
let h = |e: SolidError| harness(e.to_string());
|
||||
let mut rm = RustModel::build(&b.model)?;
|
||||
let fg = rm.geom(b.fg)?;
|
||||
let mut exec = FilletExecutiveData::default();
|
||||
for (ci, c) in b.corners.iter().enumerate() {
|
||||
let kind = *KINDS.get(c.kind).ok_or_else(|| harness("kind"))?;
|
||||
let vert = match usize::try_from(c.v_edge) {
|
||||
Ok(e) => {
|
||||
let edge = *rm
|
||||
.target_edges
|
||||
.get(e)
|
||||
.ok_or_else(|| harness("target edge"))?;
|
||||
let [vs, ve] = rm.target.edge(edge).map_err(h)?.vertices;
|
||||
Some(if c.v_sub == 0 { vs } else { ve })
|
||||
}
|
||||
Err(_) => None,
|
||||
};
|
||||
let mut fc = FilletCorner::new(kind, vert, None, None, c.tol, c.tan);
|
||||
fc.topology_adjusted = c.adjusted;
|
||||
for &ei in &c.edges {
|
||||
fc.edges.push(rm.edge(ei)?);
|
||||
}
|
||||
exec.corners.insert(FilletCornerId(ci as u32), fc);
|
||||
}
|
||||
for &ci in &b.fillet_corners {
|
||||
exec.fillet_corners.push(FilletCornerId(ci as u32));
|
||||
}
|
||||
let mut solvers = Vec::new();
|
||||
for (si, s) in b.solvers.iter().enumerate() {
|
||||
let mut fs = FilletSolver::new(s.tol, s.ang, s.tang);
|
||||
if let Some(r) = &s.rails {
|
||||
let face_eu = |rm: &RustModel, fi: usize, eui: usize| -> Result<(FaceId, EdgeuseId), HarnessError> {
|
||||
let f = *rm.target_faces.get(fi).ok_or_else(|| harness("face"))?;
|
||||
let loops = solidcore::topology::face::p2::loops(&rm.target, f, false, None).map_err(h)?;
|
||||
let l = *loops.first().ok_or_else(|| harness("loop"))?;
|
||||
let eus = solidcore::topology::loopuse::loop_edgeuses(&rm.target, l).map_err(h)?;
|
||||
let eu = *eus.get(eui).ok_or_else(|| harness("eu"))?;
|
||||
Ok((f, eu))
|
||||
};
|
||||
let (f0, eu0) = face_eu(&rm, r.f0, r.eu0)?;
|
||||
let (f1, eu1_id) = face_eu(&rm, r.f1, r.eu1)?;
|
||||
let s0_base = rm.target.face(f0).map_err(h)?.surface.clone().ok_or_else(|| harness("s0"))?;
|
||||
let s1_base = rm.target.face(f1).map_err(h)?.surface.clone().ok_or_else(|| harness("s1"))?;
|
||||
let mut os0 = OffsetSurface::new(
|
||||
&rm.k,
|
||||
r.d0,
|
||||
s0_base,
|
||||
SurfaceId::Face(f0),
|
||||
solidcore::surface::offset_surface::SS_UNKNOWN,
|
||||
)
|
||||
.map_err(h)?;
|
||||
let mut os1 = OffsetSurface::new(
|
||||
&rm.k,
|
||||
r.d1,
|
||||
s1_base,
|
||||
SurfaceId::Face(f1),
|
||||
solidcore::surface::offset_surface::SS_UNKNOWN,
|
||||
)
|
||||
.map_err(h)?;
|
||||
os0.set_allow_extension(true);
|
||||
os1.set_allow_extension(true);
|
||||
fs.edgeuses = [Some(eu0), Some(eu1_id)];
|
||||
fs.set_surface(
|
||||
0,
|
||||
Some(OffsetSurfaceSlot {
|
||||
key: (si * 2 + 1) as u64,
|
||||
surface: os0,
|
||||
}),
|
||||
)
|
||||
.map_err(h)?;
|
||||
fs.set_surface(
|
||||
1,
|
||||
Some(OffsetSurfaceSlot {
|
||||
key: (si * 2 + 2) as u64,
|
||||
surface: os1,
|
||||
}),
|
||||
)
|
||||
.map_err(h)?;
|
||||
}
|
||||
solvers.push(fs);
|
||||
}
|
||||
for (gi, &si) in b.geom_solvers.iter().enumerate() {
|
||||
let gid = rm.geom(gi)?;
|
||||
rm.fb.geom_mut(gid).map_err(h)?.solver =
|
||||
usize::try_from(si).ok().map(|i| FilletSolverId(i as u32));
|
||||
}
|
||||
for &si in &b.fillet_solvers {
|
||||
exec.fillet_solvers.push(FilletSolverId(si as u32));
|
||||
}
|
||||
|
||||
let res = exec.test_and_convert_big_rad_fillet(
|
||||
k,
|
||||
&mut rm.target,
|
||||
&mut rm.fb,
|
||||
&mut solvers,
|
||||
fg,
|
||||
);
|
||||
let (st, converted) = match res {
|
||||
Ok(conv) => (0.0, conv),
|
||||
Err(e) if e.is_unported() => {
|
||||
return Ok(Output::unported(&e).unwrap_or_else(|| Output::err(e.to_string())))
|
||||
}
|
||||
Err(e) => (status(&e), false),
|
||||
};
|
||||
let mut rec = vec![st, flag(converted)];
|
||||
if st == 0.0 {
|
||||
rec.push(exec.fillet_corners.len() as f64);
|
||||
for &cid in &exec.fillet_corners {
|
||||
rec.push(f64::from(cid.0));
|
||||
}
|
||||
rec.push(exec.fillet_solvers.len() as f64);
|
||||
for &sid in &exec.fillet_solvers {
|
||||
rec.push(f64::from(sid.0));
|
||||
}
|
||||
if converted {
|
||||
let new_id = FilletCornerId(b.corners.len() as u32);
|
||||
let nc = exec.corners.get(&new_id).ok_or_else(|| harness("new corner"))?;
|
||||
rec.push(nc.corner_type() as i64 as f64);
|
||||
rec.push(nc.vertices.len() as f64);
|
||||
for &v in &nc.vertices {
|
||||
rec.push(rm.vertex_index(Some(v)));
|
||||
}
|
||||
rec.push(nc.edges.len() as f64);
|
||||
for &e in &nc.edges {
|
||||
rec.push(rm.edge_index(Some(e)));
|
||||
}
|
||||
rec.push(nc.approx_tol_3d);
|
||||
rec.push(nc.tangency_tol_radians);
|
||||
rec.push((rm.target_edges.len() * 2) as f64);
|
||||
for &te in &rm.target_edges {
|
||||
let [vs, ve] = rm.target.edge(te).map_err(h)?.vertices;
|
||||
for tv in [vs, ve] {
|
||||
rec.push(
|
||||
exec.vertex_corner_map
|
||||
.get(&tv)
|
||||
.map_or(-1.0, |c| f64::from(c.0)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
rm.put(&mut rec)?;
|
||||
}
|
||||
big_rad_output(&rec)
|
||||
}
|
||||
|
||||
/// Curated `TestAndConvertBigRadFillet` cases exercising full corner conversion
|
||||
/// (`squeeze = 0` & `1`, converging `CalcRailIntRail` on 3 orthogonal planes, edge
|
||||
/// reversal, corner-edge relation updates, index swap in `m_vFilletCorners`) as well as
|
||||
/// non-converting early-outs and error statuses.
|
||||
fn big_rad_cases() -> Result<Vec<Case>, HarnessError> {
|
||||
let mk_vert = |point: [f64; 3],
|
||||
vtype: i32,
|
||||
status: i32,
|
||||
corner: i64,
|
||||
face: i64,
|
||||
mates: [i64; 2]| VertexIn {
|
||||
point,
|
||||
vtype: i64::from(vtype),
|
||||
status: i64::from(status),
|
||||
corner,
|
||||
pc_class: if face >= 0 { 102 } else { 100 },
|
||||
pc_kind: if face >= 0 { 3 } else { 0 },
|
||||
pc_index: face,
|
||||
pc_sub: 0,
|
||||
pc_face: face,
|
||||
t_param: 0.0,
|
||||
uv: [0.2, 0.2],
|
||||
mates,
|
||||
};
|
||||
let mk_edge = |v0: usize, v1: usize, etype: i32, corner: i64, g0: i64, g1: i64| EdgeIn {
|
||||
vs: v0,
|
||||
ve: v1,
|
||||
etype: i64::from(etype),
|
||||
status: 1, // SM_FIL_PROCESSED
|
||||
corner,
|
||||
curve: Some(line([0.0, 0.0, 0.0], [1.0, 0.0, 0.0])),
|
||||
interval: Some([0.0, 1.0]),
|
||||
fbe: [-1, -1],
|
||||
eus: [
|
||||
EuIn {
|
||||
uv_curve: None,
|
||||
geom: g0,
|
||||
vu_uv: [0.2, 0.2],
|
||||
},
|
||||
EuIn {
|
||||
uv_curve: None,
|
||||
geom: g1,
|
||||
vu_uv: [0.2, 0.2],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Base converting setup:
|
||||
// Target faces: f0 = XY plane z=0, f1 = XZ plane y=0, f2 = YZ plane x=0.
|
||||
// fg = geom 0 between corner 0 (Nx2, target vert (0,0)) and corner 1 (Nx2, target vert (0,1)).
|
||||
// Rail 0: v0 (1, 0.2, 0) -> v1 (0, 0.2, 0) (dir = (-1, 0, 0)), MATE vertices on f0 (z=0), mates v4 & v5.
|
||||
// Rail 1: v2 (0.2, 0, 0.2) -> v3 (0.8, 0, 0.2) (dir = (+0.6, 0, 0), dot < 0!).
|
||||
// Extra rails into v0 (geom 1, solver 1 on f0 & f1) and v1 (geom 2, solver 2 on f0 & f2) so that
|
||||
// after squeezing rail 0 (moving v1's uses onto v0), v0 has geoms [1, 2] and CalcRailIntRail
|
||||
// converges on f0 at (0.2, 0.2, 0.0)!
|
||||
let base_case = |swap_mates_to_rail1: bool, reverse_cs: bool, swap_fc_order: bool| -> BigRadIn {
|
||||
let (vt0, vt1) = if swap_mates_to_rail1 {
|
||||
(4, 10) // RailXRail on rail 0, Mate on rail 1 -> squeeze = 1!
|
||||
} else {
|
||||
(10, 4) // Mate on rail 0, RailXRail on rail 1 -> squeeze = 0!
|
||||
};
|
||||
let mut vertices = vec![
|
||||
mk_vert([1.0, 0.2, 0.0], vt0, 1, 0, 0, [4, -1]), // v0: start of rail 0 (corner 0)
|
||||
mk_vert([0.0, 0.2, 0.0], vt0, 1, 1, 0, [-1, -1]), // v1: end of rail 0 (corner 1)
|
||||
mk_vert([0.2, 0.0, 0.2], vt1, 1, 0, 0, [4, -1]), // v2: start of rail 1 (corner 0)
|
||||
mk_vert([0.8, 0.0, 0.2], vt1, 1, 1, 0, [-1, -1]), // v3: end of rail 1 (corner 1)
|
||||
mk_vert([0.2, 0.0, 0.3], 10, 1, 0, 1, [0, 2]), // v4: mate on f1
|
||||
mk_vert([0.0, 0.3, 0.2], 10, 1, 0, 2, [-1, -1]), // v5: extra vertex for rail of geom 1/2
|
||||
];
|
||||
if swap_mates_to_rail1 {
|
||||
if let Some(v0) = vertices.get_mut(0) {
|
||||
v0.mates = [-1, -1];
|
||||
}
|
||||
if let Some(v2) = vertices.get_mut(2) {
|
||||
v2.mates = [4, -1];
|
||||
}
|
||||
}
|
||||
// Edges:
|
||||
// e0: rail 0 of geom 0 (v0 -> v1)
|
||||
// e1: rail 1 of geom 0 (v2 -> v3)
|
||||
// e2: cross-section of corner 1 (end corner): connects v3 & v1
|
||||
// e3: cross-section of corner 0 (start corner): connects v0 & v2
|
||||
// e4: rail of geom 1 connected to squeezed start vertex (v0 or v2) -> v5
|
||||
// e5: rail of geom 2 connected to squeezed end vertex (v1 or v3) -> v5
|
||||
let sq_sv = if swap_mates_to_rail1 { 2 } else { 0 };
|
||||
let sq_ev = if swap_mates_to_rail1 { 3 } else { 1 };
|
||||
let ot_ev = if swap_mates_to_rail1 { 1 } else { 3 };
|
||||
let ot_sv = if swap_mates_to_rail1 { 0 } else { 2 };
|
||||
let (cs_end_a, cs_end_b) = if reverse_cs {
|
||||
(sq_ev, ot_ev) // GetVertex() != ot_ev -> triggers SM_REVERSE_FILLET_EDGE!
|
||||
} else {
|
||||
(ot_ev, sq_ev)
|
||||
};
|
||||
let (cs_start_a, cs_start_b) = if reverse_cs {
|
||||
(ot_sv, sq_sv) // GetVertex() != sq_sv -> triggers SM_REVERSE_FILLET_EDGE!
|
||||
} else {
|
||||
(sq_sv, ot_sv)
|
||||
};
|
||||
let edges = vec![
|
||||
mk_edge(0, 1, 9, -1, 0, -1), // e0: SM_FE_RAIL (9) of geom 0
|
||||
mk_edge(2, 3, 9, -1, 0, -1), // e1: SM_FE_RAIL (9) of geom 0
|
||||
mk_edge(cs_end_a, cs_end_b, 1, 1, 0, 2), // e2: SM_FE_CROSS_SECTION (1) of corner 1 (prim has fg=0 -> modified to mate's fg=2!)
|
||||
mk_edge(cs_start_a, cs_start_b, 1, 0, 1, 0), // e3: SM_FE_CROSS_SECTION (1) of corner 0 (prim has fg=1, mate cleared!)
|
||||
mk_edge(sq_sv, 5, 9, -1, 1, -1), // e4: SM_FE_RAIL of geom 1
|
||||
mk_edge(sq_ev, 5, 9, -1, 2, -1), // e5: SM_FE_RAIL of geom 2
|
||||
];
|
||||
let model = Model {
|
||||
targets: vec![line([0.0, 0.0, 0.0], [1.0, 0.0, 0.0])],
|
||||
faces: vec![
|
||||
plane_srf([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]), // f0: z=0 (normal +Z)
|
||||
plane_srf([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]), // f1: y=0 (normal -Y)
|
||||
plane_srf([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]), // f2: x=0 (normal +X)
|
||||
],
|
||||
fillet_brep: Vec::new(),
|
||||
vertices,
|
||||
edges,
|
||||
geoms: vec![
|
||||
GeomIn {
|
||||
gtype: 1,
|
||||
status: 0,
|
||||
rails: [0, 1],
|
||||
other1: Vec::new(),
|
||||
other2: Vec::new(),
|
||||
center: None,
|
||||
surface: None,
|
||||
sides: Vec::new(),
|
||||
},
|
||||
GeomIn {
|
||||
gtype: 1,
|
||||
status: 0,
|
||||
rails: [4, -1],
|
||||
other1: Vec::new(),
|
||||
other2: Vec::new(),
|
||||
center: None,
|
||||
surface: None,
|
||||
sides: Vec::new(),
|
||||
},
|
||||
GeomIn {
|
||||
gtype: 1,
|
||||
status: 0,
|
||||
rails: [5, -1],
|
||||
other1: Vec::new(),
|
||||
other2: Vec::new(),
|
||||
center: None,
|
||||
surface: None,
|
||||
sides: Vec::new(),
|
||||
},
|
||||
],
|
||||
};
|
||||
BigRadIn {
|
||||
model,
|
||||
fg: 0,
|
||||
corners: vec![
|
||||
BigRadCornerIn {
|
||||
kind: 7, // NByTwo
|
||||
v_edge: 0,
|
||||
v_sub: 0,
|
||||
adjusted: false,
|
||||
tol: 1.0e-5,
|
||||
tan: 1.0e-2,
|
||||
edges: vec![3],
|
||||
},
|
||||
BigRadCornerIn {
|
||||
kind: 7, // NByTwo
|
||||
v_edge: 0,
|
||||
v_sub: 1,
|
||||
adjusted: false,
|
||||
tol: 1.0e-5,
|
||||
tan: 1.0e-2,
|
||||
edges: vec![2],
|
||||
},
|
||||
],
|
||||
fillet_corners: if swap_fc_order { vec![1, 0] } else { vec![0, 1] },
|
||||
solvers: vec![
|
||||
BigRadSolverIn {
|
||||
tol: 1.0e-5,
|
||||
ang: 1.0e-3,
|
||||
tang: 1.0e-2,
|
||||
rails: None,
|
||||
},
|
||||
BigRadSolverIn {
|
||||
tol: 1.0e-5,
|
||||
ang: 1.0e-3,
|
||||
tang: 1.0e-2,
|
||||
rails: Some(BigRadRailsIn {
|
||||
f0: 0,
|
||||
eu0: 0,
|
||||
d0: 0.2,
|
||||
f1: 1,
|
||||
eu1: 0,
|
||||
d1: -0.2,
|
||||
}),
|
||||
},
|
||||
BigRadSolverIn {
|
||||
tol: 1.0e-5,
|
||||
ang: 1.0e-3,
|
||||
tang: 1.0e-2,
|
||||
rails: Some(BigRadRailsIn {
|
||||
f0: 0,
|
||||
eu0: 3,
|
||||
d0: 0.2,
|
||||
f1: 2,
|
||||
eu1: 0,
|
||||
d1: 0.2,
|
||||
}),
|
||||
},
|
||||
],
|
||||
geom_solvers: vec![0, 1, 2],
|
||||
fillet_solvers: vec![0, 1, 2],
|
||||
}
|
||||
};
|
||||
|
||||
let mut raw: Vec<(&str, BigRadIn)> = vec![
|
||||
("big_rad_convert_squeeze0", base_case(false, false, false)),
|
||||
("big_rad_convert_squeeze0_rev_swap", base_case(false, true, true)),
|
||||
("big_rad_convert_squeeze1", base_case(true, true, false)),
|
||||
];
|
||||
|
||||
// Non-converting: topology_adjusted == true
|
||||
{
|
||||
let mut c = base_case(false, false, false);
|
||||
if let Some(c0) = c.corners.get_mut(0) {
|
||||
c0.adjusted = true;
|
||||
}
|
||||
raw.push(("big_rad_skip_adjusted", c));
|
||||
}
|
||||
// Non-converting: same rail direction (dot > 0)
|
||||
{
|
||||
let mut c = base_case(false, false, false);
|
||||
if let Some(v2) = c.model.vertices.get_mut(2) {
|
||||
v2.point = [0.8, 0.0, 0.2];
|
||||
}
|
||||
if let Some(v3) = c.model.vertices.get_mut(3) {
|
||||
v3.point = [0.2, 0.0, 0.2];
|
||||
}
|
||||
raw.push(("big_rad_skip_same_dir", c));
|
||||
}
|
||||
// Non-converting: non-Nx2 corner type
|
||||
{
|
||||
let mut c = base_case(false, false, false);
|
||||
if let Some(c1) = c.corners.get_mut(1) {
|
||||
c1.kind = 9; // ConvexNByN
|
||||
}
|
||||
raw.push(("big_rad_skip_corner_type", c));
|
||||
}
|
||||
// Error: neither rail has two SM_FV_MATE vertices -> SM_ERR
|
||||
{
|
||||
let mut c = base_case(false, false, false);
|
||||
if let Some(v0) = c.model.vertices.get_mut(0) {
|
||||
v0.vtype = 4;
|
||||
}
|
||||
if let Some(v2) = c.model.vertices.get_mut(2) {
|
||||
v2.vtype = 4;
|
||||
}
|
||||
raw.push(("big_rad_err_no_mate_rail", c));
|
||||
}
|
||||
// Error: start corner has NULL GetFilletCorner -> SM_ERR_NULL_POINTER
|
||||
{
|
||||
let mut c = base_case(false, false, false);
|
||||
if let Some(v0) = c.model.vertices.get_mut(0) {
|
||||
v0.corner = -1;
|
||||
}
|
||||
raw.push(("big_rad_err_null_corner", c));
|
||||
}
|
||||
|
||||
raw.into_iter()
|
||||
.map(|(name, b)| {
|
||||
let v = serde_json::to_value(Inputs::BigRad(b))
|
||||
.map_err(|e| harness(e.to_string()))?;
|
||||
Ok(Case {
|
||||
unit: "blend.fillet_corner_p3".into(),
|
||||
name: name.into(),
|
||||
source: crate::case::CaseSource::Inline,
|
||||
inputs: v,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The corner's surfaces as the shim's `p3_put_surfaces` writes them: per surface its kind
|
||||
/// (`1` plane, `2` surface of revolution, cone, sphere or torus, `0` any other) and the
|
||||
/// B-spline record of the `SmBSplineSurface` it derives from upstream.
|
||||
@@ -1598,6 +2173,7 @@ fn cpp(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
solvers,
|
||||
} => create_cpp(brep, *validate_only, *init_gaps, *eu, more, solvers),
|
||||
Inputs::Corner { op, model, corner } => corner_cpp(*op, model, corner),
|
||||
Inputs::BigRad(b) => big_rad_cpp(b),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1615,6 +2191,7 @@ fn rust(k: &Kernel, i: &Inputs) -> Result<Output, HarnessError> {
|
||||
ffi::validate_corner(&corner_input(model, corner)?).map_err(|e| harness(e.to_string()))?;
|
||||
corner_rust(k, *op, model, corner)
|
||||
}
|
||||
Inputs::BigRad(b) => big_rad_rust(k, b),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1646,7 +2223,7 @@ impl UnitDiff for Runner {
|
||||
Inputs::Corner { op, model, corner } => {
|
||||
corner_input(model, corner).is_ok_and(|v| ffi::corner(op.ffi(), &v).is_ok())
|
||||
}
|
||||
Inputs::Create { .. } => true,
|
||||
Inputs::Create { .. } | Inputs::BigRad(_) => true,
|
||||
};
|
||||
if ok {
|
||||
return serde_json::to_value(inputs).ok();
|
||||
@@ -1660,7 +2237,9 @@ impl UnitDiff for Runner {
|
||||
seeds: ctx.seeds.max(MIN_GENERATED),
|
||||
..ctx.clone()
|
||||
};
|
||||
default_cases(self, &ctx)
|
||||
let mut out = default_cases(self, &ctx)?;
|
||||
out.extend(big_rad_cases()?);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,9 +132,8 @@ fn try_generate(rng: &mut Rng) -> Option<TopoIn> {
|
||||
if n_edges == 0 {
|
||||
return None;
|
||||
}
|
||||
// one edge per case: after an earlier topo edge replaced a face's surface, the
|
||||
// port's later B-rep edits can diverge (plan/findings.md, RemoveTopoEdges; fixture
|
||||
// `remove_topo_edges_mouse_two_struts`); lists of several edges are fixtures
|
||||
// one edge per generated case; multi-edge sequences (whose earlier merge can
|
||||
// delete a later listed edge unless disjoint) are covered by fixtures
|
||||
let edges = vec![rng.below(n_edges as u64) as i64];
|
||||
Some(TopoIn {
|
||||
brep,
|
||||
|
||||
@@ -262,6 +262,7 @@ enum Handle {
|
||||
Edgeuse(EdgeuseId),
|
||||
Generator(usize),
|
||||
Solver(usize),
|
||||
Corner(usize),
|
||||
Law(usize),
|
||||
Geom(solidcore::blend::fillet_geom::p1::FilletGeomId),
|
||||
Pseudo,
|
||||
@@ -277,12 +278,40 @@ impl SolutionObject for Handle {
|
||||
}
|
||||
}
|
||||
|
||||
enum SolverEntry {
|
||||
ConstantRadius(ConstantRadiusAssistedFs),
|
||||
ConstantDistance(solidcore::blend::fillet_standard_solver::p1::ConstantDistanceFs),
|
||||
VariableRadius(Box<solidcore::blend::fillet_standard_solver::p1::VariableRadiusFs>),
|
||||
SurfaceSurface(solidcore::blend::fillet_standard_solver::p1::SurfaceSurfaceFs),
|
||||
}
|
||||
|
||||
impl SolverEntry {
|
||||
fn base(&self) -> &solidcore::blend::fillet_solver::p1::FilletSolver {
|
||||
match self {
|
||||
Self::ConstantRadius(s) => &s.solver,
|
||||
Self::ConstantDistance(s) => &s.solver,
|
||||
Self::VariableRadius(s) => &s.curve_based.solver,
|
||||
Self::SurfaceSurface(s) => &s.base.solver,
|
||||
}
|
||||
}
|
||||
|
||||
fn base_mut(&mut self) -> &mut solidcore::blend::fillet_solver::p1::FilletSolver {
|
||||
match self {
|
||||
Self::ConstantRadius(s) => &mut s.solver,
|
||||
Self::ConstantDistance(s) => &mut s.solver,
|
||||
Self::VariableRadius(s) => &mut s.curve_based.solver,
|
||||
Self::SurfaceSurface(s) => &mut s.base.solver,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Access {
|
||||
breps: Vec<Brep>,
|
||||
/// The executive's pseudo B-rep (`m_pPseudoBrep`): the solvers' fillet geometries.
|
||||
pseudo: FilletBrep,
|
||||
generators: Vec<FilletSurfaceGenerator>,
|
||||
solvers: Vec<ConstantRadiusAssistedFs>,
|
||||
solvers: Vec<SolverEntry>,
|
||||
corners: Vec<solidcore::blend::fillet_corner::p3::FilletCorner>,
|
||||
laws: Vec<FilletLaw>,
|
||||
edge_caches: BTreeMap<EdgeId, solidcore::curve::curve_cache::CurveCache<Handle>>,
|
||||
}
|
||||
@@ -377,7 +406,7 @@ impl ExecutiveAccess<Handle> for Access {
|
||||
};
|
||||
self.solvers
|
||||
.get(index)
|
||||
.map(|solver| &solver.solver)
|
||||
.map(SolverEntry::base)
|
||||
.ok_or_else(missing_error)
|
||||
}
|
||||
fn solver_base_mut(
|
||||
@@ -389,7 +418,7 @@ impl ExecutiveAccess<Handle> for Access {
|
||||
};
|
||||
self.solvers
|
||||
.get_mut(index)
|
||||
.map(|solver| &mut solver.solver)
|
||||
.map(SolverEntry::base_mut)
|
||||
.ok_or_else(missing_error)
|
||||
}
|
||||
fn solver_has_surface(&self, solver: Handle, rail: usize) -> SolidResult<bool> {
|
||||
@@ -704,8 +733,17 @@ impl ExecutiveAccess<Handle> for Access {
|
||||
fn solver_geom(&self, solver: Handle, index: usize) -> SolidResult<Option<Handle>> {
|
||||
Ok(self.solver_base(solver)?.fillet_geoms.get(index).copied().map(Handle::Geom))
|
||||
}
|
||||
fn solver_edge(&self, _: Handle) -> SolidResult<Handle> {
|
||||
missing()
|
||||
fn solver_edge(&self, solver: Handle) -> SolidResult<Handle> {
|
||||
const OP: &str = "blend::fillet_executive::p2: solver_edge";
|
||||
let eu = self
|
||||
.solver_base(solver)?
|
||||
.edgeuse(0)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))?;
|
||||
self.brep(Handle::Brep(0))?
|
||||
.edgeuse(eu)?
|
||||
.edge
|
||||
.map(Handle::Edge)
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))
|
||||
}
|
||||
fn solver_approx_tolerance(&self, _: Handle) -> SolidResult<f64> {
|
||||
missing()
|
||||
@@ -805,26 +843,47 @@ impl ExecutiveAccess<Handle> for Access {
|
||||
fn corner_edges(&self, _: Handle) -> SolidResult<Vec<Option<Handle>>> {
|
||||
missing()
|
||||
}
|
||||
fn edge_vertices(&self, _: Handle) -> SolidResult<(Option<Handle>, Option<Handle>)> {
|
||||
missing()
|
||||
fn edge_vertices(&self, edge_handle: Handle) -> SolidResult<(Option<Handle>, Option<Handle>)> {
|
||||
let Handle::Edge(e) = edge_handle else {
|
||||
return missing();
|
||||
};
|
||||
let b = self.brep(Handle::Brep(0))?;
|
||||
let v1 = edge::start_vertex(b, e)?;
|
||||
let v2 = match v1 {
|
||||
Some(s) => edge::other_vertex(b, e, Some(s))?,
|
||||
None => None,
|
||||
};
|
||||
Ok((v1.map(Handle::Vertex), v2.map(Handle::Vertex)))
|
||||
}
|
||||
fn variable_radius_solver(
|
||||
&self,
|
||||
solver: Handle,
|
||||
) -> SolidResult<Option<&solidcore::blend::fillet_standard_solver::p1::VariableRadiusFs>> {
|
||||
self.solver_base(solver)?;
|
||||
Ok(None) // Every retained solver here is a real ConstantRadiusAssistedFs.
|
||||
let Handle::Solver(idx) = solver else {
|
||||
return missing();
|
||||
};
|
||||
match self.solvers.get(idx).ok_or_else(missing_error)? {
|
||||
SolverEntry::VariableRadius(vr) => Ok(Some(vr.as_ref())),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
fn variable_radius_solver_mut(
|
||||
&mut self,
|
||||
solver: Handle,
|
||||
) -> SolidResult<Option<&mut solidcore::blend::fillet_standard_solver::p1::VariableRadiusFs>>
|
||||
{
|
||||
self.solver_base(solver)?;
|
||||
Ok(None)
|
||||
let Handle::Solver(idx) = solver else {
|
||||
return missing();
|
||||
};
|
||||
match self.solvers.get_mut(idx).ok_or_else(missing_error)? {
|
||||
SolverEntry::VariableRadius(vr) => Ok(Some(vr.as_mut())),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
fn solver_edgeuse(&self, solver: Handle, rail: usize) -> SolidResult<Option<Handle>> {
|
||||
self.solver_base(solver)?.edgeuse(rail).map(|edgeuse| edgeuse.map(Handle::Edgeuse))
|
||||
self.solver_base(solver)?
|
||||
.edgeuse(rail)
|
||||
.map(|edgeuse| edgeuse.map(Handle::Edgeuse))
|
||||
}
|
||||
fn solver_corner(&self, _: Handle, _: usize) -> SolidResult<Option<Handle>> {
|
||||
missing()
|
||||
@@ -832,6 +891,187 @@ impl ExecutiveAccess<Handle> for Access {
|
||||
fn solver_other_corner(&self, _: Handle, _: Handle) -> SolidResult<Option<Handle>> {
|
||||
missing()
|
||||
}
|
||||
fn solver_corner_in(
|
||||
&self,
|
||||
executive: &FilletExecutive<Handle>,
|
||||
solver: Handle,
|
||||
index: usize,
|
||||
) -> SolidResult<Option<Handle>> {
|
||||
let b = self.brep(Handle::Brep(0))?;
|
||||
let Some(v) = self.solver_base(solver)?.vertex(b, index)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
executive.fillet_corner_of_vertex(Handle::Vertex(v))
|
||||
}
|
||||
fn solver_other_corner_in(
|
||||
&self,
|
||||
executive: &FilletExecutive<Handle>,
|
||||
solver: Handle,
|
||||
corner: Handle,
|
||||
) -> SolidResult<Option<Handle>> {
|
||||
let c0 = self.solver_corner_in(executive, solver, 0)?;
|
||||
let c1 = self.solver_corner_in(executive, solver, 1)?;
|
||||
if c0 == Some(corner) {
|
||||
Ok(c1)
|
||||
} else if c1 == Some(corner) {
|
||||
Ok(c0)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
fn corner_tangent_solver_in(
|
||||
&self,
|
||||
k: &Kernel,
|
||||
executive: &FilletExecutive<Handle>,
|
||||
corner: Handle,
|
||||
solver: Handle,
|
||||
) -> SolidResult<Option<Handle>> {
|
||||
let Handle::Corner(cid) = corner else {
|
||||
return missing();
|
||||
};
|
||||
let c = self.corners.get(cid).ok_or_else(missing_error)?;
|
||||
let b = self.brep(Handle::Brep(0))?;
|
||||
c.tangent_fillet_solver_with_lookup(k, b, solver, |eu| {
|
||||
executive.fillet_solver_of_edgeuse(Handle::Edgeuse(eu))
|
||||
})
|
||||
}
|
||||
fn smooth_solver_edge0_id(&self, solver: Handle) -> SolidResult<EdgeId> {
|
||||
let Handle::Edge(e) = self.solver_edge(solver)? else {
|
||||
return missing();
|
||||
};
|
||||
Ok(e)
|
||||
}
|
||||
fn smooth_target_brep(&self) -> SolidResult<&Brep> {
|
||||
self.brep(Handle::Brep(0))
|
||||
}
|
||||
fn smooth_with_target_mut<T>(
|
||||
&mut self,
|
||||
edges: &[EdgeId],
|
||||
f: impl FnOnce(&mut Brep) -> SolidResult<T>,
|
||||
) -> SolidResult<T> {
|
||||
for &e in edges {
|
||||
self.edge_caches.remove(&e);
|
||||
}
|
||||
let b = self.breps.first_mut().ok_or_else(missing_error)?;
|
||||
f(b)
|
||||
}
|
||||
fn smooth_set_edge_curve(
|
||||
&mut self,
|
||||
k: &mut Kernel,
|
||||
edge_id: EdgeId,
|
||||
curve: solidcore::curve::curve::Curve,
|
||||
) -> SolidResult<()> {
|
||||
use solidcore::topology::brep::p3 as bp3;
|
||||
self.edge_caches.remove(&edge_id);
|
||||
let b = self.breps.first_mut().ok_or_else(missing_error)?;
|
||||
bp3::with_kernel_callbacks(k, b, |_k, b, env| {
|
||||
let _ = edge::set_curve(
|
||||
b,
|
||||
edge_id,
|
||||
Some(curve),
|
||||
edge::SetCurveOptions {
|
||||
delete_old_curve: true,
|
||||
dbg_warn_leaks: true,
|
||||
call_notify: true,
|
||||
},
|
||||
env,
|
||||
[None, None],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
fn smooth_solver_original_curve(
|
||||
&self,
|
||||
solver: Handle,
|
||||
) -> SolidResult<Option<&solidcore::curve::curve::Curve>> {
|
||||
let Handle::Solver(idx) = solver else {
|
||||
return missing();
|
||||
};
|
||||
Ok(match self.solvers.get(idx).ok_or_else(missing_error)? {
|
||||
SolverEntry::VariableRadius(vr) => vr.curve_based.original_curve(),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
fn smooth_set_solver_original_curve(
|
||||
&mut self,
|
||||
solver: Handle,
|
||||
curve: solidcore::curve::curve::Curve,
|
||||
) -> SolidResult<()> {
|
||||
let Handle::Solver(idx) = solver else {
|
||||
return missing();
|
||||
};
|
||||
if let SolverEntry::VariableRadius(vr) =
|
||||
self.solvers.get_mut(idx).ok_or_else(missing_error)?
|
||||
{
|
||||
vr.curve_based.set_original_curve(Some(curve));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn smooth_set_solver_law_interval(
|
||||
&mut self,
|
||||
solver: Handle,
|
||||
interval: Extent1d,
|
||||
) -> SolidResult<()> {
|
||||
let Handle::Solver(idx) = solver else {
|
||||
return missing();
|
||||
};
|
||||
if let SolverEntry::VariableRadius(vr) =
|
||||
self.solvers.get_mut(idx).ok_or_else(missing_error)?
|
||||
{
|
||||
vr.set_fillet_law_interval(interval);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn create_corners(
|
||||
&mut self,
|
||||
k: &mut Kernel,
|
||||
executive: &mut FilletExecutive<Handle>,
|
||||
error: Option<&mut ErrorInfo<Handle>>,
|
||||
) -> SolidResult<()> {
|
||||
let b = self.breps.first_mut().ok_or_else(missing_error)?;
|
||||
let solvers = &self.solvers;
|
||||
let corners = &mut self.corners;
|
||||
executive.create_fillet_corners(
|
||||
k,
|
||||
|k, executive, vertex, edgeuses| {
|
||||
let Some(Handle::Vertex(v)) = vertex else {
|
||||
return missing();
|
||||
};
|
||||
let eus = edgeuses
|
||||
.iter()
|
||||
.map(|h| match *h {
|
||||
Handle::Edgeuse(eu) => Ok(eu),
|
||||
_ => missing(),
|
||||
})
|
||||
.collect::<SolidResult<Vec<_>>>()?;
|
||||
let made =
|
||||
solidcore::blend::fillet_corner::p3::FilletCorner::create_with_solver_lookup(
|
||||
k,
|
||||
b,
|
||||
v,
|
||||
&eus,
|
||||
|eu| {
|
||||
let Some(Handle::Solver(idx)) =
|
||||
executive.fillet_solver_of_edgeuse(Handle::Edgeuse(eu))
|
||||
else {
|
||||
return missing();
|
||||
};
|
||||
solvers
|
||||
.get(idx)
|
||||
.map(SolverEntry::base)
|
||||
.ok_or_else(missing_error)
|
||||
},
|
||||
)?;
|
||||
let Some(corner) = made else {
|
||||
return Ok(None);
|
||||
};
|
||||
let id = corners.len();
|
||||
corners.push(corner);
|
||||
Ok(Some(Handle::Corner(id)))
|
||||
},
|
||||
error,
|
||||
)
|
||||
}
|
||||
fn corner_setback_distance(&self, _: Handle) -> SolidResult<f64> {
|
||||
missing()
|
||||
}
|
||||
@@ -965,16 +1205,140 @@ impl ExecutiveAccess<Handle> for Access {
|
||||
eu,
|
||||
None,
|
||||
)?;
|
||||
self.solvers.push(fs);
|
||||
self.solvers.push(SolverEntry::ConstantRadius(fs));
|
||||
Ok(Handle::Solver(self.solvers.len() - 1))
|
||||
}
|
||||
fn new_fillet_solver(
|
||||
&mut self,
|
||||
k: &mut Kernel,
|
||||
spec: &solidcore::blend::fillet_executive::p2::SolverSpec<'_, Handle>,
|
||||
) -> SolidResult<Handle> {
|
||||
use solidcore::blend::fillet_custom_solver::SurfaceId;
|
||||
use solidcore::blend::fillet_executive::p2::SolverSpec;
|
||||
use solidcore::blend::fillet_standard_solver::p1::{
|
||||
ConstantDistanceFs, SurfaceSurfaceFs, VariableRadiusFs,
|
||||
};
|
||||
|
||||
match *spec {
|
||||
SolverSpec::ConstantRadius {
|
||||
approx_tol,
|
||||
ang_tol_rad,
|
||||
tan_tol_rad,
|
||||
radius,
|
||||
edgeuse,
|
||||
} => self.new_constant_radius_solver(
|
||||
k,
|
||||
approx_tol,
|
||||
ang_tol_rad,
|
||||
tan_tol_rad,
|
||||
radius,
|
||||
edgeuse,
|
||||
),
|
||||
SolverSpec::ConstantDistance {
|
||||
approx_tol,
|
||||
ang_tol_rad,
|
||||
tan_tol_rad,
|
||||
radius,
|
||||
edgeuse,
|
||||
} => {
|
||||
let Handle::Edgeuse(eu) = edgeuse else {
|
||||
return missing();
|
||||
};
|
||||
let id = FilletSolverId(
|
||||
u32::try_from(self.solvers.len()).map_err(|_| missing_error())?,
|
||||
);
|
||||
let b = self.breps.first_mut().ok_or_else(missing_error)?;
|
||||
let fs = ConstantDistanceFs::from_edgeuse(
|
||||
k,
|
||||
b,
|
||||
&mut self.pseudo,
|
||||
id,
|
||||
approx_tol,
|
||||
ang_tol_rad,
|
||||
tan_tol_rad,
|
||||
radius,
|
||||
eu,
|
||||
)?;
|
||||
self.solvers.push(SolverEntry::ConstantDistance(fs));
|
||||
Ok(Handle::Solver(self.solvers.len() - 1))
|
||||
}
|
||||
SolverSpec::VariableRadius {
|
||||
approx_tol,
|
||||
ang_tol_rad,
|
||||
tan_tol_rad,
|
||||
radius,
|
||||
edgeuse,
|
||||
law,
|
||||
reverse,
|
||||
} => {
|
||||
let (Handle::Edgeuse(eu), Handle::Law(law_idx)) = (edgeuse, law) else {
|
||||
return missing();
|
||||
};
|
||||
let law = self.laws.get(law_idx).ok_or_else(missing_error)?.clone();
|
||||
let id = FilletSolverId(
|
||||
u32::try_from(self.solvers.len()).map_err(|_| missing_error())?,
|
||||
);
|
||||
let b = self.breps.first_mut().ok_or_else(missing_error)?;
|
||||
let fs = VariableRadiusFs::from_edgeuse(
|
||||
k,
|
||||
b,
|
||||
&mut self.pseudo,
|
||||
id,
|
||||
approx_tol,
|
||||
ang_tol_rad,
|
||||
tan_tol_rad,
|
||||
radius,
|
||||
eu,
|
||||
law,
|
||||
reverse,
|
||||
)?;
|
||||
self.solvers.push(SolverEntry::VariableRadius(Box::new(fs)));
|
||||
Ok(Handle::Solver(self.solvers.len() - 1))
|
||||
}
|
||||
SolverSpec::SurfaceSurface {
|
||||
tolerance,
|
||||
ang_tol,
|
||||
tan_tol,
|
||||
radii,
|
||||
surfaces,
|
||||
orientations,
|
||||
guesses,
|
||||
trim_type,
|
||||
reverse_trim,
|
||||
} => {
|
||||
let id = FilletSolverId(
|
||||
u32::try_from(self.solvers.len()).map_err(|_| missing_error())?,
|
||||
);
|
||||
let pair = [
|
||||
(surfaces[0].clone(), SurfaceId::Free(0)),
|
||||
(surfaces[1].clone(), SurfaceId::Free(1)),
|
||||
];
|
||||
let mut fs = SurfaceSurfaceFs::new(
|
||||
k,
|
||||
&mut self.pseudo,
|
||||
id,
|
||||
tolerance,
|
||||
ang_tol,
|
||||
tan_tol,
|
||||
radii,
|
||||
pair,
|
||||
orientations,
|
||||
guesses,
|
||||
)?;
|
||||
fs.set_fillet_trim_type(trim_type);
|
||||
fs.base.solver.set_reverse_trim(reverse_trim);
|
||||
self.solvers.push(SolverEntry::SurfaceSurface(fs));
|
||||
Ok(Handle::Solver(self.solvers.len() - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
fn set_surface_generator(&mut self, solver: Handle, generator: Handle) -> SolidResult<()> {
|
||||
let (Handle::Solver(s), Handle::Generator(g)) = (solver, generator) else {
|
||||
return missing();
|
||||
};
|
||||
let fsg = *self.generators.get(g).ok_or_else(missing_error)?;
|
||||
let fs = self.solvers.get_mut(s).ok_or_else(missing_error)?;
|
||||
fs.solver.set_fillet_surface_generator(Some(fsg));
|
||||
fs.base_mut().set_fillet_surface_generator(Some(fsg));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1101,6 +1465,7 @@ fn box_access(bx: &BoxIn) -> Result<Result<Access, Output>, HarnessError> {
|
||||
pseudo: FilletBrep::new(),
|
||||
generators: Vec::new(),
|
||||
solvers: Vec::new(),
|
||||
corners: Vec::new(),
|
||||
laws: Vec::new(),
|
||||
edge_caches: BTreeMap::new(),
|
||||
}))
|
||||
@@ -1279,6 +1644,32 @@ fn load_solvers(
|
||||
ex.set_fillet_parameters(k, access, ¶ms)
|
||||
}
|
||||
|
||||
fn push_bspline_curve(o: &mut Vec<f64>, crv: Option<&solidcore::curve::curve::Curve>) {
|
||||
use solidcore::curve::curve::CurveKind;
|
||||
let bsc = crv.and_then(|c| match c.kind() {
|
||||
CurveKind::BSpline(b) => Some(std::borrow::Cow::Borrowed(b)),
|
||||
CurveKind::Line(l) => l.base_with_nurb().ok(),
|
||||
CurveKind::Ellipse(e) => Some(std::borrow::Cow::Borrowed(e.base())),
|
||||
CurveKind::Parabola(p) => Some(std::borrow::Cow::Borrowed(p.base())),
|
||||
CurveKind::Hyperbola(h) => Some(std::borrow::Cow::Borrowed(h.base())),
|
||||
_ => None,
|
||||
});
|
||||
let Some(nurb) = bsc.as_deref().and_then(|b| b.gw_nurb()) else {
|
||||
o.push(0.0);
|
||||
return;
|
||||
};
|
||||
o.push(1.0);
|
||||
let cpts = nurb.cpts();
|
||||
let knots = nurb.knots();
|
||||
o.push(cpts.len() as f64 - 1.0);
|
||||
o.push(nurb.degree() as f64);
|
||||
o.push(knots.len() as f64 - 1.0);
|
||||
for p in cpts {
|
||||
o.extend([p.x, p.y, p.z, p.w]);
|
||||
}
|
||||
o.extend_from_slice(knots);
|
||||
}
|
||||
|
||||
fn exec(sub: usize, n: usize, bx: &BoxIn) -> Result<Output, HarnessError> {
|
||||
let mut access = match box_access(bx)? {
|
||||
Ok(a) => a,
|
||||
@@ -1287,6 +1678,168 @@ fn exec(sub: usize, n: usize, bx: &BoxIn) -> Result<Output, HarnessError> {
|
||||
let mut k = Kernel::default();
|
||||
let mut executive = executive(Some(Handle::Brep(0)));
|
||||
let ex = &mut executive;
|
||||
if sub >= 3 {
|
||||
use solidcore::geom::attribute::ObjectKey;
|
||||
use solidcore::geom::object::ObjectRef;
|
||||
use solidcore::topology::brep::{p3 as bp3, p9 as bp9};
|
||||
|
||||
let all_edges: Vec<EdgeId> = access
|
||||
.brep(Handle::Brep(0))
|
||||
.map_err(|e| harness(e.to_string()))?
|
||||
.edges()
|
||||
.ids()
|
||||
.collect();
|
||||
let e0 = *all_edges.first().ok_or_else(|| harness("empty box edges"))?;
|
||||
let ivl = access
|
||||
.brep(Handle::Brep(0))
|
||||
.and_then(|b| edge::interval(b, e0))
|
||||
.map_err(|e| harness(e.to_string()))?;
|
||||
let split_t = 0.35 * ivl.min() + 0.65 * ivl.max();
|
||||
let split_res = {
|
||||
let b = access
|
||||
.brep_mut(Handle::Brep(0))
|
||||
.map_err(|e| harness(e.to_string()))?;
|
||||
bp3::with_kernel_callbacks(&mut k, b, |k, b, env| {
|
||||
bp9::make_vertex_split_edge(k, b, e0, split_t, env, &|eid, kind| ObjectRef {
|
||||
key: ObjectKey(((kind as u64) << 32) | u64::from(eid.index())),
|
||||
type_code: kind,
|
||||
})
|
||||
})
|
||||
};
|
||||
let split = match split_res {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Ok(error_output(&e)),
|
||||
};
|
||||
let Some([e1, e2]) = split.edges else {
|
||||
return Ok(values_output(vec![f64::from(split.status())]));
|
||||
};
|
||||
{
|
||||
let b = access
|
||||
.brep_mut(Handle::Brep(0))
|
||||
.map_err(|e| harness(e.to_string()))?;
|
||||
if let Err(e) = edge::reverse_orientation(&k, b, e2) {
|
||||
return Ok(error_output(&e));
|
||||
}
|
||||
let e2_ivl = Extent1d::from_min_max(0.0, 2.0);
|
||||
if let Some(crv) = b
|
||||
.edge_mut(e2)
|
||||
.map_err(|e| harness(e.to_string()))?
|
||||
.curve
|
||||
.as_mut()
|
||||
{
|
||||
let _ = crv.edit_parameterization(&k, &e2_ivl);
|
||||
}
|
||||
b.edge_mut(e2)
|
||||
.map_err(|e| harness(e.to_string()))?
|
||||
.interval = e2_ivl;
|
||||
}
|
||||
let mut edges = vec![Handle::Edge(e2), Handle::Edge(e1)];
|
||||
for &eid in all_edges.iter().skip(1).take(n.saturating_sub(2)) {
|
||||
edges.push(Handle::Edge(eid));
|
||||
}
|
||||
let mut law_handles = Vec::new();
|
||||
if sub == 5 {
|
||||
for i in 0..edges.len() {
|
||||
access.laws.push(FilletLaw::linear(1.0, 2.0, None));
|
||||
law_handles.push(Some(Handle::Law(i)));
|
||||
}
|
||||
}
|
||||
let radius_types = [if sub == 5 {
|
||||
FilletSolverType::VariableRadius
|
||||
} else {
|
||||
FilletSolverType::ConstRadius
|
||||
}];
|
||||
let params = FilletParameters {
|
||||
brep: Some(Handle::Brep(0)),
|
||||
edges: &edges,
|
||||
xsect_types: &[FilletSurfaceGeneratorType::Linear],
|
||||
radius_types: &radius_types,
|
||||
radius_values: &[1.0],
|
||||
var_rad_laws: if sub == 5 { Some(&law_handles) } else { None },
|
||||
approx_tol: 0.0,
|
||||
ang_tol_deg: 0.0,
|
||||
tan_tol_deg: 0.0,
|
||||
xsect_tol: 0.0,
|
||||
continuity: 1,
|
||||
thumbweight: 1.0,
|
||||
};
|
||||
if let Err(e) = ex.set_fillet_parameters(&mut k, &mut access, ¶ms) {
|
||||
return Ok(if e.is_unported() {
|
||||
error_output(&e)
|
||||
} else {
|
||||
values_output(vec![status_of(&e)])
|
||||
});
|
||||
}
|
||||
if let Err(e) = access.create_corners(&mut k, ex, None) {
|
||||
return Ok(if e.is_unported() {
|
||||
error_output(&e)
|
||||
} else {
|
||||
values_output(vec![status_of(&e)])
|
||||
});
|
||||
}
|
||||
if sub == 3 {
|
||||
let mut list: Vec<Option<Handle>> = ex.solvers.iter().copied().map(Some).collect();
|
||||
let mut seq = Vec::new();
|
||||
return match ex.collect_tangent_edge_sequence_narrow(&k, &access, &mut list, &mut seq) {
|
||||
Ok(()) => Ok(values_output(vec![0.0, seq.len() as f64, list.len() as f64])),
|
||||
Err(e) if e.is_unported() => Ok(error_output(&e)),
|
||||
Err(e) => Ok(values_output(vec![
|
||||
status_of(&e),
|
||||
seq.len() as f64,
|
||||
list.len() as f64,
|
||||
])),
|
||||
};
|
||||
}
|
||||
if let Err(e) = ex.process_smooth_edge_sequences(&mut k, &mut access) {
|
||||
return Ok(if e.is_unported() {
|
||||
error_output(&e)
|
||||
} else {
|
||||
values_output(vec![status_of(&e)])
|
||||
});
|
||||
}
|
||||
let b = access
|
||||
.brep(Handle::Brep(0))
|
||||
.map_err(|e| harness(e.to_string()))?;
|
||||
let ivl1 = edge::interval(b, e1).map_err(|e| harness(e.to_string()))?;
|
||||
let ivl2 = edge::interval(b, e2).map_err(|e| harness(e.to_string()))?;
|
||||
let mut o = vec![0.0, ivl1.min(), ivl1.max(), ivl2.min(), ivl2.max()];
|
||||
if sub == 5 {
|
||||
let vr0 = access
|
||||
.variable_radius_solver(Handle::Solver(0))
|
||||
.map_err(|e| harness(e.to_string()))?;
|
||||
let vr1 = access
|
||||
.variable_radius_solver(Handle::Solver(1))
|
||||
.map_err(|e| harness(e.to_string()))?;
|
||||
o.push(vr0.map_or(-1.0, |v| v.fillet_law_interval().min()));
|
||||
o.push(vr0.map_or(-1.0, |v| v.fillet_law_interval().max()));
|
||||
o.push(vr1.map_or(-1.0, |v| v.fillet_law_interval().min()));
|
||||
o.push(vr1.map_or(-1.0, |v| v.fillet_law_interval().max()));
|
||||
push_bspline_curve(
|
||||
&mut o,
|
||||
b.edge(e1)
|
||||
.map_err(|e| harness(e.to_string()))?
|
||||
.curve
|
||||
.as_ref(),
|
||||
);
|
||||
push_bspline_curve(&mut o, vr0.and_then(|v| v.curve_based.original_curve()));
|
||||
} else {
|
||||
push_bspline_curve(
|
||||
&mut o,
|
||||
b.edge(e1)
|
||||
.map_err(|e| harness(e.to_string()))?
|
||||
.curve
|
||||
.as_ref(),
|
||||
);
|
||||
push_bspline_curve(
|
||||
&mut o,
|
||||
b.edge(e2)
|
||||
.map_err(|e| harness(e.to_string()))?
|
||||
.curve
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
return Ok(values_output(o));
|
||||
}
|
||||
if n > 0 {
|
||||
if let Err(e) = load_solvers(&mut k, &mut access, ex, n) {
|
||||
return Ok(error_output(&e));
|
||||
@@ -1313,7 +1866,7 @@ fn exec(sub: usize, n: usize, bx: &BoxIn) -> Result<Output, HarnessError> {
|
||||
_ => {
|
||||
let mut list: Vec<Option<Handle>> = ex.solvers.iter().copied().map(Some).collect();
|
||||
let mut seq = Vec::new();
|
||||
ex.collect_tangent_edge_sequence(&access, &mut list, &mut seq)
|
||||
ex.collect_tangent_edge_sequence_narrow(&k, &access, &mut list, &mut seq)
|
||||
.map(|()| vec![0.0, seq.len() as f64, list.len() as f64])
|
||||
}
|
||||
};
|
||||
@@ -1386,15 +1939,44 @@ fn temp_radius(variable: bool, start: f64, end: f64, bx: &BoxIn) -> Result<Outpu
|
||||
"harness::fillet_executive_p2::temporary_radius_change",
|
||||
)));
|
||||
};
|
||||
match solidcore::blend::fillet_executive::p2::with_temporary_radius_change(
|
||||
&mut access,
|
||||
solver,
|
||||
0,
|
||||
side,
|
||||
|_| Ok(()),
|
||||
) {
|
||||
Ok(()) => Ok(values_output(vec![0.0])),
|
||||
Err(e) => Ok(error_output(&e)),
|
||||
let read_linear_radii = |acc: &Access| -> SolidResult<(f64, f64)> {
|
||||
let vrfs = acc
|
||||
.variable_radius_solver(solver)?
|
||||
.ok_or_else(missing_error)?;
|
||||
match vrfs.fillet_law().kind {
|
||||
FilletLawKind::Linear {
|
||||
start_radius,
|
||||
end_radius,
|
||||
} => Ok((start_radius, end_radius)),
|
||||
_ => missing(),
|
||||
}
|
||||
};
|
||||
if variable {
|
||||
let during = match solidcore::blend::fillet_executive::p2::with_temporary_radius_change(
|
||||
&mut access,
|
||||
solver,
|
||||
0,
|
||||
side,
|
||||
|acc| read_linear_radii(acc),
|
||||
) {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => return Ok(error_output(&e)),
|
||||
};
|
||||
let after = read_linear_radii(&access).map_err(|e| harness(e.to_string()))?;
|
||||
Ok(values_output(vec![
|
||||
0.0, during.0, during.1, after.0, after.1,
|
||||
]))
|
||||
} else {
|
||||
match solidcore::blend::fillet_executive::p2::with_temporary_radius_change(
|
||||
&mut access,
|
||||
solver,
|
||||
0,
|
||||
side,
|
||||
|_| Ok(()),
|
||||
) {
|
||||
Ok(()) => Ok(values_output(vec![0.0])),
|
||||
Err(e) => Ok(error_output(&e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ pub(crate) fn loader_rust(i: &LoaderScript) -> Result<Output, HarnessError> {
|
||||
.solver
|
||||
.set_fillet_geom_for_surfaces(solver.solver.fillet_geoms.first().copied());
|
||||
}
|
||||
access.solvers.push(solver);
|
||||
access.solvers.push(super::SolverEntry::ConstantRadius(solver));
|
||||
}
|
||||
let mut executive = executive(Some(Handle::Brep(0)));
|
||||
executive.pseudo_brep = Some(Handle::Pseudo);
|
||||
@@ -369,7 +369,7 @@ pub(crate) fn loader_rust(i: &LoaderScript) -> Result<Output, HarnessError> {
|
||||
}
|
||||
loader_snapshot(&access, &executive, &mut out)?;
|
||||
for solver in &access.solvers {
|
||||
let back_pointer_set = solver.solver.fillet_executive().is_some();
|
||||
let back_pointer_set = solver.base().fillet_executive().is_some();
|
||||
out.push(flag(back_pointer_set));
|
||||
let same_pseudo = if back_pointer_set {
|
||||
let pseudo = executive.pseudo_brep.ok_or_else(missing_error)?;
|
||||
@@ -378,8 +378,8 @@ pub(crate) fn loader_rust(i: &LoaderScript) -> Result<Output, HarnessError> {
|
||||
false
|
||||
};
|
||||
out.push(flag(same_pseudo));
|
||||
out.push(solver.solver.g1_knots.len() as f64);
|
||||
out.extend(&solver.solver.g1_knots);
|
||||
out.push(solver.base().g1_knots.len() as f64);
|
||||
out.extend(&solver.base().g1_knots);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
|
||||
@@ -178,6 +178,17 @@ impl ExecutiveAccess<Handle> for Access {
|
||||
fn corner_tangent_solver(&self, _: Handle, _: Handle) -> SolidResult<Option<Handle>> {
|
||||
missing()
|
||||
}
|
||||
/// `SmFilletExecutive::CreateFilletCorners`, the ported
|
||||
/// `FilletExecutive::create_fillet_corners`. The fixture names no vertices, so
|
||||
/// its corner factory (`SmFilletCorner::Create`) rejects any it is asked for.
|
||||
fn create_corners(
|
||||
&mut self,
|
||||
k: &mut Kernel,
|
||||
executive: &mut FilletExecutive<Handle>,
|
||||
error: Option<&mut ErrorInfo<Handle>>,
|
||||
) -> SolidResult<()> {
|
||||
executive.create_fillet_corners(k, |_, _, _, _| missing(), error)
|
||||
}
|
||||
// This empty-B-rep fixture has no face handles or intersector world. These
|
||||
// required adapters reject nonexistent objects; they are not UNPORTED calls
|
||||
// to the now-ported face p3, topology intersector p2, stitch p2, fillet geom p4
|
||||
@@ -744,24 +755,8 @@ fn rust(input: &Inputs) -> Result<Output, HarnessError> {
|
||||
if !input.record.is_empty() {
|
||||
return Err(HarnessError::new("mixed fixture forms"));
|
||||
}
|
||||
// These fixture constructors are the actual missing API entrypoints, not
|
||||
// geometric passes or a replacement for direct p3 parity.
|
||||
let symbol = match example {
|
||||
// UNPORTED(blend.fillet_executive_p1): SmApiCircularFillet
|
||||
0 => "SmApiCircularFillet",
|
||||
// UNPORTED(blend.fillet_executive_p1): SmApiChamferFillet
|
||||
1 => "SmApiChamferFillet",
|
||||
// UNPORTED(blend.fillet_executive_p1): SmApiFilletEdges
|
||||
2 => "SmApiFilletEdges",
|
||||
// UNPORTED(blend.fillet_executive_p1): SmApiFilletEdgesPerEdge
|
||||
3 => "SmApiFilletEdgesPerEdge",
|
||||
// UNPORTED(blend.fillet_executive_p1): SmApiVariableRadiusFillet
|
||||
4 => "SmApiVariableRadiusFillet",
|
||||
// UNPORTED(blend.fillet_executive_p1): SmApiSetBevelCorners
|
||||
5 | 6 => "SmApiSetBevelCorners",
|
||||
_ => return Err(HarnessError::new("unknown upstream fixture")),
|
||||
};
|
||||
return Ok(Output::deferred("blend.fillet_executive_p1", symbol));
|
||||
return end_to_end::upstream_example(example)?
|
||||
.ok_or_else(|| HarnessError::new("unknown upstream fixture"));
|
||||
}
|
||||
ffi::validate(&input.record).map_err(|e| HarnessError::new(e.to_string()))?;
|
||||
let [op, has_target, initialized, prior_boolean, editing, want_error, seed_code, tolerance, piecewise, insertion, global, trimming, classification] =
|
||||
|
||||
@@ -31,6 +31,10 @@ enum Checkpoint {
|
||||
ShrunkFaces,
|
||||
CreatedFilletBrep,
|
||||
MappedFilletEdges,
|
||||
InsertedIntersectionTopology,
|
||||
SelectedOriginalSurfaces,
|
||||
DeletedOriginalFaces,
|
||||
MergedFilletFaces,
|
||||
}
|
||||
impl Checkpoint {
|
||||
fn all_corners(self) -> bool {
|
||||
@@ -71,6 +75,10 @@ impl Checkpoint {
|
||||
Self::ShrunkFaces => 13,
|
||||
Self::CreatedFilletBrep => 14,
|
||||
Self::MappedFilletEdges => 15,
|
||||
Self::InsertedIntersectionTopology => 16,
|
||||
Self::SelectedOriginalSurfaces => 17,
|
||||
Self::DeletedOriginalFaces => 18,
|
||||
Self::MergedFilletFaces => 19,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,9 +300,9 @@ fn run_rust(input: &RealSolid) -> SolidResult<Vec<f64>> {
|
||||
let mut geometry = Vec::new();
|
||||
crate::util::primitive::append(&k, &body, &mut geometry)?;
|
||||
section(&mut values, &geometry);
|
||||
// Unreachable while apply is unported. Before connecting the real API,
|
||||
// replace this draft with its actual post-cleanup attribute store and
|
||||
// entity identities; a fresh constructor alone does not justify emptiness.
|
||||
// The public API leaves no attribute on the body: the executive's fillet-edge
|
||||
// attribute and the temporary merge tags are destroyed with its session, as
|
||||
// upstream's executive destructor and SmObjsDelete do, and the box carries none.
|
||||
let store = solidcore::geom::attribute::AttributeStore::default();
|
||||
let attributes = solidcore::mass::integrator::properties::PropertyAttributes {
|
||||
store: &store,
|
||||
@@ -323,13 +331,92 @@ fn run_rust(input: &RealSolid) -> SolidResult<Vec<f64>> {
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
fn apply(_: &mut Kernel, _: &mut Brep, _: &[EdgeId], _: &RealSolid) -> SolidResult<()> {
|
||||
// Replaced with the actual session API once the session worker's commit is
|
||||
// available. The harness never owns an alternate executive implementation.
|
||||
Err(SolidError::Unported {
|
||||
upstream: "SmApiFilletEdges",
|
||||
unit: "blend.fillet_executive_p1",
|
||||
})
|
||||
fn apply(k: &mut Kernel, body: &mut Brep, edges: &[EdgeId], input: &RealSolid) -> SolidResult<()> {
|
||||
solidcore::blend::fillet_executive::api::fillet_edges(
|
||||
k,
|
||||
body,
|
||||
edges,
|
||||
input.radius,
|
||||
input.xsect,
|
||||
input.continuity,
|
||||
input.thumbweight,
|
||||
)
|
||||
}
|
||||
/// The `TestSmFillets.cpp` examples 0-6, as the shim runs them (examples 2-4 on the
|
||||
/// 10x10x10 box with its first four edges in `GetEdges` order: `SmApiFilletEdges(r=1,
|
||||
/// circular)`, `SmApiFilletEdgesPerEdge(r = 0.5 + 0.25 i, circular)` and
|
||||
/// `SmApiVariableRadiusFillet(0.5 -> 2.0, circular, G1)`). Returns the shim's
|
||||
/// `[status, faces, edges, vertices]` record, or `None` for an unknown example.
|
||||
pub(super) fn upstream_example(example: usize) -> Result<Option<Output>, HarnessError> {
|
||||
if !matches!(example, 0..=6) {
|
||||
return Ok(None);
|
||||
}
|
||||
let run = || -> SolidResult<Vec<f64>> {
|
||||
let mut k = Kernel::default();
|
||||
let (dx, dy, dz) = if example < 2 {
|
||||
(5.0, 10.0, 15.0)
|
||||
} else {
|
||||
(10.0, 10.0, 10.0)
|
||||
};
|
||||
let mut body = solidcore::primitive::api_primitives::create_box(&mut k, Vec3::ZERO, dx, dy, dz)?;
|
||||
let edges = p1::edges(&body, None)?;
|
||||
let selected: Vec<EdgeId> = edges.iter().copied().take(4).collect();
|
||||
let result = match example {
|
||||
0 => solidcore::blend::fillet_executive::api::circular_fillet(&mut k, &mut body, 0.5),
|
||||
1 => solidcore::blend::fillet_executive::api::chamfer_fillet(&mut k, &mut body, 0.5),
|
||||
2 => solidcore::blend::fillet_executive::api::fillet_edges(
|
||||
&mut k, &mut body, &selected, 1.0, 1, 1, 1.0,
|
||||
),
|
||||
3 => {
|
||||
let radii: Vec<f64> =
|
||||
(0..selected.len()).map(|i| 0.5 + 0.25 * i as f64).collect();
|
||||
let types = vec![
|
||||
solidcore::blend::fillet_solver::p1::FilletSurfaceGeneratorType::Circular;
|
||||
selected.len()
|
||||
];
|
||||
solidcore::blend::fillet_executive::api::fillet_edges_per_edge(
|
||||
&mut k, &mut body, &selected, &radii, &types, 1, 1.0,
|
||||
)
|
||||
}
|
||||
4 => solidcore::blend::fillet_executive::api::variable_radius_fillet(
|
||||
&mut k, &mut body, &selected, 0.5, 2.0, 1, 1,
|
||||
),
|
||||
_ => {
|
||||
let bevel: Vec<_> = if example == 6 {
|
||||
p1::vertices(&body, None)?.into_iter().take(1).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
solidcore::blend::fillet_executive::api::set_bevel_corners(
|
||||
&mut k, &mut body, &edges, 1.0, &bevel,
|
||||
)
|
||||
}
|
||||
};
|
||||
let result_status = match result {
|
||||
Ok(()) => 0.0,
|
||||
Err(error) => status(error)?,
|
||||
};
|
||||
Ok(vec![
|
||||
result_status,
|
||||
p1::faces(&body, None)?.len() as f64,
|
||||
p1::edges(&body, None)?.len() as f64,
|
||||
p1::vertices(&body, None)?.len() as f64,
|
||||
])
|
||||
};
|
||||
match run() {
|
||||
Ok(values) => {
|
||||
let mut out = Output::new();
|
||||
out.ints.insert(
|
||||
"api_status_counts".into(),
|
||||
values.into_iter().map(|v| v as i64).collect(),
|
||||
);
|
||||
Ok(Some(out))
|
||||
}
|
||||
Err(error) => match Output::unported(&error) {
|
||||
Some(deferred) => Ok(Some(deferred)),
|
||||
None => Err(HarnessError::new(error.to_string())),
|
||||
},
|
||||
}
|
||||
}
|
||||
pub(super) fn rust(input: &RealSolid) -> Result<Output, HarnessError> {
|
||||
ffi::validate_real_solid(&input.wire()).map_err(|e| HarnessError::new(e.to_string()))?;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! One call to the actual retained insertion endpoint under its production scope.
|
||||
use super::{
|
||||
insertion_state, intermediate_faces, original_deletion, original_merge, original_selection,
|
||||
section, status, Checkpoint, FilletSession, RealSolid, SolidResult,
|
||||
};
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::topology::{Brep, EdgeId};
|
||||
|
||||
pub(super) fn run(
|
||||
k: &mut Kernel,
|
||||
body: &mut Brep,
|
||||
edges: &[EdgeId],
|
||||
input: &RealSolid,
|
||||
before: &[f64],
|
||||
) -> SolidResult<Vec<f64>> {
|
||||
let mut session = FilletSession::new(k, body)?;
|
||||
let mut statuses = [-1.0; 5];
|
||||
statuses[0] = match session.prepare_constant_radius_edges(
|
||||
k,
|
||||
edges,
|
||||
input.radius,
|
||||
input.xsect,
|
||||
input.continuity,
|
||||
input.thumbweight,
|
||||
) {
|
||||
Ok(()) => 0.0,
|
||||
Err(error) => status(error)?,
|
||||
};
|
||||
if statuses[0] == 0.0 {
|
||||
statuses[1] = match session.create_corners(k, None) {
|
||||
Ok(()) => 0.0,
|
||||
Err(error) => status(error)?,
|
||||
};
|
||||
}
|
||||
let pack = |k: &mut Kernel,
|
||||
session: &FilletSession<'_>,
|
||||
statuses: &[f64],
|
||||
prior: &(Vec<f64>, Vec<f64>),
|
||||
after: &(Vec<f64>, Vec<f64>),
|
||||
complete: bool,
|
||||
selection: &[f64],
|
||||
selection_intervals: &[f64]|
|
||||
-> SolidResult<Vec<f64>> {
|
||||
let mut out = Vec::new();
|
||||
section(&mut out, before);
|
||||
section(&mut out, statuses);
|
||||
for (state, values) in [prior, after] {
|
||||
section(&mut out, state);
|
||||
section(&mut out, values);
|
||||
}
|
||||
out.push(f64::from(complete));
|
||||
if complete {
|
||||
let target = intermediate_faces::body_snapshot(k, session.target())?;
|
||||
let managed = intermediate_faces::snapshot(k, session)?;
|
||||
for (state, values) in [&target, &managed] {
|
||||
section(&mut out, state);
|
||||
section(&mut out, values);
|
||||
}
|
||||
}
|
||||
if input.checkpoint == Checkpoint::SelectedOriginalSurfaces {
|
||||
section(&mut out, selection);
|
||||
}
|
||||
if matches!(
|
||||
input.checkpoint,
|
||||
Checkpoint::DeletedOriginalFaces | Checkpoint::MergedFilletFaces
|
||||
) {
|
||||
section(&mut out, selection);
|
||||
section(&mut out, selection_intervals);
|
||||
}
|
||||
Ok(out)
|
||||
};
|
||||
if statuses[1] != 0.0 {
|
||||
return pack(
|
||||
k,
|
||||
&session,
|
||||
&statuses,
|
||||
&(Vec::new(), Vec::new()),
|
||||
&(Vec::new(), Vec::new()),
|
||||
false,
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
}
|
||||
session.with_filleting_context(k, |session, k| {
|
||||
statuses[2] = match session.create_fillet_brep(k, None) {
|
||||
Ok(()) => 0.0,
|
||||
Err(error) => status(error)?,
|
||||
};
|
||||
let mut prior = (Vec::new(), Vec::new());
|
||||
let mut after = (Vec::new(), Vec::new());
|
||||
if statuses[2] == 0.0 {
|
||||
statuses[3] = match session.shrink_fillet_faces(k) {
|
||||
Ok(()) => 0.0,
|
||||
Err(error) => status(error)?,
|
||||
};
|
||||
if statuses[3] == 0.0 {
|
||||
prior = insertion_state::snapshot(k, session)?;
|
||||
statuses[4] = match session.insert_fillet_intersection_topology(k) {
|
||||
Ok(()) => 0.0,
|
||||
Err(error) => status(error)?,
|
||||
};
|
||||
after = insertion_state::snapshot(k, session)?;
|
||||
}
|
||||
}
|
||||
let (selection, intervals, complete) = if statuses[4] == 0.0
|
||||
&& input.checkpoint == Checkpoint::MergedFilletFaces
|
||||
{
|
||||
original_merge::run(k, session)?
|
||||
} else if statuses[4] == 0.0 && input.checkpoint == Checkpoint::DeletedOriginalFaces {
|
||||
original_deletion::run(k, session)?
|
||||
} else if statuses[4] == 0.0 && input.checkpoint == Checkpoint::SelectedOriginalSurfaces {
|
||||
let (selection, complete) = original_selection::run(k, session)?;
|
||||
(selection, Vec::new(), complete)
|
||||
} else {
|
||||
(Vec::new(), Vec::new(), statuses[4] == 0.0)
|
||||
};
|
||||
pack(
|
||||
k, session, &statuses, &prior, &after, complete, &selection, &intervals,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//! Passive actual target/managed state around the complete insertion component.
|
||||
use super::{
|
||||
edge_map_state, intermediate_faces, section, stitch_state, FilletSession, SolidResult,
|
||||
};
|
||||
use solidcore::blend::fillet_executive::session::{FilletBody, FilletObject};
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::topology::{brep::p1, Brep};
|
||||
use solidcore::EntityRef;
|
||||
|
||||
fn counts(body: &Brep, out: &mut Vec<f64>) -> SolidResult<()> {
|
||||
out.extend([
|
||||
p1::vertices(body, None)?.len() as f64,
|
||||
p1::edges(body, None)?.len() as f64,
|
||||
p1::faces(body, None)?.len() as f64,
|
||||
]);
|
||||
Ok(())
|
||||
}
|
||||
pub(super) fn snapshot(
|
||||
k: &Kernel,
|
||||
session: &FilletSession<'_>,
|
||||
) -> SolidResult<(Vec<f64>, Vec<f64>)> {
|
||||
let target = session.target();
|
||||
let intermediate = session.intermediate_brep();
|
||||
let ti = &session.executive().intersector;
|
||||
let mut out = vec![
|
||||
f64::from(k.context.doing_boolean()),
|
||||
f64::from(ti.doing_fillets),
|
||||
f64::from(target.editing_enabled()),
|
||||
f64::from(ti.primary_brep() == Some(FilletObject::Body(FilletBody::Target))),
|
||||
f64::from(
|
||||
intermediate.is_some() && ti.other_brep() == Some(FilletObject::IntermediateBrep),
|
||||
),
|
||||
f64::from(ti.topology_deleted),
|
||||
];
|
||||
counts(target, &mut out)?;
|
||||
if let Some(body) = intermediate {
|
||||
counts(body, &mut out)?;
|
||||
} else {
|
||||
out.extend([0.0; 3]);
|
||||
}
|
||||
counts(session.pseudo_brep().brep(), &mut out)?;
|
||||
let (maps, intervals) = edge_map_state::snapshot(session)?;
|
||||
section(&mut out, &maps);
|
||||
section(&mut out, &intermediate_faces::source_binding(session)?);
|
||||
section(&mut out, &stitch_state::attributes(session)?);
|
||||
let faces = p1::faces(target, None)?;
|
||||
out.push(faces.len() as f64);
|
||||
for id in faces {
|
||||
let key = session.target_identity(EntityRef::Face(id)).key;
|
||||
let mut markers = Vec::new();
|
||||
for handle in session.attributes().attributes(key) {
|
||||
let attribute = session
|
||||
.attributes()
|
||||
.get(handle)
|
||||
.ok_or_else(|| super::invalid("missing actual target attribute"))?;
|
||||
if matches!(attribute.attribute_id(), 777 | 778) {
|
||||
markers.push(attribute.attribute_id() as f64);
|
||||
}
|
||||
}
|
||||
section(&mut out, &markers);
|
||||
}
|
||||
Ok((out, intervals))
|
||||
}
|
||||
@@ -11,6 +11,13 @@ pub(super) fn snapshot(
|
||||
let Some(body) = session.intermediate_brep() else {
|
||||
return Ok((vec![0.0], Vec::new()));
|
||||
};
|
||||
body_snapshot(k, body)
|
||||
}
|
||||
|
||||
pub(super) fn body_snapshot(
|
||||
k: &mut Kernel,
|
||||
body: &solidcore::topology::Brep,
|
||||
) -> SolidResult<(Vec<f64>, Vec<f64>)> {
|
||||
let vertices = p1::vertices(body, None)?;
|
||||
let edges = p1::edges(body, None)?;
|
||||
let faces = p1::faces(body, None)?;
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//! Actual cross-body attribute aliases and ordered users after body copying.
|
||||
use super::{invalid, FilletObject, FilletSession, SolidResult};
|
||||
use solidcore::geom::attribute::{ids, AttributeHandle, ObjectKey};
|
||||
use solidcore::topology::brep::p1;
|
||||
use solidcore::EntityRef;
|
||||
|
||||
fn users(
|
||||
session: &FilletSession<'_>,
|
||||
handle: AttributeHandle,
|
||||
keys: &[Vec<ObjectKey>],
|
||||
out: &mut Vec<f64>,
|
||||
) -> SolidResult<()> {
|
||||
let store = session.attributes();
|
||||
let attribute = store
|
||||
.get(handle)
|
||||
.ok_or_else(|| invalid("actual merge attribute missing"))?;
|
||||
let users = store.users(handle)?;
|
||||
out.extend([
|
||||
attribute.attribute_id() as f64,
|
||||
attribute.type_code() as f64,
|
||||
users.len() as f64,
|
||||
]);
|
||||
for user in users {
|
||||
let mut matches = Vec::new();
|
||||
for (body, entities) in keys.iter().enumerate() {
|
||||
if let Some(index) = entities.iter().position(|key| *key == user) {
|
||||
matches.push(((body + 1) as f64, index as f64));
|
||||
}
|
||||
}
|
||||
if matches.len() != 1 {
|
||||
return Err(invalid(
|
||||
"merge attribute user lacks unique current body edge",
|
||||
));
|
||||
}
|
||||
let (body, edge) = matches
|
||||
.first()
|
||||
.copied()
|
||||
.ok_or_else(|| invalid("missing merge attribute user"))?;
|
||||
out.extend([body, edge]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn snapshot(session: &FilletSession<'_>) -> SolidResult<Vec<f64>> {
|
||||
let target = p1::edges(session.target(), None)?;
|
||||
let managed = session
|
||||
.intermediate_brep()
|
||||
.ok_or_else(|| invalid("actual merge body missing"))?;
|
||||
let intermediate = p1::edges(managed, None)?;
|
||||
let keys = vec![
|
||||
target
|
||||
.into_iter()
|
||||
.map(|id| session.target_identity(EntityRef::Edge(id)).key)
|
||||
.collect(),
|
||||
intermediate
|
||||
.into_iter()
|
||||
.map(|id| {
|
||||
session
|
||||
.intermediate_identity(EntityRef::Edge(id))
|
||||
.map(|value| value.key)
|
||||
})
|
||||
.collect::<SolidResult<Vec<_>>>()?,
|
||||
];
|
||||
let expected = match session.executive().edge_attribute {
|
||||
Some(FilletObject::Attribute(handle)) => Some(handle),
|
||||
None => None,
|
||||
_ => return Err(invalid("merge executive attribute is not actual handle")),
|
||||
};
|
||||
let mut out = vec![f64::from(expected.is_some())];
|
||||
if let Some(handle) = expected {
|
||||
users(session, handle, &keys, &mut out)?;
|
||||
}
|
||||
let store = session.attributes();
|
||||
let mut tags = Vec::new();
|
||||
for body in &keys {
|
||||
for &key in body {
|
||||
for handle in store.attributes(key) {
|
||||
let attribute = store
|
||||
.get(handle)
|
||||
.ok_or_else(|| invalid("merge edge attribute not live"))?;
|
||||
if attribute.attribute_id() == ids::FILLET_EDGES && !tags.contains(&handle) {
|
||||
tags.push(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(tags.len() as f64);
|
||||
for &handle in &tags {
|
||||
users(session, handle, &keys, &mut out)?;
|
||||
}
|
||||
for body in keys {
|
||||
out.push(body.len() as f64);
|
||||
for key in body {
|
||||
let mut own = Vec::new();
|
||||
for handle in store.attributes(key) {
|
||||
let attribute = store
|
||||
.get(handle)
|
||||
.ok_or_else(|| invalid("merge edge attribute not live"))?;
|
||||
if matches!(
|
||||
attribute.attribute_id(),
|
||||
ids::FILLET_EDGES | ids::ALL_FILLET_EDGES
|
||||
) {
|
||||
own.push(handle);
|
||||
}
|
||||
}
|
||||
out.push(own.len() as f64);
|
||||
for handle in own {
|
||||
let attribute = store
|
||||
.get(handle)
|
||||
.ok_or_else(|| invalid("merge edge attribute missing"))?;
|
||||
if attribute.attribute_id() == ids::ALL_FILLET_EDGES && Some(handle) != expected {
|
||||
return Err(invalid("unexpected unrelated executive edge attribute"));
|
||||
}
|
||||
out.extend([
|
||||
attribute.attribute_id() as f64,
|
||||
f64::from(Some(handle) == expected),
|
||||
tags.iter()
|
||||
.position(|value| *value == handle)
|
||||
.map_or(-1.0, |index| index as f64),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
//! Actual retained deletion attempt; removed identities are labeled before use.
|
||||
use super::{
|
||||
insertion_state, invalid, original_selection, section, status, FilletSession, SolidResult,
|
||||
};
|
||||
use solidcore::blend::fillet_executive::session::{
|
||||
FilletBody, FilletObject, OriginalSurfaceDeletion,
|
||||
};
|
||||
use solidcore::geom::context::MarkType;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::topology::brep::{p1, p3::topology_ref};
|
||||
use solidcore::topology::Brep;
|
||||
use solidcore::EntityRef;
|
||||
|
||||
pub(super) struct Frame {
|
||||
pub(super) entities: [Vec<EntityRef>; 3],
|
||||
pub(super) marks: [Vec<u64>; 3],
|
||||
}
|
||||
pub(super) fn entities(body: &Brep) -> SolidResult<[Vec<EntityRef>; 3]> {
|
||||
Ok([
|
||||
p1::vertices(body, None)?
|
||||
.into_iter()
|
||||
.map(EntityRef::Vertex)
|
||||
.collect(),
|
||||
p1::edges(body, None)?
|
||||
.into_iter()
|
||||
.map(EntityRef::Edge)
|
||||
.collect(),
|
||||
p1::faces(body, None)?
|
||||
.into_iter()
|
||||
.map(EntityRef::Face)
|
||||
.collect(),
|
||||
])
|
||||
}
|
||||
pub(super) fn frames(session: &mut FilletSession<'_>) -> SolidResult<Vec<Frame>> {
|
||||
let state = session.insertion_state()?;
|
||||
let managed = state
|
||||
.intermediate
|
||||
.notify_serial()
|
||||
.ok_or_else(|| invalid("deletion managed identity missing"))?;
|
||||
let mut frames = Vec::new();
|
||||
for (body, serial) in [
|
||||
(&*state.target, state.target_serial),
|
||||
(&*state.intermediate, managed),
|
||||
] {
|
||||
let entities = entities(body)?;
|
||||
let mut marks = [Vec::new(), Vec::new(), Vec::new()];
|
||||
for (entities, marks) in entities.iter().zip(&mut marks) {
|
||||
for entity in entities {
|
||||
let record = state
|
||||
.notification_records
|
||||
.get(&topology_ref(serial, *entity).key)
|
||||
.ok_or_else(|| invalid("predeletion actual record missing"))?;
|
||||
marks.push(record.get_mark(MarkType::Mark));
|
||||
}
|
||||
}
|
||||
frames.push(Frame { entities, marks });
|
||||
}
|
||||
Ok(frames)
|
||||
}
|
||||
pub(super) fn face_labels(values: &[FilletObject], frames: &[Frame]) -> SolidResult<Vec<f64>> {
|
||||
let mut out = Vec::new();
|
||||
for object in values {
|
||||
let (body, entity) = match object {
|
||||
FilletObject::Topology(FilletBody::Target, entity @ EntityRef::Face(_)) => (0, *entity),
|
||||
FilletObject::IntermediateTopology(entity @ EntityRef::Face(_)) => (1, *entity),
|
||||
_ => return Err(invalid("deletion output lacks actual body-qualified face")),
|
||||
};
|
||||
let faces = frames
|
||||
.get(body)
|
||||
.and_then(|f| f.entities.get(2))
|
||||
.ok_or_else(|| invalid("missing deletion input frame"))?;
|
||||
let index = faces
|
||||
.iter()
|
||||
.position(|value| *value == entity)
|
||||
.ok_or_else(|| invalid("deletion output not in pre-call owning traversal"))?;
|
||||
out.extend([(body + 1) as f64, index as f64]);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(super) fn run(
|
||||
k: &mut Kernel,
|
||||
session: &mut FilletSession<'_>,
|
||||
) -> SolidResult<(Vec<f64>, Vec<f64>, bool)> {
|
||||
let mut out = vec![f64::from(session.executive().options.global_merge)];
|
||||
let epochs = original_selection::MARK_TYPES.map(|mark| k.context.current_mark(mark));
|
||||
original_selection::context(k, &mut out, None)?;
|
||||
let before = frames(session)?;
|
||||
let mut deleted = OriginalSurfaceDeletion::default();
|
||||
let result = session.trim_and_delete_original_fillet_faces(k, &mut deleted);
|
||||
let complete = result.is_ok();
|
||||
out.push(match result {
|
||||
Ok(()) => 0.0,
|
||||
Err(error) => status(error)?,
|
||||
});
|
||||
original_selection::context(k, &mut out, Some(&epochs))?;
|
||||
out.extend([
|
||||
f64::from(k.context.doing_boolean()),
|
||||
f64::from(session.target().editing_enabled()),
|
||||
f64::from(session.executive().intersector.doing_fillets),
|
||||
f64::from(session.executive().intersector.topology_deleted),
|
||||
]);
|
||||
// Identity-only lookups in pre-call arrays: never dereference removed faces.
|
||||
section(&mut out, &face_labels(&deleted.kept, &before)?);
|
||||
section(&mut out, &face_labels(&deleted.deleted, &before)?);
|
||||
{
|
||||
let state = session.insertion_state()?;
|
||||
let serial = state
|
||||
.intermediate
|
||||
.notify_serial()
|
||||
.ok_or_else(|| invalid("managed deletion identity missing"))?;
|
||||
for (body_index, (body, serial)) in [
|
||||
(&*state.target, state.target_serial),
|
||||
(&*state.intermediate, serial),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let frame = before
|
||||
.get(body_index)
|
||||
.ok_or_else(|| invalid("missing actual before frame"))?;
|
||||
for (kind, entities) in entities(body)?.into_iter().enumerate() {
|
||||
let previous = frame
|
||||
.entities
|
||||
.get(kind)
|
||||
.ok_or_else(|| invalid("missing actual pre-call kind"))?;
|
||||
let previous_marks = frame
|
||||
.marks
|
||||
.get(kind)
|
||||
.ok_or_else(|| invalid("missing actual pre-call marks"))?;
|
||||
let mut marks = vec![entities.len() as f64];
|
||||
for entity in entities {
|
||||
let old = previous
|
||||
.iter()
|
||||
.position(|value| *value == entity)
|
||||
.ok_or_else(|| invalid("deletion created unexpected owning topology"))?;
|
||||
let raw_before = previous_marks
|
||||
.get(old)
|
||||
.ok_or_else(|| invalid("missing pre-call mark value"))?;
|
||||
let record = state
|
||||
.notification_records
|
||||
.get(&topology_ref(serial, entity).key)
|
||||
.ok_or_else(|| invalid("postdeletion actual record missing"))?;
|
||||
let raw = record.get_mark(MarkType::Mark);
|
||||
marks.extend([
|
||||
old as f64,
|
||||
f64::from(record.is_marked(&k.context, MarkType::Mark)),
|
||||
f64::from(
|
||||
k.context.current_mark(MarkType::Mark).checked_sub(1) == Some(raw),
|
||||
),
|
||||
f64::from(*raw_before == raw),
|
||||
]);
|
||||
}
|
||||
section(&mut out, &marks);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not dereference a managed FG source after its owning face was retired.
|
||||
// The ordinary-box observation requires an actual surviving source binding.
|
||||
let managed = session
|
||||
.intermediate_brep()
|
||||
.ok_or_else(|| invalid("missing deletion managed body"))?;
|
||||
let live_faces = p1::faces(managed, None)?;
|
||||
for object in &session.executive().solvers {
|
||||
let FilletObject::Solver(id) = *object else {
|
||||
return Err(invalid("actual deletion solver required"));
|
||||
};
|
||||
for &geometry in &session.solver(id)?.base().fillet_geoms {
|
||||
if let Some(surface) = session.fillet_surface(geometry)? {
|
||||
let mut live = false;
|
||||
for &face in &live_faces {
|
||||
if managed
|
||||
.face(face)?
|
||||
.surface
|
||||
.as_ref()
|
||||
.is_some_and(|value| std::ptr::eq(value, surface))
|
||||
{
|
||||
live = true;
|
||||
}
|
||||
}
|
||||
if !live {
|
||||
return Err(invalid(
|
||||
"post-deletion FG source not in surviving managed faces",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let (after, intervals) = insertion_state::snapshot(k, session)?;
|
||||
section(&mut out, &after);
|
||||
Ok((out, intervals, complete))
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
//! Actual retained selection/deletion/tagging/merge, before body cleanup or stitch.
|
||||
use super::{
|
||||
insertion_state, invalid, merge_attributes,
|
||||
original_deletion::{entities, face_labels, frames},
|
||||
original_selection, section, status, FilletSession, SolidResult,
|
||||
};
|
||||
use solidcore::blend::fillet_executive::session::{FilletObject, OriginalSurfaceDeletion};
|
||||
use solidcore::geom::context::MarkType;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::topology::brep::{p1, p3::topology_ref};
|
||||
|
||||
pub(super) fn run(
|
||||
k: &mut Kernel,
|
||||
session: &mut FilletSession<'_>,
|
||||
) -> SolidResult<(Vec<f64>, Vec<f64>, bool)> {
|
||||
let mut out = vec![f64::from(session.executive().options.global_merge)];
|
||||
let epochs = original_selection::MARK_TYPES.map(|mark| k.context.current_mark(mark));
|
||||
original_selection::context(k, &mut out, None)?;
|
||||
let before = frames(session)?;
|
||||
let mut deleted = OriginalSurfaceDeletion::default();
|
||||
let result = session.trim_delete_and_merge_fillet_faces(k, &mut deleted);
|
||||
let complete = result.is_ok();
|
||||
out.push(match result {
|
||||
Ok(()) => 0.0,
|
||||
Err(error) => status(error)?,
|
||||
});
|
||||
original_selection::context(k, &mut out, Some(&epochs))?;
|
||||
out.extend([
|
||||
f64::from(k.context.doing_boolean()),
|
||||
f64::from(session.target().editing_enabled()),
|
||||
f64::from(session.executive().intersector.doing_fillets),
|
||||
f64::from(session.executive().intersector.topology_deleted),
|
||||
]);
|
||||
// Identity-only lookups in pre-call arrays: never dereference removed faces.
|
||||
section(&mut out, &face_labels(&deleted.kept, &before)?);
|
||||
section(&mut out, &face_labels(&deleted.deleted, &before)?);
|
||||
{
|
||||
let state = session.insertion_state()?;
|
||||
let serial = state
|
||||
.intermediate
|
||||
.notify_serial()
|
||||
.ok_or_else(|| invalid("managed deletion identity missing"))?;
|
||||
for (body_index, (body, serial)) in [
|
||||
(&*state.target, state.target_serial),
|
||||
(&*state.intermediate, serial),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let frame = before
|
||||
.get(body_index)
|
||||
.ok_or_else(|| invalid("missing actual before frame"))?;
|
||||
for (kind, entities) in entities(body)?.into_iter().enumerate() {
|
||||
let previous = frame
|
||||
.entities
|
||||
.get(kind)
|
||||
.ok_or_else(|| invalid("missing actual pre-call kind"))?;
|
||||
let previous_marks = frame
|
||||
.marks
|
||||
.get(kind)
|
||||
.ok_or_else(|| invalid("missing actual pre-call marks"))?;
|
||||
let mut marks = vec![entities.len() as f64];
|
||||
for entity in entities {
|
||||
let old = previous.iter().position(|value| *value == entity);
|
||||
let record = state
|
||||
.notification_records
|
||||
.get(&topology_ref(serial, entity).key)
|
||||
.ok_or_else(|| invalid("post-merge actual record missing"))?;
|
||||
let raw = record.get_mark(MarkType::Mark);
|
||||
marks.extend([
|
||||
f64::from(old.is_some()),
|
||||
old.map_or(-1.0, |index| index as f64),
|
||||
f64::from(record.is_marked(&k.context, MarkType::Mark)),
|
||||
f64::from(
|
||||
k.context.current_mark(MarkType::Mark).checked_sub(1) == Some(raw),
|
||||
),
|
||||
]);
|
||||
if let Some(old) = old {
|
||||
let raw_before = previous_marks
|
||||
.get(old)
|
||||
.ok_or_else(|| invalid("missing pre-merge mark value"))?;
|
||||
marks.push(f64::from(*raw_before == raw));
|
||||
}
|
||||
}
|
||||
section(&mut out, &marks);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not dereference a managed FG source after its owning face was retired.
|
||||
// The ordinary-box observation requires an actual surviving source binding.
|
||||
let managed = session
|
||||
.intermediate_brep()
|
||||
.ok_or_else(|| invalid("missing deletion managed body"))?;
|
||||
let live_faces = p1::faces(managed, None)?;
|
||||
for object in &session.executive().solvers {
|
||||
let FilletObject::Solver(id) = *object else {
|
||||
return Err(invalid("actual deletion solver required"));
|
||||
};
|
||||
for &geometry in &session.solver(id)?.base().fillet_geoms {
|
||||
if let Some(surface) = session.fillet_surface(geometry)? {
|
||||
let mut live = false;
|
||||
for &face in &live_faces {
|
||||
if managed
|
||||
.face(face)?
|
||||
.surface
|
||||
.as_ref()
|
||||
.is_some_and(|value| std::ptr::eq(value, surface))
|
||||
{
|
||||
live = true;
|
||||
}
|
||||
}
|
||||
if !live {
|
||||
return Err(invalid(
|
||||
"post-deletion FG source not in surviving managed faces",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let (after, intervals) = insertion_state::snapshot(k, session)?;
|
||||
section(&mut out, &after);
|
||||
section(&mut out, &merge_attributes::snapshot(session)?);
|
||||
Ok((out, intervals, complete))
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
//! One real selection call, its ordered outputs, and passive retained mark records.
|
||||
use super::{invalid, label, section, status, FilletSession, SolidResult};
|
||||
use solidcore::blend::fillet_executive::session::{
|
||||
FilletBody, FilletObject, OriginalSurfaceSelection,
|
||||
};
|
||||
use solidcore::geom::context::MarkType;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::topology::brep::{p1, p3::topology_ref};
|
||||
use solidcore::EntityRef;
|
||||
|
||||
pub(super) const MARK_TYPES: [MarkType; 5] = [
|
||||
MarkType::Mark,
|
||||
MarkType::Mark2,
|
||||
MarkType::Mark3,
|
||||
MarkType::MarkIo,
|
||||
MarkType::MarkAssert,
|
||||
];
|
||||
|
||||
pub(super) fn context(
|
||||
k: &Kernel,
|
||||
out: &mut Vec<f64>,
|
||||
before: Option<&[u64; 5]>,
|
||||
) -> SolidResult<()> {
|
||||
for (index, mark) in MARK_TYPES.into_iter().enumerate() {
|
||||
out.extend([
|
||||
f64::from(mark as i32),
|
||||
f64::from(k.context.is_mark_locked(mark)),
|
||||
]);
|
||||
if let Some(before) = before {
|
||||
let previous = before
|
||||
.get(index)
|
||||
.ok_or_else(|| invalid("missing mark epoch"))?;
|
||||
let advance = k
|
||||
.context
|
||||
.current_mark(mark)
|
||||
.checked_sub(*previous)
|
||||
.ok_or_else(|| invalid("selection mark epoch decreased"))?;
|
||||
out.push(advance as f64);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mark_history(session: &mut FilletSession<'_>) -> SolidResult<Vec<u64>> {
|
||||
let state = session.insertion_state()?;
|
||||
let serial = state
|
||||
.intermediate
|
||||
.notify_serial()
|
||||
.ok_or_else(|| invalid("managed body lacks issued identity"))?;
|
||||
let mut out = Vec::new();
|
||||
for (body, serial) in [
|
||||
(&*state.target, state.target_serial),
|
||||
(&*state.intermediate, serial),
|
||||
] {
|
||||
for entities in [
|
||||
p1::vertices(body, None)?
|
||||
.into_iter()
|
||||
.map(EntityRef::Vertex)
|
||||
.collect::<Vec<_>>(),
|
||||
p1::edges(body, None)?
|
||||
.into_iter()
|
||||
.map(EntityRef::Edge)
|
||||
.collect(),
|
||||
p1::faces(body, None)?
|
||||
.into_iter()
|
||||
.map(EntityRef::Face)
|
||||
.collect(),
|
||||
] {
|
||||
for entity in entities {
|
||||
let record = state
|
||||
.notification_records
|
||||
.get(&topology_ref(serial, entity).key)
|
||||
.ok_or_else(|| invalid("preselection actual topology record missing"))?;
|
||||
out.push(record.get_mark(MarkType::Mark));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(super) fn run(
|
||||
k: &mut Kernel,
|
||||
session: &mut FilletSession<'_>,
|
||||
) -> SolidResult<(Vec<f64>, bool)> {
|
||||
let mut out = vec![f64::from(session.executive().options.global_merge)];
|
||||
let epochs = MARK_TYPES.map(|mark| k.context.current_mark(mark));
|
||||
context(k, &mut out, None)?;
|
||||
let history = mark_history(session)?;
|
||||
let target_vertices = p1::vertices(session.target(), None)?;
|
||||
let target_edges = p1::edges(session.target(), None)?;
|
||||
let managed = session
|
||||
.intermediate_brep()
|
||||
.ok_or_else(|| invalid("selection requires actual managed body"))?;
|
||||
let managed_vertices = p1::vertices(managed, None)?;
|
||||
let managed_edges = p1::edges(managed, None)?;
|
||||
for entities in [
|
||||
target_vertices
|
||||
.iter()
|
||||
.copied()
|
||||
.map(EntityRef::Vertex)
|
||||
.collect::<Vec<_>>(),
|
||||
target_edges.iter().copied().map(EntityRef::Edge).collect(),
|
||||
] {
|
||||
let mut mates = vec![entities.len() as f64];
|
||||
for (index, entity) in entities.into_iter().enumerate() {
|
||||
let mate = session
|
||||
.executive()
|
||||
.intersector
|
||||
.other_mate(Some(FilletObject::Topology(FilletBody::Target, entity)));
|
||||
let other = match (entity, mate) {
|
||||
(_, None) => -1.0,
|
||||
(
|
||||
EntityRef::Vertex(_),
|
||||
Some(FilletObject::IntermediateTopology(EntityRef::Vertex(id))),
|
||||
) => label(&managed_vertices, Some(id)),
|
||||
(
|
||||
EntityRef::Edge(_),
|
||||
Some(FilletObject::IntermediateTopology(EntityRef::Edge(id))),
|
||||
) => label(&managed_edges, Some(id)),
|
||||
_ => return Err(invalid("selection mate has unexpected actual body role")),
|
||||
};
|
||||
if mate.is_some() && other < 0.0 {
|
||||
return Err(invalid("selection mate is not in actual owning traversal"));
|
||||
}
|
||||
mates.extend([index as f64, f64::from(mate.is_some()), other]);
|
||||
}
|
||||
section(&mut out, &mates);
|
||||
}
|
||||
let mut selected = OriginalSurfaceSelection::default();
|
||||
let result = session.select_original_fillet_surfaces(k, &mut selected);
|
||||
let success = result.is_ok();
|
||||
out.push(match result {
|
||||
Ok(()) => 0.0,
|
||||
Err(error) => status(error)?,
|
||||
});
|
||||
context(k, &mut out, Some(&epochs))?;
|
||||
out.extend([
|
||||
f64::from(k.context.doing_boolean()),
|
||||
f64::from(session.target().editing_enabled()),
|
||||
f64::from(session.executive().intersector.doing_fillets),
|
||||
]);
|
||||
let target_faces = p1::faces(session.target(), None)?;
|
||||
let managed_faces = p1::faces(
|
||||
session
|
||||
.intermediate_brep()
|
||||
.ok_or_else(|| invalid("selection lost actual managed body"))?,
|
||||
None,
|
||||
)?;
|
||||
for (selected, owning) in [
|
||||
(&selected.kept, &managed_faces),
|
||||
(&selected.deleted, &target_faces),
|
||||
] {
|
||||
let mut labels = Vec::new();
|
||||
for id in selected {
|
||||
let id = label(owning, Some(*id));
|
||||
if id < 0.0 {
|
||||
return Err(invalid("selection output is outside actual owning body"));
|
||||
}
|
||||
labels.push(id);
|
||||
}
|
||||
section(&mut out, &labels);
|
||||
}
|
||||
let state = session.insertion_state()?;
|
||||
let managed_serial = state
|
||||
.intermediate
|
||||
.notify_serial()
|
||||
.ok_or_else(|| invalid("managed selection body lacks issued identity"))?;
|
||||
let mut history = history.into_iter();
|
||||
for (body, serial) in [
|
||||
(&*state.target, state.target_serial),
|
||||
(&*state.intermediate, managed_serial),
|
||||
] {
|
||||
for entities in [
|
||||
p1::vertices(body, None)?
|
||||
.into_iter()
|
||||
.map(EntityRef::Vertex)
|
||||
.collect::<Vec<_>>(),
|
||||
p1::edges(body, None)?
|
||||
.into_iter()
|
||||
.map(EntityRef::Edge)
|
||||
.collect(),
|
||||
p1::faces(body, None)?
|
||||
.into_iter()
|
||||
.map(EntityRef::Face)
|
||||
.collect(),
|
||||
] {
|
||||
let mut marks = vec![entities.len() as f64];
|
||||
for entity in entities {
|
||||
let topology = state
|
||||
.notification_records
|
||||
.get(&topology_ref(serial, entity).key)
|
||||
.ok_or_else(|| invalid("selection actual topology record missing"))?;
|
||||
marks.extend([
|
||||
f64::from(topology.is_marked(&k.context, MarkType::Mark)),
|
||||
f64::from(
|
||||
k.context.current_mark(MarkType::Mark).checked_sub(1)
|
||||
== Some(topology.get_mark(MarkType::Mark)),
|
||||
),
|
||||
f64::from(
|
||||
history
|
||||
.next()
|
||||
.ok_or_else(|| invalid("selection changed actual owning traversal"))?
|
||||
== topology.get_mark(MarkType::Mark),
|
||||
),
|
||||
]);
|
||||
}
|
||||
section(&mut out, &marks);
|
||||
}
|
||||
}
|
||||
if history.next().is_some() {
|
||||
return Err(invalid("selection removed actual owning topology"));
|
||||
}
|
||||
Ok((out, success))
|
||||
}
|
||||
@@ -9,8 +9,20 @@ mod edge_geometry;
|
||||
mod edge_map_state;
|
||||
#[path = "generated_geometry.rs"]
|
||||
mod generated_geometry;
|
||||
#[path = "insertion_checkpoint.rs"]
|
||||
mod insertion_checkpoint;
|
||||
#[path = "insertion_state.rs"]
|
||||
mod insertion_state;
|
||||
#[path = "intermediate_faces.rs"]
|
||||
mod intermediate_faces;
|
||||
#[path = "merge_attributes.rs"]
|
||||
mod merge_attributes;
|
||||
#[path = "original_deletion.rs"]
|
||||
mod original_deletion;
|
||||
#[path = "original_merge.rs"]
|
||||
mod original_merge;
|
||||
#[path = "original_selection.rs"]
|
||||
mod original_selection;
|
||||
#[path = "rail_classification.rs"]
|
||||
mod rail_classification;
|
||||
#[path = "rail_metadata.rs"]
|
||||
@@ -236,6 +248,15 @@ pub(super) fn run(
|
||||
input: &RealSolid,
|
||||
before: &[f64],
|
||||
) -> SolidResult<Vec<f64>> {
|
||||
if matches!(
|
||||
input.checkpoint,
|
||||
Checkpoint::InsertedIntersectionTopology
|
||||
| Checkpoint::SelectedOriginalSurfaces
|
||||
| Checkpoint::DeletedOriginalFaces
|
||||
| Checkpoint::MergedFilletFaces
|
||||
) {
|
||||
return insertion_checkpoint::run(k, body, selected, input, before);
|
||||
}
|
||||
let mut session = FilletSession::new(k, body)?;
|
||||
let mut statuses = [-1.0; 12];
|
||||
let prepare = session.prepare_constant_radius_edges(
|
||||
@@ -622,6 +643,16 @@ pub(super) fn run(
|
||||
|
||||
pub(super) fn output(mut values: &[f64], checkpoint: Checkpoint) -> Result<Output, HarnessError> {
|
||||
let mut out = Output::new();
|
||||
if matches!(
|
||||
checkpoint,
|
||||
Checkpoint::InsertedIntersectionTopology
|
||||
| Checkpoint::SelectedOriginalSurfaces
|
||||
| Checkpoint::DeletedOriginalFaces
|
||||
| Checkpoint::MergedFilletFaces
|
||||
) {
|
||||
return insertion_output(values, checkpoint);
|
||||
}
|
||||
|
||||
out.values.insert(
|
||||
"selected_edge_before".into(),
|
||||
take_section(&mut values)?.to_vec(),
|
||||
@@ -893,3 +924,108 @@ pub(super) fn cpp(input: &RealSolid) -> Result<Output, HarnessError> {
|
||||
.map_err(|e| HarnessError::new(e.to_string()))?;
|
||||
output(&values, input.checkpoint)
|
||||
}
|
||||
|
||||
fn insertion_output(mut values: &[f64], checkpoint: Checkpoint) -> Result<Output, HarnessError> {
|
||||
let mut out = Output::new();
|
||||
out.values.insert(
|
||||
"selected_edge_before".into(),
|
||||
take_section(&mut values)?.to_vec(),
|
||||
);
|
||||
for (name, exact) in [
|
||||
("insertion_statuses", true),
|
||||
("insertion_passive_before", true),
|
||||
("insertion_raw_intervals_before", false),
|
||||
("insertion_passive_after", true),
|
||||
("insertion_raw_intervals_after", false),
|
||||
] {
|
||||
let section = take_section(&mut values)?;
|
||||
if exact {
|
||||
if section.iter().any(|v| !v.is_finite() || v.fract() != 0.0) {
|
||||
return Err(HarnessError::new("invalid insertion integer state"));
|
||||
}
|
||||
out.ints
|
||||
.insert(name.into(), section.iter().map(|v| *v as i64).collect());
|
||||
} else {
|
||||
out.values.insert(name.into(), section.to_vec());
|
||||
}
|
||||
}
|
||||
let (&complete, tail) = values
|
||||
.split_first()
|
||||
.ok_or_else(|| HarnessError::new("missing insertion completion"))?;
|
||||
if complete != 0.0 && complete != 1.0 {
|
||||
return Err(HarnessError::new("invalid insertion completion"));
|
||||
}
|
||||
values = tail;
|
||||
let geometry_kind = if checkpoint == Checkpoint::MergedFilletFaces {
|
||||
"merge"
|
||||
} else if checkpoint == Checkpoint::DeletedOriginalFaces {
|
||||
"deletion"
|
||||
} else {
|
||||
"insertion"
|
||||
};
|
||||
out.ints.insert(
|
||||
format!("{geometry_kind}_geometry_present"),
|
||||
vec![i64::from(complete == 1.0)],
|
||||
);
|
||||
if complete == 1.0 {
|
||||
for body in ["target", "intermediate"] {
|
||||
let state = take_section(&mut values)?;
|
||||
if state.iter().any(|v| !v.is_finite() || v.fract() != 0.0) {
|
||||
return Err(HarnessError::new("invalid insertion topology"));
|
||||
}
|
||||
out.ints.insert(
|
||||
format!("{geometry_kind}_{body}_topology"),
|
||||
state.iter().map(|v| *v as i64).collect(),
|
||||
);
|
||||
out.values.insert(
|
||||
format!("{geometry_kind}_{body}_geometry"),
|
||||
take_section(&mut values)?.to_vec(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if checkpoint == Checkpoint::SelectedOriginalSurfaces {
|
||||
let state = take_section(&mut values)?;
|
||||
if state.iter().any(|v| !v.is_finite() || v.fract() != 0.0) {
|
||||
return Err(HarnessError::new("invalid original selection state"));
|
||||
}
|
||||
out.ints.insert(
|
||||
"original_surface_selection".into(),
|
||||
state.iter().map(|v| *v as i64).collect(),
|
||||
);
|
||||
}
|
||||
if checkpoint == Checkpoint::DeletedOriginalFaces {
|
||||
let state = take_section(&mut values)?;
|
||||
if state.iter().any(|v| !v.is_finite() || v.fract() != 0.0) {
|
||||
return Err(HarnessError::new("invalid original deletion state"));
|
||||
}
|
||||
out.ints.insert(
|
||||
"original_surface_deletion".into(),
|
||||
state.iter().map(|v| *v as i64).collect(),
|
||||
);
|
||||
out.values.insert(
|
||||
"original_surface_deletion_raw_intervals".into(),
|
||||
take_section(&mut values)?.to_vec(),
|
||||
);
|
||||
}
|
||||
if checkpoint == Checkpoint::MergedFilletFaces {
|
||||
let state = take_section(&mut values)?;
|
||||
if state
|
||||
.iter()
|
||||
.any(|value| !value.is_finite() || value.fract() != 0.0)
|
||||
{
|
||||
return Err(HarnessError::new("invalid retained merge state"));
|
||||
}
|
||||
out.ints.insert(
|
||||
"original_faces_merge".into(),
|
||||
state.iter().map(|value| *value as i64).collect(),
|
||||
);
|
||||
out.values.insert(
|
||||
"original_faces_merge_raw_intervals".into(),
|
||||
take_section(&mut values)?.to_vec(),
|
||||
);
|
||||
}
|
||||
if !values.is_empty() {
|
||||
return Err(HarnessError::new("trailing insertion data"));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -1462,6 +1462,11 @@ pub(crate) struct RustModel {
|
||||
pub(crate) fillet: Brep,
|
||||
pub(crate) fillet_edges: Vec<EdgeId>,
|
||||
pub(crate) fb: FilletBrep,
|
||||
/// The Model's own geometries (the first `geom_count` of the arena; the shim's
|
||||
/// `geoms` vector). Geometries the library adds later (a temporary solver's, which
|
||||
/// upstream deletes with the solver) are not the Model's: the shim neither exports
|
||||
/// nor addresses them.
|
||||
pub(crate) geom_count: usize,
|
||||
}
|
||||
|
||||
impl RustModel {
|
||||
@@ -1656,6 +1661,7 @@ impl RustModel {
|
||||
fillet,
|
||||
fillet_edges,
|
||||
fb,
|
||||
geom_count: gids.len(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1692,7 +1698,7 @@ impl RustModel {
|
||||
}
|
||||
|
||||
pub(crate) fn geom(&self, i: usize) -> Result<FilletGeomId, HarnessError> {
|
||||
if i < self.fb.geoms().len() {
|
||||
if i < self.geom_count.min(self.fb.geoms().len()) {
|
||||
Ok(FilletGeomId(i as u32))
|
||||
} else {
|
||||
Err(harness("geometry index"))
|
||||
@@ -1875,11 +1881,17 @@ impl RustModel {
|
||||
o.push(self.vertex_index(v));
|
||||
dump_curve(o, r.uv_trim_curve.as_ref());
|
||||
let g = self.fb.fillet_edgeuse(eu).map_err(h)?.fillet_geom();
|
||||
o.push(g.map_or(-1.0, |g| f64::from(g.0)));
|
||||
// index_of(geoms, ..): -1 for a geometry that is not the Model's
|
||||
o.push(
|
||||
g.filter(|g| (g.0 as usize) < self.geom_count)
|
||||
.map_or(-1.0, |g| f64::from(g.0)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// the shim's `geoms` (the Model's own geometries)
|
||||
let gs = self.fb.geoms();
|
||||
let gs = gs.get(..self.geom_count).unwrap_or(gs);
|
||||
o.push(gs.len() as f64);
|
||||
for g in gs {
|
||||
o.push(g.fillet_geom_type() as i64 as f64);
|
||||
|
||||
@@ -1279,6 +1279,27 @@ impl UnitDiff for Runner {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// HANG-29: `--fuzz 30000` fuzz09135, `RailRail` whose original face has a control
|
||||
/// point of `-1e300` (no native result in 60 s). Both sides refuse it.
|
||||
#[test]
|
||||
fn rail_rail_refuses_an_original_face_in_the_overflow_range() {
|
||||
let recorded: Value = serde_json::from_str(include_str!(
|
||||
"../../tests/fuzz_regressions/blend.fillet_geom_p2.json"
|
||||
))
|
||||
.unwrap();
|
||||
for recorded in recorded["cases"].as_array().unwrap() {
|
||||
let case = Case {
|
||||
unit: "blend.fillet_geom_p2".into(),
|
||||
name: recorded["name"].as_str().unwrap().into(),
|
||||
source: crate::case::CaseSource::Inline,
|
||||
inputs: recorded["inputs"].clone(),
|
||||
};
|
||||
for error in [Runner.run_cpp(&case), Runner.run_rust(&case)] {
|
||||
assert!(error.is_err(), "{}: {error:?}", case.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tallies the Rust outcome of every case per query (status `0`, an upstream error
|
||||
/// status, a deferral): with no failed case, the passes' success-path / error-path
|
||||
/// split. `CASE_SEEDS` sets the generated count.
|
||||
|
||||
@@ -29,8 +29,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use solidcore::blend::fillet_custom_solver::{OffsetSurface, SurfaceId};
|
||||
use solidcore::blend::fillet_corner::p3::{FilletCorner, FilletCornerKind};
|
||||
use solidcore::blend::fillet_custom_solver::{OffsetSurface, SurfaceId};
|
||||
use solidcore::blend::fillet_executive::p3::ErrorInfo;
|
||||
use solidcore::blend::fillet_geom::p1::{
|
||||
CornerLookup, FilStatus, FilletBrep, FilletCornerId, FilletEdgeType, FilletEdgeuse, FilletGeomId,
|
||||
FilletGeomType, FilletSolverId, FilletVertex, FilletVertexType, FilletVertexuse,
|
||||
@@ -987,6 +988,7 @@ fn rust(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
let mut bases: Vec<FilletSolver> = m.solvers.iter().map(|s| s.base.clone()).collect();
|
||||
let corners = shim_corners(&m)?;
|
||||
let corners = CornerLookup::table(&corners);
|
||||
let mut error_info = ErrorInfo::default();
|
||||
let res = match &i.op {
|
||||
OpIn::Export => Ok(()),
|
||||
OpIn::CornerVert { v } => {
|
||||
@@ -1000,12 +1002,27 @@ fn rust(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
OpIn::CornerEdge { e } => {
|
||||
let e = *m.edges.get(*e).ok_or_else(|| harness("edge"))?;
|
||||
// the shim refuses the part 6 calculators (its corners carry no data)
|
||||
m.fb.calc_corner_edge_geom(&k, e, corners, &mut bases, &mut tg.brep)
|
||||
m.fb.calc_corner_edge_geom_with_error_info(
|
||||
&k,
|
||||
e,
|
||||
corners,
|
||||
&mut bases,
|
||||
&mut tg.brep,
|
||||
Some(&mut error_info),
|
||||
)
|
||||
}
|
||||
OpIn::FilletIntFillet { e } => {
|
||||
let e = *m.edges.get(*e).ok_or_else(|| harness("edge"))?;
|
||||
let corner = m.fb.fillet_edge(e).map_err(h)?.fillet_corner();
|
||||
m.fb.calc_edge_fillet_int_fillet(&k, e, corner, corners, &bases, &tg.brep)
|
||||
m.fb.calc_edge_fillet_int_fillet_with_error_info(
|
||||
&k,
|
||||
e,
|
||||
corner,
|
||||
corners,
|
||||
&bases,
|
||||
&tg.brep,
|
||||
Some(&mut error_info),
|
||||
)
|
||||
}
|
||||
OpIn::ReCalc {
|
||||
g,
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
//! and UVs can be given relative to the target (a fraction of an edge's interval or of a
|
||||
//! face's surface domain); both sides get the resolved values.
|
||||
//!
|
||||
//! An op that evaluates a target face surface or edge curve of an analytic kind (cone,
|
||||
//! cylinder, torus, sphere, surface of revolution or extrusion; ellipse, circle and the
|
||||
//! other analytic curves) is deferred under `harness:analytic-geometry-export`: the
|
||||
//! shared B-rep export carries those as their B-spline part, so the Rust side would
|
||||
//! evaluate different geometry (`plan/findings.md`).
|
||||
//! Analytic target geometry the shared B-rep export carries exactly (cone, cylinder,
|
||||
//! sphere, torus, surfaces of revolution and extrusion rebuilt from their upstream
|
||||
//! database record; lossless ellipses and circles) is compared as its own kind, through
|
||||
//! `ImportTypes::inexact_surface` / `inexact_curve`. An op that evaluates a face surface
|
||||
//! or edge curve the import still holds only as its B-spline part is deferred under
|
||||
//! `harness:analytic-geometry-export` (`plan/findings.md`).
|
||||
//!
|
||||
//! `SmFilletGeom::TrimOriginalSurfaces` is not compared: it needs a fillet B-rep and the
|
||||
//! executive's topology intersector mates the shim does not build; its Rust port is
|
||||
@@ -32,6 +33,7 @@ use serde_json::Value;
|
||||
|
||||
use solidcore::blend::fillet_corner::p3::{FilletCorner, FilletCornerKind};
|
||||
use solidcore::blend::fillet_custom_solver::{OffsetSurface, SurfaceId};
|
||||
use solidcore::blend::fillet_executive::p3::ErrorInfo;
|
||||
use solidcore::blend::fillet_geom::p1::{
|
||||
FilStatus, FilletBrep, FilletCornerId, FilletEdgeType, FilletEdgeuse,
|
||||
FilletErrorType, FilletGeom, FilletGeomId, FilletGeomType, FilletSolverId, FilletVertex,
|
||||
@@ -39,7 +41,7 @@ use solidcore::blend::fillet_geom::p1::{
|
||||
};
|
||||
use solidcore::blend::fillet_geom::p3::new_solver_fillet_geom;
|
||||
use solidcore::blend::fillet_geom::p6::{
|
||||
join_two_fillet_verts_by_curves, move_tsp,
|
||||
join_two_fillet_verts_by_curves, move_tsp, GeomErrorObject,
|
||||
};
|
||||
use solidcore::blend::fillet_solver::p1::{
|
||||
FilletExecutiveRef, FilletSolver, OffsetSurfaceSlot,
|
||||
@@ -515,15 +517,14 @@ impl Target {
|
||||
self.counts[9]
|
||||
}
|
||||
|
||||
/// Whether target face `f`'s surface is imported exactly.
|
||||
/// Whether target face `f`'s surface is imported exactly (a plane, a B-spline surface
|
||||
/// or an analytic surface rebuilt from its upstream database record).
|
||||
fn exact_face(&self, f: Option<FaceId>) -> bool {
|
||||
f.and_then(|f| {
|
||||
f.is_none_or(|f| {
|
||||
self.types
|
||||
.face_surface_types
|
||||
.get(f.index() as usize)
|
||||
.copied()
|
||||
.inexact_surface(f.index() as usize)
|
||||
.is_none_or(|t| EXACT_SURFACES.contains(&t))
|
||||
})
|
||||
.is_none_or(|t| t < 0 || EXACT_SURFACES.contains(&t))
|
||||
}
|
||||
|
||||
/// Whether target edge `e`'s curve is imported exactly.
|
||||
@@ -1437,6 +1438,15 @@ fn inexact(i: &Inputs, tg: &Target) -> Result<Option<Output>, HarnessError> {
|
||||
}
|
||||
ok
|
||||
}
|
||||
OpIn::RailEuIntersect { eu, .. } => {
|
||||
if let Some(eu) = tg.edgeuse(*eu) {
|
||||
let e = teu::edge(&tg.brep, eu).map_err(h)?;
|
||||
let f = teu::face(&tg.brep, eu).map_err(h)?;
|
||||
tg.exact_edge(e) && tg.exact_face(f)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
Ok((!exact).then(|| {
|
||||
@@ -1646,7 +1656,7 @@ fn rust(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
};
|
||||
// the shim's SmTsectPnt t
|
||||
let mut tsect = TsectPnt::default();
|
||||
let r = m.fb.rail_edgeuse_intersect(
|
||||
let r = m.fb.rail_edgeuse_intersect_with_geometry(
|
||||
&k,
|
||||
g,
|
||||
&mut solver,
|
||||
@@ -1667,7 +1677,8 @@ fn rust(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
}
|
||||
OpIn::RecordError { g, etype } => {
|
||||
let g = geom(&m, *g)?;
|
||||
let etype = match etype {
|
||||
let code = *etype;
|
||||
let etype = match code {
|
||||
0 => FilletErrorType::Ok,
|
||||
1 => FilletErrorType::ExecUninitialized,
|
||||
2 => FilletErrorType::InvalidInput,
|
||||
@@ -1677,7 +1688,64 @@ fn rust(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
6 => FilletErrorType::BadTrim,
|
||||
_ => FilletErrorType::BadMerge,
|
||||
};
|
||||
m.fb.record_fillet_error(g, etype, None, None, None, None, None, &m.solvers)
|
||||
let filleted_edge = (tg.n_e() > 0 && (code & 1) != 0).then(|| EdgeId::from_index(0));
|
||||
let target_face = (tg.n_f() > 0 && (code & 2) != 0).then(|| FaceId::from_index(0));
|
||||
let rail = if (code & 4) != 0 {
|
||||
m.fb.geom(g).map_err(h)?.rail(0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let fillet_surface = m
|
||||
.fb
|
||||
.geom(g)
|
||||
.map_err(h)?
|
||||
.fillet_surface()
|
||||
.map_err(h)?
|
||||
.map(|_| GeomErrorObject::FilletSurface(g));
|
||||
let comment = ((code & 1) != 0).then_some("SmFilletGeom::RecordFilletError");
|
||||
let mut info = ErrorInfo::default();
|
||||
let r = m.fb.record_fillet_error_in(
|
||||
g,
|
||||
etype,
|
||||
filleted_edge,
|
||||
target_face,
|
||||
rail,
|
||||
fillet_surface,
|
||||
comment,
|
||||
&m.solvers,
|
||||
Some(&mut info),
|
||||
);
|
||||
if r.is_ok() {
|
||||
results.push(f64::from(info.code));
|
||||
results.push(f64::from(info.status));
|
||||
results.push(f64::from(info.edge_type));
|
||||
results.push(match info.edge {
|
||||
Some(GeomErrorObject::FilletedEdge(e)) => f64::from(e.index()),
|
||||
_ => -1.0,
|
||||
});
|
||||
results.push(match info.side_face {
|
||||
Some(GeomErrorObject::SideFace(f)) => f64::from(f.index()),
|
||||
_ => -1.0,
|
||||
});
|
||||
results.push(info.edges.len() as f64);
|
||||
for item in &info.edges {
|
||||
let idx = match item {
|
||||
Some(GeomErrorObject::FilletEdge(e)) => m
|
||||
.edges
|
||||
.iter()
|
||||
.position(|&x| x == *e)
|
||||
.map_or(-1.0, |i| i as f64),
|
||||
_ => -1.0,
|
||||
};
|
||||
results.push(idx);
|
||||
}
|
||||
results.push(info.surfaces.len() as f64);
|
||||
for item in &info.surfaces {
|
||||
results.push(b(matches!(item, Some(GeomErrorObject::FilletSurface(_)))));
|
||||
}
|
||||
results.push(info.comment.len() as f64);
|
||||
}
|
||||
r
|
||||
}
|
||||
};
|
||||
if let Err(e) = &res {
|
||||
@@ -2540,7 +2608,7 @@ fn gen_move_tsp(rng: &mut Rng) -> Inputs {
|
||||
}
|
||||
}
|
||||
|
||||
/// `RailEdgeuseIntersect` and `RecordFilletError` (both reach unported callees).
|
||||
/// `RailEdgeuseIntersect` and `RecordFilletError`.
|
||||
fn gen_deferred(rng: &mut Rng) -> Inputs {
|
||||
let mut vertices = Vec::new();
|
||||
let mut edges = Vec::new();
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
//! wrong edge kind, jittered points; the C++ stores into a fresh `SmFilletGeom` with two
|
||||
//! rails, the Rust side compares the built parts, [`FilletSolver::cylinder_fillet_geom_parts`]
|
||||
//! etc.), `GetFilletCorner` above index 1 (`corner`) and `RecordFilletError`
|
||||
//! (`record`: the shim skips it, upstream reaches the `NULL` executive; the Rust side
|
||||
//! defers at `SmFilletGeom::GetFilletSurface`, unit `blend.fillet_geom_p1`).
|
||||
//! (`record`: compares the recorded `SmFilletErrorInfo` fields across rail-0, WR-190
|
||||
//! rail-1, dual-rail deduplication and empty-tsect modes).
|
||||
//!
|
||||
//! B-rep cases build the C++ B-rep, export it through unit `topology.edgeuse_p1` and
|
||||
//! import the same records on the Rust side; entity selectors are resolved modulo the
|
||||
@@ -37,14 +37,16 @@ use serde_json::Value;
|
||||
use solidcore::blend::fillet_custom_solver::{
|
||||
InitialValues, JacobianLoad, OffsetSurface, SurfaceId,
|
||||
};
|
||||
use solidcore::blend::fillet_executive::p3::ErrorInfo;
|
||||
use solidcore::blend::fillet_geom::p1::{
|
||||
FilStatus, FilletBrep, FilletEdgeType, FilletEdgeuse, FilletGeom, FilletGeomId, FilletGeomType,
|
||||
FilletVertex, FilletVertexType, FilletVertexuse,
|
||||
FilStatus, FilletBrep, FilletEdgeType, FilletEdgeuse, FilletErrorType, FilletGeom,
|
||||
FilletGeomId, FilletGeomType, FilletVertex, FilletVertexType, FilletVertexuse,
|
||||
};
|
||||
use solidcore::blend::fillet_solver::p1::{
|
||||
FilletExecutiveRef, FilletSolver, FilletSolverOps, FilletSurfaceGenerator,
|
||||
OffsetSurfaceSlot,
|
||||
};
|
||||
use solidcore::blend::fillet_solver::p2::SolverErrorObject;
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::curve::ellipse::Ellipse;
|
||||
use solidcore::curve::line::Line;
|
||||
@@ -1190,6 +1192,7 @@ fn rust_run(i: &Inputs, o: &mut Vec<f64>) -> Result<Result<(), SolidError>, Harn
|
||||
return Err(harness("B-rep failed on the Rust side only"));
|
||||
};
|
||||
o.push(0.0);
|
||||
let h = |e: SolidError| harness(e.to_string());
|
||||
let mut fs = solver_of(
|
||||
cfg,
|
||||
&Surfs {
|
||||
@@ -1199,18 +1202,134 @@ fn rust_run(i: &Inputs, o: &mut Vec<f64>) -> Result<Result<(), SolidError>, Harn
|
||||
orient: [1.0, 1.0],
|
||||
},
|
||||
)?;
|
||||
fs.base.edgeuses = [eu_id(sel(*eu, n_eu(&c))), None];
|
||||
// the executive is not modelled: the report to it is the stop
|
||||
// (`SmFilletExecutive::NoteFilletError`, deferred)
|
||||
let eu0_idx = sel(*eu, n_eu(&c));
|
||||
let eu0 = eu_id(eu0_idx).ok_or_else(|| harness("record edge use"))?;
|
||||
let filleted = solidcore::topology::edgeuse::p1::edge(&c.brep, eu0).map_err(h)?;
|
||||
fs.base.edgeuses = [Some(eu0), None];
|
||||
fs.base.status = if eu0_idx % 2 == 0 {
|
||||
FilStatus::SurfTracingFailure
|
||||
} else {
|
||||
FilStatus::Unprocessed
|
||||
};
|
||||
let err_code = if eu0_idx % 3 == 0 {
|
||||
FilletErrorType::EdgeProblem
|
||||
} else {
|
||||
FilletErrorType::BadInt
|
||||
};
|
||||
fs.base.executive = Some(FilletExecutiveRef);
|
||||
let (fb, _) = fillet_geom_model(&k, None)?;
|
||||
let pts = [TsectPnt::default()];
|
||||
match fs
|
||||
.base
|
||||
.record_fillet_error(&c.brep, &fb, 5, &pts, "difftest")
|
||||
{
|
||||
|
||||
let (mut fb, g) = fillet_geom_model(&k, None)?;
|
||||
fs.base.fillet_geoms = vec![g];
|
||||
let rail0 = fb
|
||||
.geom(g)
|
||||
.map_err(h)?
|
||||
.rail(0)
|
||||
.ok_or_else(|| harness("rail 0"))?;
|
||||
let rail1 = fb
|
||||
.geom(g)
|
||||
.map_err(h)?
|
||||
.rail(1)
|
||||
.ok_or_else(|| harness("rail 1"))?;
|
||||
|
||||
let mut pts = Vec::new();
|
||||
if cfg.target < -1.5 {
|
||||
let r0 = Curve::from(
|
||||
solidcore::nurbs::b_spline_curve::BSplineCurve::create_line_segment(
|
||||
3,
|
||||
Vec3::ZERO,
|
||||
Vec3::new(1.0, 0.0, 0.0),
|
||||
)
|
||||
.map_err(h)?,
|
||||
);
|
||||
fb.brep_mut().edge_mut(rail0).map_err(h)?.curve = Some(r0);
|
||||
let mut t = TsectPnt::default();
|
||||
*t.crv_pos_mut() = Vec3::new(cfg.target, cfg.radius, fs.base.approx_tol_3d);
|
||||
pts.push(t);
|
||||
} else if cfg.target < 0.0 {
|
||||
let r1 = Curve::from(
|
||||
solidcore::nurbs::b_spline_curve::BSplineCurve::create_line_segment(
|
||||
3,
|
||||
Vec3::ZERO,
|
||||
Vec3::new(0.0, 1.0, 0.0),
|
||||
)
|
||||
.map_err(h)?,
|
||||
);
|
||||
fb.brep_mut().edge_mut(rail1).map_err(h)?.curve = Some(r1);
|
||||
let mut plane = solidcore::surface::plane::Plane::from_normal(
|
||||
Vec3::ZERO,
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
);
|
||||
plane.make_nurb().map_err(h)?;
|
||||
fb.geom_mut(g)
|
||||
.map_err(h)?
|
||||
.set_fillet_surface(Some(Surface::from(plane)));
|
||||
let cl = solidcore::nurbs::b_spline_curve::BSplineCurve::create_line_segment(
|
||||
3,
|
||||
Vec3::ZERO,
|
||||
Vec3::new(2.0, 0.0, 0.0),
|
||||
)
|
||||
.map_err(h)?;
|
||||
fb.geom_mut(g).map_err(h)?.center_line_curve = Some(Curve::from(cl));
|
||||
let mut t = TsectPnt::default();
|
||||
*t.crv_pos_mut() = Vec3::new(cfg.target, -cfg.radius, fs.base.approx_tol_3d);
|
||||
pts.push(t);
|
||||
} else if cfg.target < 1.5 {
|
||||
let r0 = Curve::from(
|
||||
solidcore::nurbs::b_spline_curve::BSplineCurve::create_line_segment(
|
||||
3,
|
||||
Vec3::ZERO,
|
||||
Vec3::new(1.0, 0.0, 0.0),
|
||||
)
|
||||
.map_err(h)?,
|
||||
);
|
||||
let r1 = Curve::from(
|
||||
solidcore::nurbs::b_spline_curve::BSplineCurve::create_line_segment(
|
||||
3,
|
||||
Vec3::ZERO,
|
||||
Vec3::new(0.0, 1.0, 0.0),
|
||||
)
|
||||
.map_err(h)?,
|
||||
);
|
||||
fb.brep_mut().edge_mut(rail0).map_err(h)?.curve = Some(r0);
|
||||
fb.brep_mut().edge_mut(rail1).map_err(h)?.curve = Some(r1);
|
||||
pts.push(TsectPnt::default());
|
||||
}
|
||||
|
||||
let mut ei = ErrorInfo::default();
|
||||
match fs.base.record_fillet_error_in(
|
||||
&c.brep,
|
||||
&fb,
|
||||
err_code,
|
||||
&pts,
|
||||
Some("difftest"),
|
||||
Some(&mut ei),
|
||||
) {
|
||||
Ok(()) => {
|
||||
o.push(-2.0);
|
||||
o.push(f64::from(ei.code));
|
||||
o.push(f64::from(ei.status));
|
||||
o.push(f64::from(ei.edge_type));
|
||||
o.push(b(ei.edge == filleted.map(SolverErrorObject::FilletedEdge)));
|
||||
o.push(ei.surfaces.len() as f64);
|
||||
o.push(b(
|
||||
ei.surfaces.as_slice() == [Some(SolverErrorObject::FilletSurface(g))]
|
||||
));
|
||||
o.push(ei.curves.len() as f64);
|
||||
o.push(b(
|
||||
ei.curves.as_slice() == [Some(SolverErrorObject::CenterLineCurve(g))]
|
||||
));
|
||||
o.push(ei.edges.len() as f64);
|
||||
o.push(b(
|
||||
ei.edges.as_slice() == [Some(SolverErrorObject::FilletEdge(rail0))]
|
||||
));
|
||||
let loc = ei.location.unwrap_or(Vec3::UNDEF);
|
||||
o.push(b(loc.is_initialized()));
|
||||
if loc.is_initialized() {
|
||||
o.extend([loc.x, loc.y, loc.z]);
|
||||
}
|
||||
o.push(ei.comment.len() as f64);
|
||||
for byte in ei.comment.bytes() {
|
||||
o.push(f64::from(byte));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
|
||||
@@ -29,8 +29,10 @@ use solidcore::geom::a_object::BooleanOperation;
|
||||
use solidcore::geom::attribute::{ids, Attribute, AttributeStore, Behavior, ObjectKey};
|
||||
use solidcore::geom::context::MarkLock;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::math::Point3;
|
||||
use solidcore::topology::brep_cache::{arena_identity, CacheObject};
|
||||
use solidcore::topology::{brep::p1 as bp, Brep, FaceId};
|
||||
use solidcore::topology::topology_traverser::Marks;
|
||||
use solidcore::topology::{brep::p1 as bp, Brep, FaceId, NamingScheme};
|
||||
use solidcore::SolidError;
|
||||
use solidcore_ffi::units::boolean_merge_p3 as ffi;
|
||||
|
||||
@@ -125,7 +127,7 @@ fn engine_output(status: i64, manifold: bool, lamina: u64, result: bool) -> Outp
|
||||
/// The engine mirrors the native `EngineAccess`: the context-only constructor's
|
||||
/// members (`KernelManifoldIntersector::new`), the same explicit tolerances and
|
||||
/// `SetMergeOptions`, manifold flag true and lamina mode 0 before the call.
|
||||
fn engine_probe(i: &Inputs, pool: &mut Pool) -> Result<Output, HarnessError> {
|
||||
fn engine_probe(k: &mut Kernel, i: &Inputs, pool: &mut Pool) -> Result<Output, HarnessError> {
|
||||
let mut breps = std::mem::take(&mut pool.breps).into_iter();
|
||||
let mut primary = if i.a.is_empty() { None } else { breps.next() };
|
||||
let mut other = if i.b.is_empty() { None } else { breps.next() };
|
||||
@@ -139,11 +141,10 @@ fn engine_probe(i: &Inputs, pool: &mut Pool) -> Result<Output, HarnessError> {
|
||||
state: ManifoldState::default(),
|
||||
attributes: AttributeStore::default(),
|
||||
});
|
||||
let mut k = Kernel::default();
|
||||
let mut manifold = true;
|
||||
let mut lamina = 0;
|
||||
let result = port::non_manifold_boolean(
|
||||
&mut k,
|
||||
k,
|
||||
&mut driver,
|
||||
&mut primary,
|
||||
&mut other,
|
||||
@@ -390,44 +391,35 @@ struct Pool {
|
||||
engine: Option<port::MergeEngine>,
|
||||
}
|
||||
impl Pool {
|
||||
fn load(i: &Inputs) -> Result<Self, HarnessError> {
|
||||
fn load(k: &mut Kernel, i: &Inputs) -> Result<Self, HarnessError> {
|
||||
let mut breps = Vec::new();
|
||||
|
||||
for operand in i.a.iter().chain(&i.b) {
|
||||
let exported = ffi::eval(&encode(&Inputs {
|
||||
family: 5,
|
||||
operation: 0,
|
||||
options: None,
|
||||
a: vec![operand.clone()],
|
||||
b: Vec::new(),
|
||||
tree: Vec::new(),
|
||||
expected_refusal: None,
|
||||
}))
|
||||
.map_err(|error| harness(&error))?;
|
||||
// export_graph starts at validate_only; vertexuse import expects a
|
||||
// leading success tag. Geometry comes only from fixture construction.
|
||||
let mut tagged = vec![0.0];
|
||||
tagged.extend(&exported);
|
||||
let (mut brep, _) = super::topology_vertexuse::import(&tagged)?;
|
||||
// The vertexuse importer predates canonical arena owner keys. Preserve
|
||||
// its native owned/unowned flag, but translate existing owners so
|
||||
// canonical GetSurfaces sees the same standalone surfaces as C++.
|
||||
let faces: Vec<_> = brep.faces().ids().collect();
|
||||
for face in faces {
|
||||
if let Some(surface) = brep
|
||||
.face_mut(face)
|
||||
let [x, y, z] = operand.point;
|
||||
let point = Point3::new(x, y, z);
|
||||
let mut brep = if let Some([dx, dy, dz]) = operand.size {
|
||||
solidcore::primitive::api_primitives::create_box(k, point, dx, dy, dz)
|
||||
.map_err(|error| harness(&error))?
|
||||
.surface
|
||||
.as_mut()
|
||||
{
|
||||
if surface.owner().is_some() {
|
||||
surface.set_owner(Some(arena_identity(CacheObject::Face(face))));
|
||||
}
|
||||
}
|
||||
// a new owner key drops the cached face tolerance: record it again
|
||||
brep.sync_face_surface_tolerance(face)
|
||||
.map_err(|error| harness(&error))?;
|
||||
}
|
||||
} else {
|
||||
let mut b = empty_brep(
|
||||
k,
|
||||
solidcore::geom::tol::USE_DEFAULT,
|
||||
solidcore::geom::tol::USE_DEFAULT,
|
||||
None,
|
||||
)
|
||||
.map_err(|error| harness(&error))?;
|
||||
let inf = b.infinite_region();
|
||||
let mut state = solidcore::topology::brep::p2::ArenaNotifyState::new(&b);
|
||||
solidcore::topology::brep::p9::make_shell_vertex(
|
||||
&mut b,
|
||||
inf,
|
||||
point,
|
||||
&mut state.env(Some(&mut k.context)),
|
||||
)
|
||||
.map_err(|error| harness(&error))?;
|
||||
b
|
||||
};
|
||||
brep.use_arena_naming().map_err(|error| harness(&error))?;
|
||||
breps.push(brep);
|
||||
}
|
||||
Ok(Self {
|
||||
@@ -657,6 +649,14 @@ impl PiecewiseAccess for Pool {
|
||||
(self.primary, self.other)
|
||||
}
|
||||
fn set_operands(&mut self, a: Option<usize>, b: Option<usize>) {
|
||||
if let Some(engine) = self.engine.as_mut() {
|
||||
if self.primary != a {
|
||||
engine.state.primary_marks = Marks::default();
|
||||
}
|
||||
if self.other != b {
|
||||
engine.state.other_marks = Marks::default();
|
||||
}
|
||||
}
|
||||
self.primary = a;
|
||||
self.other = b;
|
||||
}
|
||||
@@ -679,7 +679,8 @@ impl PiecewiseAccess for Pool {
|
||||
a,
|
||||
b,
|
||||
&mut engine.attributes,
|
||||
)
|
||||
)?;
|
||||
engine.adopt_operand_owners(Some(a), Some(b))
|
||||
})
|
||||
}
|
||||
fn clear_relationships(&mut self) -> solidcore::Result<()> {
|
||||
@@ -752,6 +753,7 @@ impl PiecewiseAccess for Pool {
|
||||
let tolerance = self.brep(source)?.tolerance();
|
||||
set_tolerance(k, &mut result, tolerance, true, true)?;
|
||||
result.set_editing_enabled(true);
|
||||
result.set_naming(NamingScheme::Arena);
|
||||
let id = self.breps.len();
|
||||
self.breps.push(result); // append only: a handle is never reused
|
||||
Ok(id)
|
||||
@@ -784,7 +786,8 @@ impl PiecewiseAccess for Pool {
|
||||
lock.mark_type1(),
|
||||
lock.mark_type2(),
|
||||
&mut keep,
|
||||
)
|
||||
)?;
|
||||
engine.adopt_operand_owners(Some(a), Some(b))
|
||||
})
|
||||
}
|
||||
fn release_marks(&mut self, k: &mut Kernel, lock: &mut MarkLock) -> solidcore::Result<()> {
|
||||
@@ -907,11 +910,11 @@ fn rust(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
return Err(harness("fixture-only family is not a unit operation"));
|
||||
}
|
||||
ffi::validate(&encode(i)).map_err(|error| harness(&error))?;
|
||||
let mut pool = Pool::load(i)?;
|
||||
if i.family == ENGINE_PROBE {
|
||||
return engine_probe(i, &mut pool);
|
||||
}
|
||||
let mut k = Kernel::default();
|
||||
let mut pool = Pool::load(&mut k, i)?;
|
||||
if i.family == ENGINE_PROBE {
|
||||
return engine_probe(&mut k, i, &mut pool);
|
||||
}
|
||||
let opts = i.options.map(options);
|
||||
let mut values = vec![0.0];
|
||||
let result: solidcore::Result<()> = match i.family {
|
||||
@@ -1161,7 +1164,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn single_success_records_compare_rust_geometry_controls_and_nulls() {
|
||||
let mut pool = Pool::load(&inputs()).unwrap();
|
||||
let mut pool = Pool::load(&mut Kernel::default(), &inputs()).unwrap();
|
||||
let graph = fixture_graph();
|
||||
let mut native = vec![0.0, 1.0, 1.0, 1.0, 0.0, graph.len() as f64];
|
||||
native.extend(&graph);
|
||||
@@ -1197,7 +1200,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn list_success_records_compare_actual_outputs_and_surface_count() {
|
||||
let pool = Pool::load(&inputs()).unwrap();
|
||||
let pool = Pool::load(&mut Kernel::default(), &inputs()).unwrap();
|
||||
let graph = fixture_graph();
|
||||
let mut native = vec![0.0, 1.0, -1.0, graph.len() as f64];
|
||||
native.extend(&graph);
|
||||
@@ -1236,8 +1239,8 @@ mod tests {
|
||||
fn engine(pool: &Pool) -> &KernelManifoldIntersector {
|
||||
&pool.engine.as_ref().unwrap().intersector
|
||||
}
|
||||
let mut pool = Pool::load(&inputs()).unwrap();
|
||||
let mut k = Kernel::default();
|
||||
let mut pool = Pool::load(&mut k, &inputs()).unwrap();
|
||||
pool.construct(&mut k, &mut Some(0), &mut Some(1), 0.0, 0.0, None)
|
||||
.unwrap();
|
||||
assert_eq!(pool.surfaces(0).unwrap().len(), 6);
|
||||
@@ -1275,8 +1278,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn piecewise_dependency_errors_name_the_next_missing_owner() {
|
||||
let mut pool = Pool::load(&inputs()).unwrap();
|
||||
let mut k = Kernel::default();
|
||||
let mut pool = Pool::load(&mut k, &inputs()).unwrap();
|
||||
// without SmMerge's constructor the pool holds no m_vTI
|
||||
assert!(matches!(
|
||||
pool.clear_relationships(),
|
||||
|
||||
@@ -102,7 +102,7 @@ use solidcore::topology::topology::Topology;
|
||||
use solidcore::topology::vertex as vx;
|
||||
use solidcore::query::brep::{BrepIdentity, BrepQuery};
|
||||
use solidcore::topology::{
|
||||
Brep, BrepId, EdgeId, EdgeuseId, FaceId, LoopId, RegionId, VertexId, VertexuseId,
|
||||
Brep, BrepId, EdgeId, EdgeuseId, FaceId, LoopId, LoopuseId, RegionId, VertexId, VertexuseId,
|
||||
};
|
||||
use solidcore::{EntityRef, SolidError};
|
||||
use solidcore_ffi::units::boolean_topology_intersector_p1::{self as ffi, Op};
|
||||
@@ -617,6 +617,8 @@ pub(super) const K_VERTEXUSE: u8 = 12;
|
||||
pub(super) const K_TRIM_CURVE: u8 = 13;
|
||||
/// A region (classification results).
|
||||
const K_REGION: u8 = 14;
|
||||
/// A loop use a face's trimmed surface cache names (Rust side only, never exported).
|
||||
const K_LOOPUSE: u8 = 15;
|
||||
|
||||
/// An object handle: the B-rep (`2` for the curve stores, `-1` for the intersector),
|
||||
/// the kind and the index. Ordered as the shim sorts (`brep`, `kind`, `index`). A
|
||||
@@ -819,6 +821,50 @@ impl Two {
|
||||
}
|
||||
}
|
||||
|
||||
/// `SmBrep::GlueVertices(v1, v2)` where `v1` and `v2` belong to different B-reps.
|
||||
/// Upstream notifies, reads the faces of `v2` and the shells of both vertices, then
|
||||
/// looks for a shell pair in one region (`NER(pSurvVShell)`). A region belongs to one
|
||||
/// B-rep, so a pair that shares none ends the call with `SM_ERR_NULL_POINTER`
|
||||
/// before any edit. The caller has already deleted the cache of `this`
|
||||
/// (`Notify(SM_NO_PRE_EDIT, this)`). The remaining notifications are not delivered
|
||||
/// across the two arenas: the shim registers no context callbacks, and each face of
|
||||
/// the vertex to delete was already notified `PRE_EDIT` when the vertex was made
|
||||
/// (`MakeVertexLoop` / `MakeVertexSplitEdge`), with nothing recomputing its
|
||||
/// rectangular-trim flag or trim cache in between.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Upstream`] with `SM_ERR_NULL_POINTER` (upstream's `NER`);
|
||||
/// [`SolidError::InvalidInput`] for shells of both B-reps
|
||||
/// without a region (`NULL == NULL`: upstream would go on to merge shells across
|
||||
/// B-reps) or a missing arena.
|
||||
fn glue_vertices_of_two_breps(&self, survivor: H, deleted: H) -> solidcore::Result<()> {
|
||||
let arena = |h: H| {
|
||||
usize::try_from(h.b)
|
||||
.ok()
|
||||
.and_then(|slot| self.breps.get(slot))
|
||||
.and_then(Option::as_ref)
|
||||
.ok_or_else(|| invalid("handle names an absent B-rep"))
|
||||
};
|
||||
let (surv_brep, del_brep) = (arena(survivor)?, arena(deleted)?);
|
||||
let (surv, del) = (vid(survivor), vid(deleted));
|
||||
// pVertexToDelete->GetFaces(..) (each face is notified), then GetShells for both
|
||||
vx::faces(del_brep, del)?;
|
||||
let surv_shells = vx::shells(surv_brep, surv)?;
|
||||
let del_shells = vx::shells(del_brep, del)?;
|
||||
for &first in &surv_shells {
|
||||
let region = surv_brep.shell(first)?.region;
|
||||
for &last in &del_shells {
|
||||
if region.is_none() && del_brep.shell(last)?.region.is_none() {
|
||||
return Err(invalid("shells of two B-reps share a NULL region"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(SolidError::null_pointer(
|
||||
"difftest::boolean_topology_intersector_p1::Two::glue_vertices_of_two_breps",
|
||||
))
|
||||
}
|
||||
|
||||
/// Deletes the cache of the B-rep behind `brep` (`SmBrep::Notify`'s `PRE_EDIT` /
|
||||
/// `DESTRUCTION`); the next use rebuilds it.
|
||||
pub(crate) fn delete_cache(&mut self, brep: H) -> solidcore::Result<()> {
|
||||
@@ -925,6 +971,37 @@ impl Two {
|
||||
}
|
||||
}
|
||||
|
||||
/// The handle of an entity of B-rep `b` a face's trimmed surface cache names (its
|
||||
/// faces, edges, vertices, loop uses, edge uses and vertex uses).
|
||||
fn cache_entity_h(b: i8, e: EntityRef) -> solidcore::Result<H> {
|
||||
Ok(match e {
|
||||
EntityRef::Face(f) => hd(b, K_FACE, f.index()),
|
||||
EntityRef::Edge(x) => hd(b, K_EDGE, x.index()),
|
||||
EntityRef::Vertex(v) => hd(b, K_VERTEX, v.index()),
|
||||
EntityRef::Loopuse(lu) => hd(b, K_LOOPUSE, lu.index()),
|
||||
EntityRef::Edgeuse(eu) => hd(b, K_EDGEUSE, eu.index()),
|
||||
EntityRef::Vertexuse(vu) => hd(b, K_VERTEXUSE, vu.index()),
|
||||
_ => return Err(invalid("entity a surface cache does not name")),
|
||||
})
|
||||
}
|
||||
|
||||
/// The entity of B-rep `b` a surface cache handle names (inverse of
|
||||
/// [`cache_entity_h`]).
|
||||
fn cache_h_entity(b: i8, h: H) -> solidcore::Result<EntityRef> {
|
||||
if h.b != b {
|
||||
return Err(invalid("surface cache handle of another B-rep"));
|
||||
}
|
||||
Ok(match h.kind {
|
||||
K_FACE => EntityRef::Face(fid(h)),
|
||||
K_EDGE => EntityRef::Edge(eid(h)),
|
||||
K_VERTEX => EntityRef::Vertex(vid(h)),
|
||||
K_LOOPUSE => EntityRef::Loopuse(LoopuseId::from_index(h.i)),
|
||||
K_EDGEUSE => EntityRef::Edgeuse(EdgeuseId::from_index(h.i)),
|
||||
K_VERTEXUSE => EntityRef::Vertexuse(VertexuseId::from_index(h.i)),
|
||||
_ => return Err(invalid("handle a surface cache does not name")),
|
||||
})
|
||||
}
|
||||
|
||||
impl ClassGeometry<H> for Two {
|
||||
fn curve(&self, o: H) -> Option<&SmCurve> {
|
||||
match o.kind {
|
||||
@@ -1462,12 +1539,10 @@ impl solidcore::topology::proto_topology::p3::BrepGlue<H> for Two {
|
||||
&& v1.b != v2.b
|
||||
&& (v1.b == brep.b || v2.b == brep.b)
|
||||
{
|
||||
// UNPORTED(topology.brep_p5): SmBrep::GlueVertices of vertices of two B-reps --
|
||||
// justified: topology::brep::p5::glue_vertices takes one B-rep arena
|
||||
return Err(SolidError::unported(
|
||||
"SmBrep::GlueVertices (vertices of two B-reps)",
|
||||
"topology.brep_p5",
|
||||
));
|
||||
// Notify(SM_NO_PRE_EDIT, this) deletes the cache of the B-rep the call is on,
|
||||
// before anything can fail
|
||||
self.delete_cache(brep)?;
|
||||
return self.glue_vertices_of_two_breps(v1, v2);
|
||||
}
|
||||
if brep.kind != K_BREP
|
||||
|| v1.kind != K_VERTEX
|
||||
@@ -1968,6 +2043,30 @@ impl IntersectorBrep<H> for Two {
|
||||
fn topology_objects(&self) -> &dyn TopologyObjects<H> {
|
||||
self
|
||||
}
|
||||
fn face_cache_target(&mut self, face: H) -> solidcore::Result<ti::FaceCacheTarget<'_, H>> {
|
||||
if face.kind != K_FACE {
|
||||
return Err(invalid("not a face handle"));
|
||||
}
|
||||
let b = face.b;
|
||||
let surface = self
|
||||
.face_surface_h(face)
|
||||
.flatten()
|
||||
.ok_or_else(|| invalid("face without surface"))?;
|
||||
Ok(ti::FaceCacheTarget {
|
||||
brep: self.brep_m(face)?,
|
||||
face: fid(face),
|
||||
surface,
|
||||
handles: Box::new(move |e| cache_entity_h(b, e)),
|
||||
entities: Box::new(move |h| cache_h_entity(b, h)),
|
||||
})
|
||||
}
|
||||
fn face_entity_handles(&self, face: H) -> solidcore::Result<ti::EntityHandles<H>> {
|
||||
if face.kind != K_FACE {
|
||||
return Err(invalid("not a face handle"));
|
||||
}
|
||||
let b = face.b;
|
||||
Ok(Box::new(move |e| cache_entity_h(b, e)))
|
||||
}
|
||||
fn brep_delete_face(&mut self, k: &mut Kernel, brep: H, face: H) -> solidcore::Result<()> {
|
||||
if brep.kind != K_BREP || face.b != brep.b {
|
||||
return Err(invalid("face is not in the B-rep"));
|
||||
@@ -5439,3 +5538,103 @@ fn edge_cases() -> Vec<(String, Value)> {
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::case::CaseSource;
|
||||
|
||||
/// HANG-29: the `--fuzz 30000` inputs whose surface or curve control point of
|
||||
/// `+-1e300` kept the C++ intersection busy past the timeout, for this unit and `_p2`
|
||||
/// (which shares this reader), and UB-166's v knot snap (`ub166` in the name: the
|
||||
/// shim refuses the B-rep). They cannot be fixtures (a harness error always fails a
|
||||
/// case), so this test keeps them: each is refused before the intersection runs.
|
||||
#[cfg(feature = "cpp")]
|
||||
#[test]
|
||||
fn ffi_refuses_surface_points_in_the_hang_range() {
|
||||
let p2 = super::super::boolean_topology_intersector_p2::Runner;
|
||||
let files: [(&str, &dyn UnitDiff); 2] = [
|
||||
(
|
||||
include_str!("../../tests/fuzz_regressions/boolean.topology_intersector_p1.json"),
|
||||
&Runner,
|
||||
),
|
||||
(
|
||||
include_str!("../../tests/fuzz_regressions/boolean.topology_intersector_p2.json"),
|
||||
&p2,
|
||||
),
|
||||
];
|
||||
for (text, runner) in files {
|
||||
let recorded: Value = serde_json::from_str(text).unwrap();
|
||||
for recorded in recorded["cases"].as_array().unwrap() {
|
||||
let case = Case {
|
||||
unit: runner.id().into(),
|
||||
name: recorded["name"].as_str().unwrap().into(),
|
||||
source: CaseSource::Inline,
|
||||
inputs: recorded["inputs"].clone(),
|
||||
};
|
||||
let error = runner.run_cpp(&case).unwrap_err().to_string();
|
||||
let expected = if case.name.contains("ub166") {
|
||||
// the shim's UB-166 refusal (SC_ERR_ARG)
|
||||
"shim_rejected_args"
|
||||
} else {
|
||||
"point out of range"
|
||||
};
|
||||
assert!(error.contains(expected), "{}: {error}", case.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod glue_tests {
|
||||
use super::*;
|
||||
use solidcore::primitive::api_primitives::create_box;
|
||||
use solidcore::topology::proto_topology::p3::BrepGlue;
|
||||
|
||||
fn two_boxes() -> (Kernel, Two) {
|
||||
let mut k = Kernel::default();
|
||||
let a = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 1.0, 1.0, 1.0).unwrap();
|
||||
let b = create_box(&mut k, Point3::new(5.0, 0.0, 0.0), 1.0, 1.0, 1.0).unwrap();
|
||||
(k, Two::new(a, Some(b)))
|
||||
}
|
||||
|
||||
/// `GlueVertices` on `this` with vertices of two B-reps: `Notify(SM_NO_PRE_EDIT, this)`
|
||||
/// deletes the cache of `this`, then `NER(pSurvVShell)` (no region is shared) edits
|
||||
/// nothing.
|
||||
#[test]
|
||||
fn glue_of_two_breps_drops_this_cache_and_returns_null_pointer() {
|
||||
for this_slot in [0i8, 1] {
|
||||
let (mut k, mut two) = two_boxes();
|
||||
two.brep_cache(hd(0, K_BREP, 0)).unwrap();
|
||||
two.brep_cache(hd(1, K_BREP, 0)).unwrap();
|
||||
let counts = |two: &Two| {
|
||||
two.breps
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|b| (b.vertices().len(), b.edges().len(), b.faces().len()))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let before = counts(&two);
|
||||
let first_vertex = |slot: usize| {
|
||||
let id = two.breps[slot].as_ref().unwrap().vertices().ids().next();
|
||||
hd(slot as i8, K_VERTEX, id.unwrap().index())
|
||||
};
|
||||
let (v0, v1) = (first_vertex(0), first_vertex(1));
|
||||
let r = two.glue_vertices(&mut k, hd(this_slot, K_BREP, 0), v0, v1, None);
|
||||
assert!(
|
||||
matches!(
|
||||
r,
|
||||
Err(SolidError::Upstream { code: solidcore::error::SM_ERR_NULL_POINTER, .. })
|
||||
),
|
||||
"this={this_slot}: {r:?}"
|
||||
);
|
||||
assert_eq!(counts(&two), before, "no topology edit");
|
||||
let cached = |i: usize| two.caches[i].get().is_some();
|
||||
assert_eq!(
|
||||
(cached(0), cached(1)),
|
||||
(this_slot != 0, this_slot != 1),
|
||||
"only the cache of `this` is deleted"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
//! them through the base curve's virtual knot queries. Every run has at least
|
||||
//! [`MIN_GENERATED`] generated cases; generated inputs the ffi wrapper rejects are
|
||||
//! redrawn. Any genuinely unported downstream call still defers the whole case.
|
||||
//!
|
||||
//! Deviations: the runner-local SMS reader validates counts below the exclusive
|
||||
//! `2^usize::BITS` bound, checks dimension and payload arithmetic and the available
|
||||
//! tokens before constructing the net, and grows storage incrementally. Valid point
|
||||
//! order, weights, knots, lazy float parsing and path-bearing errors are unchanged.
|
||||
|
||||
#[path = "fixtures/crv_on_surf_sms.rs"]
|
||||
mod sms;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -254,46 +262,13 @@ enum Inputs {
|
||||
/// `m+q+2` v knots.
|
||||
fn read_sms(path: &str) -> Result<InlineSrf, HarnessError> {
|
||||
let text = std::fs::read_to_string(path).map_err(|e| harness(format!("{path}: {e}")))?;
|
||||
let mut it = text.split_whitespace();
|
||||
let mut num = || -> Result<f64, HarnessError> {
|
||||
it.next()
|
||||
.ok_or_else(|| harness(format!("{path}: truncated")))?
|
||||
.parse::<f64>()
|
||||
.map_err(|e| harness(format!("{path}: {e}")))
|
||||
};
|
||||
let int = |v: f64| -> Result<usize, HarnessError> {
|
||||
if v.fract() != 0.0 || v < 0.0 {
|
||||
return Err(harness(format!("{path}: bad count {v}")));
|
||||
}
|
||||
Ok(v as usize)
|
||||
};
|
||||
let n = int(num()?)?;
|
||||
let m = int(num()?)?;
|
||||
let p = int(num()?)?;
|
||||
let q = int(num()?)?;
|
||||
let rat = num()? != 0.0;
|
||||
let mut net = Vec::with_capacity(n + 1);
|
||||
for _ in 0..=n {
|
||||
let mut row = Vec::with_capacity(m + 1);
|
||||
for _ in 0..=m {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rat { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let u = (0..n + p + 2)
|
||||
.map(|_| num())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let v = (0..m + q + 2)
|
||||
.map(|_| num())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let s = sms::parse_sms(&text, path)?;
|
||||
Ok(InlineSrf {
|
||||
net,
|
||||
p,
|
||||
q,
|
||||
u,
|
||||
v,
|
||||
net: s.net,
|
||||
p: s.p,
|
||||
q: s.q,
|
||||
u: s.u,
|
||||
v: s.v,
|
||||
borrow_nurb: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ enum CurveIn {
|
||||
Hermite(HermiteIn),
|
||||
Circle(CircleIn),
|
||||
Line(LineIn),
|
||||
Ellipse(EllipseIn),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -304,6 +305,13 @@ fn curve_block(v: &mut Vec<f64>, c: &CurveIn) -> Result<(), HarnessError> {
|
||||
push_line(v, l);
|
||||
}
|
||||
CurveIn::Circle(_) => return Err(harness("unresolved circle")),
|
||||
CurveIn::Ellipse(e) => {
|
||||
v.extend([3.0, 3.0]);
|
||||
v.extend_from_slice(&e.c);
|
||||
v.extend_from_slice(&e.x);
|
||||
v.extend_from_slice(&e.y);
|
||||
v.extend([e.domain[0], e.domain[1], e.radii[0], e.radii[1], b(e.inside_out)]);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -571,6 +579,9 @@ fn build(c: &CurveIn) -> Result<Curve, HarnessError> {
|
||||
))),
|
||||
CurveIn::Line(l) => Ok(Curve::from(build_line(l))),
|
||||
CurveIn::Circle(_) => Err(harness("unresolved circle")),
|
||||
CurveIn::Ellipse(e) => build_ellipse(&Kernel::default(), e)
|
||||
.map(Curve::from)
|
||||
.map_err(|e| harness(format!("ellipse: {e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1215,7 +1226,9 @@ fn range_of(c: &CurveIn) -> (f64, f64) {
|
||||
bs.u.first().copied().unwrap_or(0.0),
|
||||
bs.u.last().copied().unwrap_or(1.0),
|
||||
),
|
||||
CurveIn::Hermite(_) => (0.0, 1.0),
|
||||
// Like Hermite curves, ownerless quadratic ellipses use a normalized interval,
|
||||
// not their angular domain (NL_CrvCommon.cpp circle construction).
|
||||
CurveIn::Hermite(_) | CurveIn::Ellipse(_) => (0.0, 1.0),
|
||||
CurveIn::Line(l) => {
|
||||
let nat = build_line(l).natural_interval();
|
||||
(nat.min(), nat.max())
|
||||
@@ -1693,6 +1706,100 @@ mod tests {
|
||||
use super::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn ellipse_distance_fixture() -> Case {
|
||||
let ctx = CaseContext {
|
||||
root: std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."),
|
||||
seeds: 0,
|
||||
};
|
||||
let cases = ctx.fixture_cases("curve.curve_p4").unwrap();
|
||||
let matching: Vec<_> = cases.into_iter().filter(|c| {
|
||||
matches!(&c.source, crate::case::CaseSource::Fixture { path, .. }
|
||||
if path == "fixtures/curve.curve_p4/analytic-distance.json")
|
||||
}).collect();
|
||||
assert_eq!(matching.len(), 1);
|
||||
assert_eq!(matching[0].name, "ellipse_cp_dist_at_interior_full_jacobian");
|
||||
matching.into_iter().next().unwrap()
|
||||
}
|
||||
|
||||
fn ellipse_distance_relative(a: f64, b: f64) {
|
||||
assert!(a.is_finite() && b.is_finite());
|
||||
assert!((a - b).abs() <= 1e-9 * a.abs().max(b.abs()), "{a:?} != {b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ellipse_distance_fixture_encoding_and_source_controls() {
|
||||
use solidcore::curve::curve::p2::CurveKind;
|
||||
let case = ellipse_distance_fixture();
|
||||
let op: OpIn = case.parse().unwrap();
|
||||
assert_eq!(serde_json::to_value(&op).unwrap(), case.inputs);
|
||||
let (code, input) = cpp_input(&op).unwrap();
|
||||
assert_eq!(code, Op::CpDistAt);
|
||||
assert_eq!(input, vec![3.,3.,0.,0.,0.,1.,0.,0.,0.,1.,0.,0.,180.,2.,1.,0.,
|
||||
0.25,-0.5,0.25,1.,0.,0.5,0.1,0.2,0.3,1.]);
|
||||
ffi::validate(code, &input).unwrap();
|
||||
let OpIn::CpDistAt { curve, p, dsq, sqtol, x, jac } = &op else { panic!("wrong op") };
|
||||
assert!(*jac);
|
||||
let CurveIn::Ellipse(e) = curve else { panic!("not ellipse") };
|
||||
assert_eq!(e.domain, [0.,180.]);
|
||||
assert_eq!(range_of(curve), (0.,1.));
|
||||
let k = Kernel::default();
|
||||
let c = build(curve).unwrap();
|
||||
let CurveKind::Ellipse(actual) = c.kind() else { panic!("lost analytic kind") };
|
||||
assert!(!actual.is_circle());
|
||||
let ivl = c.natural_interval();
|
||||
assert_eq!((ivl.min(),ivl.max()), (0.,1.));
|
||||
assert!(ivl.min() < x[0] && x[0] < ivl.max());
|
||||
let pv = c.evaluate(&k, x[0], 1, true, true).unwrap();
|
||||
let pos = [pv[0].x,pv[0].y,pv[0].z];
|
||||
let tangent = [pv[1].x,pv[1].y,pv[1].z];
|
||||
assert!(pos.iter().chain(&tangent).all(|v| v.is_finite()));
|
||||
assert!(pv[1].length_squared() > 0.);
|
||||
let gap = [pos[0]-p[0],pos[1]-p[1],pos[2]-p[2]];
|
||||
let expected_f = [gap[0]*gap[0]-x[1],gap[1]*gap[1]-x[2],gap[2]*gap[2]-x[3],x[1]+x[2]+x[3]-dsq];
|
||||
let expected_j = [2.*tangent[0]*gap[0],-1.,0.,0.,
|
||||
2.*tangent[1]*gap[1],0.,-1.,0.,2.*tangent[2]*gap[2],0.,0.,-1.,0.,1.,1.,1.];
|
||||
assert_ne!(expected_j[0], 0.);
|
||||
let mut f = [f64::NAN;4];
|
||||
let mut j = Matrix::new(4,4);
|
||||
let found = CpDistAtEnfo::new(&k,&c,v3(*p),*dsq,*sqtol)
|
||||
.evaluate(x,&mut f,Some(&mut j)).unwrap();
|
||||
assert!(!found);
|
||||
for (a,b) in f.into_iter().zip(expected_f) { ellipse_distance_relative(a,b); }
|
||||
for (i,b) in expected_j.into_iter().enumerate() {
|
||||
ellipse_distance_relative(j.at(i/4,i%4).unwrap(),b);
|
||||
}
|
||||
let out = rust(&k,&op).unwrap();
|
||||
assert!(out.deferred.is_none());
|
||||
assert_eq!(out.ints["status"], vec![0]);
|
||||
assert_eq!(out.values["out"].len(),21);
|
||||
for (a,b) in out.values["out"].iter().copied().zip(
|
||||
expected_f.into_iter().chain(expected_j).chain([0.])) {
|
||||
ellipse_distance_relative(a,b);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cpp")]
|
||||
#[test]
|
||||
fn ellipse_distance_cpp_full_record_and_geometric_perturbation() {
|
||||
let case = ellipse_distance_fixture();
|
||||
let op: OpIn = case.parse().unwrap();
|
||||
let native = cpp(&op).unwrap();
|
||||
let actual = rust(&Kernel::default(),&op).unwrap();
|
||||
for out in [&native,&actual] {
|
||||
assert!(out.deferred.is_none());
|
||||
assert_eq!(out.ints["status"], vec![0]);
|
||||
assert_eq!(out.values["out"].len(),21);
|
||||
assert!(out.values["out"].iter().all(|v| v.is_finite()));
|
||||
assert_ne!(out.values["out"][4],0.);
|
||||
assert_eq!(out.values["out"][20],0.);
|
||||
}
|
||||
assert!(crate::compare::compare(&native,&actual,&[]).pass());
|
||||
let mut changed = native.clone();
|
||||
changed.values.get_mut("out").unwrap()[4] *= 1.01;
|
||||
assert!(!crate::compare::compare(&changed,&actual,&[]).pass());
|
||||
eprintln!("ellipse_distance_native={native:?}; ellipse_distance_rust={actual:?}");
|
||||
}
|
||||
|
||||
/// Generated cases cover every op with both successful and failing statuses.
|
||||
#[test]
|
||||
fn generated_cases_are_diverse() {
|
||||
|
||||
@@ -19,11 +19,18 @@
|
||||
//! `{"o", "x", "y", "scale"?, "repeat"?}` transforms (`property` `xf`) are applied on both sides
|
||||
//! (`SmAxis2Placement::SetCanonical` + `SmCurve::Transform` / `Curve::transform`) before
|
||||
//! the op; a circle with `quintic` is `SM_CO_QUINTIC`.
|
||||
//! `{"ellipse": {}}` instead constructs a genuine fixed `Ellipse` / `SmEllipse`:
|
||||
//! origin, canonical X/Y, semiaxes 3/2, angular degrees 0..180, natural NURBS 0..1.
|
||||
//!
|
||||
//! Deferred cases (unit `curve.curve_p5`): the leaf pairs that reach a Newton
|
||||
//! intersection (`LocalCurveIntersect`, through `SmCCIntersectENFO::Evaluate`) and the
|
||||
//! property solves that reach `SmCurve::LocalPropertyAnalysis` (every span the property
|
||||
//! solver does not cull).
|
||||
//!
|
||||
//! Generated circle-derived curvature-extrema cases use noncircular ellipses: a
|
||||
//! circle's constant curvature otherwise makes the number of noise-selected extrema
|
||||
//! machine-dependent. The original flat-circle case remains a full native regression
|
||||
//! outside the canonical report corpus; no solver or comparison rule is changed.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -34,6 +41,7 @@ use solidcore::curve::curve::p2::{CciGlobalSolver, Curve, CurvePropertyType};
|
||||
use solidcore::curve::curve::p5::make_curve_cache;
|
||||
use solidcore::curve::curve::p7::PropertyAnalysisOptions;
|
||||
use solidcore::curve::curve_cache::{CurveCache, CurveCacheOptions};
|
||||
use solidcore::curve::ellipse::{Ellipse, EllipseOptions};
|
||||
use solidcore::curve::hermite_curve::HermiteCurve;
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::extent1d::Extent1d;
|
||||
@@ -100,6 +108,12 @@ struct CircleIn {
|
||||
quintic: bool,
|
||||
}
|
||||
|
||||
/// Fixed genuine ellipse: origin, canonical X/Y, semiaxes 3/2, STEP degrees 0..180.
|
||||
/// Its natural NURBS interval is 0..1; there is deliberately no geometry payload.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EllipseIn {}
|
||||
|
||||
/// A transform applied to a curve before the op, `repeat` times
|
||||
/// (`SmAxis2Placement::SetCanonical(o, x, y)`, then `SmCurve::Transform` with `scale`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -123,6 +137,7 @@ enum CurveIn {
|
||||
Bspline(BSplineIn),
|
||||
Hermite(HermiteIn),
|
||||
Circle(CircleIn),
|
||||
Ellipse(EllipseIn),
|
||||
}
|
||||
|
||||
/// `SmCurveCache` constructor arguments.
|
||||
@@ -252,6 +267,7 @@ fn curve_block(v: &mut Vec<f64>, c: &CurveIn) -> Result<(), HarnessError> {
|
||||
}
|
||||
}
|
||||
CurveIn::Circle(_) => return Err(harness("unresolved circle")),
|
||||
CurveIn::Ellipse(_) => v.extend([3.0, 3.0]),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -399,6 +415,18 @@ fn build(c: &CurveIn) -> Result<Curve, HarnessError> {
|
||||
h.dim,
|
||||
))),
|
||||
CurveIn::Circle(_) => Err(harness("unresolved circle")),
|
||||
CurveIn::Ellipse(_) => Ellipse::new(
|
||||
&Kernel::default(),
|
||||
Vec3::new(0.0, 0.0, 0.0),
|
||||
Vec3::new(1.0, 0.0, 0.0),
|
||||
Vec3::new(0.0, 1.0, 0.0),
|
||||
Extent1d::from_min_max(0.0, 180.0),
|
||||
3.0,
|
||||
2.0,
|
||||
&EllipseOptions::default(),
|
||||
)
|
||||
.map(Curve::from)
|
||||
.map_err(|e| harness(format!("ellipse: {e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,12 +874,26 @@ fn gen_circle(rng: &mut Rng, closed: bool) -> CurveIn {
|
||||
}
|
||||
|
||||
fn gen_curve(rng: &mut Rng) -> CurveIn {
|
||||
gen_curve_with_circle_origin(rng).0
|
||||
}
|
||||
|
||||
fn gen_curve_with_circle_origin(rng: &mut Rng) -> (CurveIn, bool) {
|
||||
let s = if rng.chance(0.7) { 3.0 } else { 30.0 };
|
||||
match rng.below(10) {
|
||||
0 => gen_circle(rng, false),
|
||||
1 => gen_circle(rng, true),
|
||||
2..=6 => CurveIn::Bspline(gen_bspline(rng, s)),
|
||||
_ => CurveIn::Hermite(gen_hermite(rng, s)),
|
||||
0 => (gen_circle(rng, false), true),
|
||||
1 => (gen_circle(rng, true), true),
|
||||
2..=6 => (CurveIn::Bspline(gen_bspline(rng, s)), false),
|
||||
_ => (CurveIn::Hermite(gen_hermite(rng, s)), false),
|
||||
}
|
||||
}
|
||||
|
||||
// Generated circles have their X axis along global X. Stretching homogeneous X
|
||||
// by exactly two gives a noncircular rational ellipse without new random draws.
|
||||
fn stretch_circle_x(curve: &mut CurveIn) {
|
||||
if let CurveIn::Bspline(bs) = curve {
|
||||
for point in &mut bs.cpts {
|
||||
point[0] *= 2.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,7 +905,7 @@ fn range_of(c: &CurveIn) -> (f64, f64) {
|
||||
bs.u.first().copied().unwrap_or(0.0),
|
||||
bs.u.last().copied().unwrap_or(1.0),
|
||||
),
|
||||
CurveIn::Hermite(_) => (0.0, 1.0),
|
||||
CurveIn::Hermite(_) | CurveIn::Ellipse(_) => (0.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -971,7 +1013,7 @@ fn gen_partner(k: &Kernel, rng: &mut Rng, c1: &CurveIn) -> CurveIn {
|
||||
h.p2 = add(h.p2, off);
|
||||
CurveIn::Hermite(h)
|
||||
}
|
||||
other @ CurveIn::Circle(_) => other,
|
||||
other @ (CurveIn::Circle(_) | CurveIn::Ellipse(_)) => other,
|
||||
}
|
||||
}
|
||||
// a curve starting at a point of the first one
|
||||
@@ -1105,8 +1147,11 @@ fn gen_cci(k: &Kernel, rng: &mut Rng) -> CciIn {
|
||||
}
|
||||
|
||||
fn gen_property(k: &Kernel, rng: &mut Rng) -> OpIn {
|
||||
let curve = gen_curve(rng);
|
||||
let (mut curve, from_circle) = gen_curve_with_circle_origin(rng);
|
||||
let prop = rng.below(29) as i32;
|
||||
if from_circle && matches!(prop, 13 | 14) {
|
||||
stretch_circle_x(&mut curve);
|
||||
}
|
||||
let (a, bb) = range_of(&curve);
|
||||
let t = a + (bb - a) * rng.unit();
|
||||
let p = point_at(k, &curve, t);
|
||||
@@ -1228,6 +1273,236 @@ impl UnitDiff for Runner {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn real_ellipse_fixture() -> (Case, OpIn) {
|
||||
let ctx = CaseContext { root: crate::case::repo_root(), seeds: 0 };
|
||||
let cases = ctx.fixture_cases(Runner.id()).unwrap();
|
||||
let mut named = cases.into_iter().filter(|c| c.name == "real_ellipse_plane_classify_upper_arc_with_comb");
|
||||
let case = named.next().unwrap();
|
||||
assert!(named.next().is_none());
|
||||
assert!(matches!(&case.source, crate::case::CaseSource::Fixture { path, index: 0, .. }
|
||||
if path == "fixtures/curve.curve_p7/analytic-plane-classification.json"));
|
||||
let op = case.parse().unwrap();
|
||||
(case, op)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_ellipse_fixture_discovery_wire_and_concrete_geometry() {
|
||||
let (case, op) = real_ellipse_fixture();
|
||||
let file: Value = serde_json::from_str(include_str!("../../../../fixtures/curve.curve_p7/analytic-plane-classification.json")).unwrap();
|
||||
assert_eq!(file["cases"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(case.inputs, file["cases"][0]["inputs"]);
|
||||
assert_eq!(case.inputs, serde_json::json!({
|
||||
"op": "property", "curve": {"ellipse": {}}, "xf": [], "ivl": [0.0, 1.0],
|
||||
"prop": 15, "val": -1.0, "vecs": [[0.0, 1.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
|
||||
"tol": 1e-6, "comb": true
|
||||
}));
|
||||
let (code, input) = cpp_input(&op).unwrap();
|
||||
assert_eq!(code, Op::Property);
|
||||
assert_eq!(input, vec![3.0, 3.0, 0.0, 0.0, 1.0, 15.0, 1.0, -1.0, 3.0,
|
||||
0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1e-6, 1.0]);
|
||||
ffi::validate(code, &input).unwrap();
|
||||
assert!(serde_json::from_value::<CurveIn>(serde_json::json!({"ellipse": {"radius": 3}})).is_err());
|
||||
let OpIn::Property { curve: leaf, .. } = op else { panic!("property fixture") };
|
||||
assert!(matches!(resolve(&leaf).unwrap(), CurveIn::Ellipse(_)));
|
||||
assert_eq!(range_of(&leaf), (0.0, 1.0));
|
||||
let curve = build(&leaf).unwrap();
|
||||
assert_eq!(curve.type_string(), "SmEllipse_TYPE");
|
||||
let solidcore::curve::curve::CurveKind::Ellipse(e) = curve.kind() else { panic!("not a real ellipse") };
|
||||
assert!(!e.is_circle());
|
||||
assert_eq!((e.x_radius(), e.y_radius()), (3.0, 2.0));
|
||||
assert!(!e.inside_out());
|
||||
assert_eq!((e.step_interval().min(), e.step_interval().max()), (0.0, 180.0));
|
||||
assert_eq!((curve.natural_interval().min(), curve.natural_interval().max()), (0.0, 1.0));
|
||||
let nurb = e.base().gw_nurb().unwrap();
|
||||
assert!(!nurb.cpts().is_empty());
|
||||
assert!(nurb.cpts().iter().all(|p| [p.x, p.y, p.z, p.w].iter().all(|x| x.is_finite())));
|
||||
assert!(nurb.knots().iter().all(|x| x.is_finite()));
|
||||
for (t, expected) in [(0.0, [3.0, 0.0, 0.0]), (0.5, [0.0, 2.0, 0.0]), (1.0, [-3.0, 0.0, 0.0])] {
|
||||
let p = curve.evaluate_point(&Kernel::default(), t).unwrap();
|
||||
for (actual, want) in [p.x, p.y, p.z].into_iter().zip(expected) {
|
||||
assert!((actual - want).abs() <= 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_ellipse_protocol_admission_and_old_restrictions() {
|
||||
// Validation only: these are not native CCI or transformed-ellipse witnesses.
|
||||
let (_, op) = real_ellipse_fixture();
|
||||
let (code, input) = cpp_input(&op).unwrap();
|
||||
for end in 0..input.len() {
|
||||
assert!(ffi::validate(code, &input[..end]).is_err(), "prefix {end}");
|
||||
}
|
||||
for (at, value) in [(0, 3.5), (0, 4.0), (0, -1.0), (0, f64::NAN), (1, 0.0), (1, 2.0), (1, 4.0), (1, 2.5)] {
|
||||
let mut invalid = input.clone(); invalid[at] = value;
|
||||
assert!(ffi::validate(code, &invalid).is_err());
|
||||
}
|
||||
let mut extra = input.clone(); extra.push(0.0);
|
||||
assert!(ffi::validate(code, &extra).is_err());
|
||||
let mut one_vector = input.clone();
|
||||
one_vector[8] = 1.0; one_vector.drain(12..18);
|
||||
assert!(ffi::validate(code, &one_vector).unwrap_err().to_string().contains("more option vectors"));
|
||||
// Kind 2 is refused BEFORE reading its missing/invalid dimension, as before.
|
||||
assert!(ffi::validate(Op::Property, &[2.0]).unwrap_err().to_string().contains("B-splines and Hermite"));
|
||||
let hermite = CurveIn::Hermite(HermiteIn { dim: 3, p1: [0.0; 3], d1: [1.0, 0.0, 0.0], p2: [1.0; 3], d2: [1.0, 0.0, 0.0] });
|
||||
let ellipse = CurveIn::Ellipse(EllipseIn {});
|
||||
for (c1, c2) in [(ellipse.clone(), hermite.clone()), (hermite.clone(), ellipse.clone())] {
|
||||
let cci = CciIn { c1, c2: Some(c2), cache1: None, cache2: None, ivl1: [0.0, 1.0], ivl2: [0.0, 1.0], sop: 0, tol: 1e-6, proj: false, ptype: 0, vecs: vec![] };
|
||||
for candidate in [OpIn::CciSolve { cci: cci.clone() }, OpIn::CciLocal { cci, leaf1: 0, leaf2: 0 }] {
|
||||
let (code, input) = cpp_input(&candidate).unwrap(); ffi::validate(code, &input).unwrap();
|
||||
}
|
||||
}
|
||||
let mut transformed = op.clone();
|
||||
let OpIn::Property { xf, .. } = &mut transformed else { panic!("property") };
|
||||
xf.push(XformIn { o: [0.0; 3], x: [1.0, 0.0, 0.0], y: [0.0, 1.0, 0.0], scale: Some([1.0; 3]), repeat: 1 });
|
||||
let (code, input) = cpp_input(&transformed).unwrap(); ffi::validate(code, &input).unwrap();
|
||||
for (at, value) in [(2, 17.0), (3, ffi::MAX_COORD), (13, 0.0), (13, 2.0)] {
|
||||
let mut invalid = input.clone(); invalid[at] = value;
|
||||
assert!(ffi::validate(code, &invalid).is_err());
|
||||
}
|
||||
let OpIn::Property { curve, xf, .. } = &transformed else { panic!("property") };
|
||||
apply_xf(&Kernel::default(), &mut build(curve).unwrap(), xf).unwrap();
|
||||
let mut excessive = transformed.clone();
|
||||
let OpIn::Property { xf, .. } = &mut excessive else { panic!("property") }; xf[0].repeat = 17;
|
||||
let (code, input) = cpp_input(&excessive).unwrap(); assert!(ffi::validate(code, &input).is_err());
|
||||
let OpIn::Property { curve, .. } = &mut transformed else { panic!("property") }; *curve = hermite;
|
||||
let (code, input) = cpp_input(&transformed).unwrap();
|
||||
assert!(ffi::validate(code, &input).unwrap_err().to_string().contains("transforms apply to B-splines only"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "cpp")]
|
||||
fn assert_real_ellipse_output(out: &Output, curve: &Curve) {
|
||||
assert!(out.error.is_none() && out.rejected.is_none() && out.deferred.is_none(), "{out:?}");
|
||||
assert_eq!(out.ints["status"], [0]);
|
||||
let v = &out.values["out"];
|
||||
assert_eq!(v.len(), 916);
|
||||
assert!(v.iter().all(|x| x.is_finite()));
|
||||
assert_eq!(&v[..9], &[1.0, 2.0, 0.0, 0.0, -1.0, -1.0, -1.0, -1.0, 0.0]);
|
||||
assert_eq!((v[10], v[11], v[13]), (0.0, 0.0, 0.0));
|
||||
assert_eq!((v[14], v[465]), (150.0, 150.0));
|
||||
assert!(0.0 < v[9] && v[9] < v[12] && v[12] < 1.0);
|
||||
let k = Kernel::default();
|
||||
for (t, expected_t, sign) in [(v[9], 0.1705406887010544, 1.0), (v[12], 0.8294593112989456, -1.0)] {
|
||||
assert!((t - expected_t).abs() <= 1e-9 * expected_t);
|
||||
let p = curve.evaluate_point(&k, t).unwrap();
|
||||
assert!((p.y - 1.0).abs() <= 1e-6); // Fixture solver residual, NOT a comparator floor.
|
||||
let expected_x = sign * 3.0 * 3.0_f64.sqrt() / 2.0;
|
||||
assert!((p.x - expected_x).abs() <= 1e-9 * expected_x.abs());
|
||||
assert_eq!(p.z, 0.0);
|
||||
}
|
||||
let middle = curve.evaluate_point(&k, (v[9] + v[12]) / 2.0).unwrap();
|
||||
assert!(middle.x.abs() <= 1e-9 && (middle.y - 2.0).abs() <= 2e-9);
|
||||
assert_eq!(middle.z, 0.0);
|
||||
let mut positive = false; let mut negative = false;
|
||||
for i in 0..150 {
|
||||
let p = &v[15 + 3*i..18 + 3*i];
|
||||
let tine = &v[466 + 3*i..469 + 3*i];
|
||||
let expected = curve.evaluate_point(&k, i as f64 / 150.0).unwrap();
|
||||
for (actual, want) in p.iter().zip([expected.x, expected.y, expected.z]) {
|
||||
assert!((actual - want).abs() <= 1e-9);
|
||||
}
|
||||
assert_eq!(p[2], 0.0);
|
||||
assert!(p[1] >= 0.0);
|
||||
assert!((p[0]*p[0]/9.0 + p[1]*p[1]/4.0 - 1.0).abs() <= 1e-9);
|
||||
assert!(((p[0]*p[0]/9.0 + p[1]*p[1]/4.0).sqrt() - 1.0).abs() <= 1e-9);
|
||||
assert_eq!((tine[0], tine[2]), (0.0, 0.0));
|
||||
assert!((tine[1] - (1.0 - p[1])).abs() <= 1e-9);
|
||||
positive |= tine[1] > 0.0; negative |= tine[1] < 0.0;
|
||||
}
|
||||
assert!(positive && negative);
|
||||
assert!(v[240].abs() <= 1e-9 && (v[241] - 2.0).abs() <= 2e-9);
|
||||
assert!((v[692] + 1.0).abs() <= 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "cpp")]
|
||||
fn real_ellipse_plane_classify_full_native_geometry_and_comb_sensitivity() {
|
||||
let (case, op) = real_ellipse_fixture();
|
||||
let native = Runner.run_cpp(&case).unwrap();
|
||||
let port = Runner.run_rust(&case).unwrap();
|
||||
let OpIn::Property { curve, .. } = op else { panic!("property") };
|
||||
let curve = build(&curve).unwrap();
|
||||
for out in [&native, &port] { assert_real_ellipse_output(out, &curve); }
|
||||
assert!(Runner.abs_floors().is_empty());
|
||||
let comparison = crate::compare(&native, &port, Runner.abs_floors());
|
||||
assert!(comparison.pass(), "{comparison:?}");
|
||||
assert_eq!(comparison.values_compared, 916);
|
||||
let mut perturbed = port.clone();
|
||||
perturbed.values.get_mut("out").unwrap()[692] += 1e-3;
|
||||
for (i, (before, after)) in port.values["out"].iter().zip(&perturbed.values["out"]).enumerate() {
|
||||
if i != 692 { assert_eq!(before, after); }
|
||||
}
|
||||
assert_eq!(perturbed.ints, port.ints);
|
||||
assert!(!crate::compare(&native, &perturbed, Runner.abs_floors()).pass());
|
||||
}
|
||||
|
||||
// Compare the complete ordered native result, including its solution count.
|
||||
// Constant-curvature circles intentionally have no frozen extrema count.
|
||||
fn assert_native_parity(op: &OpIn) -> (usize, f64) {
|
||||
let native = cpp(op).unwrap();
|
||||
let port = rust(op).unwrap();
|
||||
for out in [&native, &port] {
|
||||
assert!(out.error.is_none(), "{out:?}");
|
||||
assert!(out.deferred.is_none(), "{out:?}");
|
||||
assert!(out.rejected.is_none(), "{out:?}");
|
||||
assert_eq!(out.ints.get("status"), Some(&vec![0]));
|
||||
assert!(!out.values["out"].is_empty());
|
||||
}
|
||||
let comparison = crate::compare(&native, &port, &[]);
|
||||
assert!(comparison.pass(), "{comparison:?}");
|
||||
assert!(comparison.values_compared > 0);
|
||||
(comparison.values_compared, native.values["out"][0])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_circle_original_gen1409_retains_full_native_parity() {
|
||||
let op: OpIn = serde_json::from_str(include_str!("../../tests/data/curve_p7_flat_circle.json")).unwrap();
|
||||
assert!(assert_native_parity(&op).1 > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_ellipse_extrema_have_stable_report_shapes() {
|
||||
// Frozen after replacing the seven circle-derived extrema inputs. CI on
|
||||
// Linux must reproduce these macOS shapes AND compare every native value.
|
||||
// No case may become rejected, deferred or error-only.
|
||||
for (index, prop, expected_values) in [
|
||||
(191, 14, 942), (510, 14, 981), (1409, 13, 929),
|
||||
(1526, 13, 16), (1682, 13, 955), (2276, 14, 916), (2526, 14, 16),
|
||||
] {
|
||||
let seed = CaseContext::seed_for("curve.curve_p7", index);
|
||||
let input = Runner.generate(&mut Rng::new(seed)).unwrap();
|
||||
let op: OpIn = serde_json::from_value(input).unwrap();
|
||||
assert!(matches!(&op, OpIn::Property { prop: actual, .. } if *actual == prop));
|
||||
assert_eq!(assert_native_parity(&op).0, expected_values, "gen{index:04}, seed {seed}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stretched_circle_has_known_nonconstant_curvature_and_native_extrema() {
|
||||
// Semiaxes 4 and 2: rho_min = b*b/a = 1, rho_max = a*a/b = 8.
|
||||
let circle = CurveIn::Circle(CircleIn {
|
||||
center: [0.0; 3], x: [1.0, 0.0, 0.0], y: [0.0, 1.0, 0.0],
|
||||
radius: 2.0, a0: 0.0, a1: 360.0, quintic: false,
|
||||
});
|
||||
let mut ellipse = resolve(&circle).unwrap();
|
||||
stretch_circle_x(&mut ellipse);
|
||||
let curve = build(&ellipse).unwrap();
|
||||
let (a, b) = range_of(&ellipse);
|
||||
for (fraction, expected) in [(0.0, 1.0), (0.25, 8.0), (0.5, 1.0), (0.75, 8.0)] {
|
||||
let derivatives = curve.evaluate(&Kernel::default(), a + fraction * (b - a), 2, true, false).unwrap();
|
||||
let speed = derivatives[1].length();
|
||||
let radius = speed * speed * speed / derivatives[1].cross(derivatives[2]).length();
|
||||
assert!((radius - expected).abs() <= 1e-9 * expected, "{radius} != {expected}");
|
||||
}
|
||||
for prop in [13, 14] {
|
||||
let (_, solutions) = assert_native_parity(&OpIn::Property {
|
||||
curve: ellipse.clone(), xf: Vec::new(), ivl: [a, b], prop, val: 0.0,
|
||||
vecs: vec![[0.0; 3]; 3], tol: 1e-6, comb: true,
|
||||
});
|
||||
assert!(solutions > 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generated cases cover every op, with passing (not deferred) cases.
|
||||
#[test]
|
||||
fn generated_cases_are_diverse() {
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
//! (`SmSurface::CalculateContinuities`, `surface.surface_p6`), the UV iso line of a
|
||||
//! constant-`u` curve (`SmLine`, `curve.line`) and reading (`SmSurface::ReadFromDB`,
|
||||
//! `surface.surface_p2`). Every run has at least [`MIN_GENERATED`] generated cases.
|
||||
//!
|
||||
//! Deviations: the runner-local SMS reader rejects non-finite, non-integral,
|
||||
//! negative and unrepresentable counts (the exclusive `2^usize::BITS` bound),
|
||||
//! checks dimension and payload arithmetic and available tokens before
|
||||
//! constructing the net, and grows storage incrementally instead of from
|
||||
//! header-declared dimensions. Valid point order, weights, knots and consumed-only
|
||||
//! float parsing are unchanged; unconsumed trailing text is ignored.
|
||||
|
||||
#[path = "fixtures/iso_curve_sms.rs"]
|
||||
mod sms;
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
@@ -176,41 +186,14 @@ struct Inputs {
|
||||
/// `m+q+2` v knots.
|
||||
fn read_sms(path: &str) -> Result<InlineSrf, HarnessError> {
|
||||
let text = std::fs::read_to_string(path).map_err(|e| harness(format!("{path}: {e}")))?;
|
||||
let mut it = text.split_whitespace();
|
||||
let mut num = || -> Result<f64, HarnessError> {
|
||||
it.next()
|
||||
.ok_or_else(|| harness(format!("{path}: truncated")))?
|
||||
.parse::<f64>()
|
||||
.map_err(|e| harness(format!("{path}: {e}")))
|
||||
};
|
||||
let int = |v: f64| -> Result<usize, HarnessError> {
|
||||
if v.fract() != 0.0 || v < 0.0 {
|
||||
return Err(harness(format!("{path}: bad count {v}")));
|
||||
}
|
||||
Ok(v as usize)
|
||||
};
|
||||
let n = int(num()?)?;
|
||||
let m = int(num()?)?;
|
||||
let p = int(num()?)?;
|
||||
let q = int(num()?)?;
|
||||
let rat = num()? != 0.0;
|
||||
let mut net = Vec::with_capacity(n + 1);
|
||||
for _ in 0..=n {
|
||||
let mut row = Vec::with_capacity(m + 1);
|
||||
for _ in 0..=m {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rat { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let u = (0..n + p + 2)
|
||||
.map(|_| num())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let v = (0..m + q + 2)
|
||||
.map(|_| num())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(InlineSrf { net, p, q, u, v })
|
||||
let s = sms::parse_sms(&text, path)?;
|
||||
Ok(InlineSrf {
|
||||
net: s.net,
|
||||
p: s.p,
|
||||
q: s.q,
|
||||
u: s.u,
|
||||
v: s.v,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve(s: &SrfIn) -> Result<InlineSrf, HarnessError> {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//! Runner-local SMS decoding, also included by non-C++ regression tests.
|
||||
//!
|
||||
//! Deviations: validate counts below the exclusive `2^usize::BITS` bound, check
|
||||
//! dimension and payload arithmetic and the available numeric tokens before
|
||||
//! constructing the net, and grow storage only as points/knots are read. Tokens are
|
||||
//! still parsed as they are consumed, so a trailing non-numeric token stays ignored
|
||||
//! as before.
|
||||
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
/// Decoded surface storage, with the v index fastest.
|
||||
pub(super) struct ParsedSrf {
|
||||
pub(super) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(super) p: usize,
|
||||
pub(super) q: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
pub(super) v: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Decodes `n m p q`, a rational flag, points, and u/v knots.
|
||||
pub(super) fn parse_sms(text: &str, path: &str) -> Result<ParsedSrf, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
// Count the tokens actually present, never the header-declared dimensions.
|
||||
let available = text.split_whitespace().count().saturating_sub(5);
|
||||
let mut it = text.split_whitespace();
|
||||
let mut num = || -> Result<f64, HarnessError> {
|
||||
it.next()
|
||||
.ok_or_else(|| error("truncated"))?
|
||||
.parse::<f64>()
|
||||
.map_err(|e| error(&e.to_string()))
|
||||
};
|
||||
let int = |value: f64, name: &str| -> Result<usize, HarnessError> {
|
||||
// usize::MAX rounds up to 2^BITS as f64 on 64-bit hosts. Use the
|
||||
// exclusive bound so that rounded, unrepresentable value is rejected too.
|
||||
let upper = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value >= upper {
|
||||
return Err(error(&format!("bad {name} count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = int(num()?, "n")?;
|
||||
let m = int(num()?, "m")?;
|
||||
let p = int(num()?, "p")?;
|
||||
let q = int(num()?, "q")?;
|
||||
let rat = num()? != 0.0;
|
||||
let rows = n
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error("row count overflow"))?;
|
||||
let cols = m
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error("column count overflow"))?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error("u knot count overflow"))?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error("v knot count overflow"))?;
|
||||
let required = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rat { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(|| error("required token count overflow"))?;
|
||||
if required > available {
|
||||
return Err(error(&format!(
|
||||
"truncated: need {required} data tokens, have {available}"
|
||||
)));
|
||||
}
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rat { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(ParsedSrf { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//! Runner-local SMS decoding, also included by non-C++ regression tests.
|
||||
//!
|
||||
//! Deviations: validate counts, check arithmetic and available whitespace tokens
|
||||
//! before constructing the net, and grow storage only as points/knots are read.
|
||||
//! Preserve the original runner's consumed-only float parsing; ignore trailing text.
|
||||
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
/// Decoded surface storage, with the v index fastest.
|
||||
pub(super) struct ParsedSrf {
|
||||
pub(super) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(super) p: usize,
|
||||
pub(super) q: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
pub(super) v: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Decodes `n m p q`, a rational flag, points, and u/v knots.
|
||||
pub(super) fn parse_sms(text: &str, path: &str) -> Result<ParsedSrf, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
let mut it = text.split_whitespace();
|
||||
// Count actual tokens without parsing or allocating: trailing text was never
|
||||
// consumed by the original runner and must not become a float error here.
|
||||
let available = it.clone().count().saturating_sub(5);
|
||||
let mut num = || {
|
||||
it.next()
|
||||
.ok_or_else(|| error("truncated"))?
|
||||
.parse::<f64>()
|
||||
.map_err(|e| error(&e.to_string()))
|
||||
};
|
||||
let int = |value: f64, name: &str| -> Result<usize, HarnessError> {
|
||||
// usize::MAX rounds up on 64-bit hosts; use the exclusive power-of-two bound.
|
||||
let upper = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value >= upper {
|
||||
return Err(error(&format!("bad {name} count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = int(num()?, "n")?;
|
||||
let m = int(num()?, "m")?;
|
||||
let p = int(num()?, "p")?;
|
||||
let q = int(num()?, "q")?;
|
||||
let rat = num()? != 0.0;
|
||||
let rows = n
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error("row count overflow"))?;
|
||||
let cols = m
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error("column count overflow"))?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error("u knot count overflow"))?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error("v knot count overflow"))?;
|
||||
let required = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rat { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(|| error("required token count overflow"))?;
|
||||
if required > available {
|
||||
// These tokens would all be consumed before the original reader reached
|
||||
// EOF. Preserve float-error precedence without constructing any storage.
|
||||
for _ in 0..available {
|
||||
num()?;
|
||||
}
|
||||
return Err(error(&format!(
|
||||
"truncated: need {required} data tokens, have {available}"
|
||||
)));
|
||||
}
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rat { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(ParsedSrf { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Allocation-checked SMC decoding shared by the runner and non-C++ regression tests.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::{NOW, NOZ};
|
||||
|
||||
pub(super) struct SmcCurve {
|
||||
pub(super) cpts: Vec<[f64; 4]>,
|
||||
pub(super) p: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(super) fn decode(text: &str, path: &str) -> Result<SmcCurve, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
// The original reader eagerly parsed the entire file before inspecting any
|
||||
// headers. Keep malformed-tail precedence, including ignored numeric tails.
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|token| token.parse::<f64>().map_err(|e| error(&e.to_string())))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut numbers = values.into_iter();
|
||||
let mut number = || numbers.next().ok_or_else(|| error("truncated"));
|
||||
let count = |value: f64| {
|
||||
// usize::MAX rounds up on 64-bit targets: check the exclusive 2^BITS
|
||||
// bound before casting, rather than allowing a saturating conversion.
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value >= exclusive {
|
||||
return Err(error(&format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = count(number()?)?;
|
||||
let p = count(number()?)?;
|
||||
let rational = number()? != 0.0;
|
||||
let dim = count(number()?)?;
|
||||
let overflow = || error("count overflow");
|
||||
let points = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let knots = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let stride = 2_usize
|
||||
.checked_add(usize::from(dim == 3))
|
||||
.and_then(|v| v.checked_add(usize::from(rational)))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = points
|
||||
.checked_mul(stride)
|
||||
.and_then(|v| v.checked_add(knots))
|
||||
.ok_or_else(overflow)?;
|
||||
// Preflight already-parsed numeric availability before growing any output.
|
||||
// No output reservation or allocation is driven by an unchecked header.
|
||||
if numbers.len() < payload {
|
||||
return Err(error("truncated"));
|
||||
}
|
||||
let mut number = || numbers.next().ok_or_else(|| error("truncated"));
|
||||
let mut cpts = Vec::new();
|
||||
for _ in 0..points {
|
||||
let (x, y) = (number()?, number()?);
|
||||
let z = if dim == 3 { number()? } else { NOZ };
|
||||
let w = if rational { number()? } else { NOW };
|
||||
cpts.push([x, y, z, w]);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..knots {
|
||||
u.push(number()?);
|
||||
}
|
||||
Ok(SmcCurve { cpts, p, u })
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Allocation-checked SMC decoding shared by the runner and non-C++ regression tests.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::{NOW, NOZ};
|
||||
|
||||
pub(super) struct SmcCurve {
|
||||
pub(super) cpts: Vec<[f64; 4]>,
|
||||
pub(super) p: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(super) fn decode(text: &str, path: &str) -> Result<SmcCurve, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
// The original reader eagerly parsed the entire file before inspecting any
|
||||
// headers. Keep malformed-tail precedence, including ignored numeric tails.
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|token| token.parse::<f64>().map_err(|e| error(&e.to_string())))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut numbers = values.into_iter();
|
||||
let mut number = || numbers.next().ok_or_else(|| error("truncated"));
|
||||
let count = |value: f64| {
|
||||
// usize::MAX rounds up on 64-bit targets: check the exclusive 2^BITS
|
||||
// bound before casting, rather than allowing a saturating conversion.
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value >= exclusive {
|
||||
return Err(error(&format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = count(number()?)?;
|
||||
let p = count(number()?)?;
|
||||
let rational = number()? != 0.0;
|
||||
let dim = count(number()?)?;
|
||||
let overflow = || error("count overflow");
|
||||
let points = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let knots = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let stride = 2_usize
|
||||
.checked_add(usize::from(dim == 3))
|
||||
.and_then(|v| v.checked_add(usize::from(rational)))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = points
|
||||
.checked_mul(stride)
|
||||
.and_then(|v| v.checked_add(knots))
|
||||
.ok_or_else(overflow)?;
|
||||
// Preflight already-parsed numeric availability before growing any output.
|
||||
// No output reservation or allocation is driven by an unchecked header.
|
||||
if numbers.len() < payload {
|
||||
return Err(error("truncated"));
|
||||
}
|
||||
let mut number = || numbers.next().ok_or_else(|| error("truncated"));
|
||||
let mut cpts = Vec::new();
|
||||
for _ in 0..points {
|
||||
let (x, y) = (number()?, number()?);
|
||||
let z = if dim == 3 { number()? } else { NOZ };
|
||||
let w = if rational { number()? } else { NOW };
|
||||
cpts.push([x, y, z, w]);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..knots {
|
||||
u.push(number()?);
|
||||
}
|
||||
Ok(SmcCurve { cpts, p, u })
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Allocation-checked SMC decoding shared by the runner and non-C++ regression tests.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::{NOW, NOZ};
|
||||
|
||||
pub(super) struct SmcCurve {
|
||||
pub(super) cpts: Vec<[f64; 4]>,
|
||||
pub(super) p: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(super) fn decode(text: &str, path: &str) -> Result<SmcCurve, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
// Preserve eager lexical validation of the entire file, even ignored tails,
|
||||
// before inspecting headers in their original n, p, rational, dim order.
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|token| token.parse::<f64>().map_err(|e| error(&e.to_string())))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut numbers = values.into_iter();
|
||||
let mut number = || numbers.next().ok_or_else(|| error("truncated"));
|
||||
let count = |value: f64| {
|
||||
// usize::MAX rounds up on 64-bit targets; reject the exclusive 2^BITS
|
||||
// boundary before casting rather than accepting a saturating conversion.
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value >= exclusive {
|
||||
return Err(error(&format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = count(number()?)?;
|
||||
let p = count(number()?)?;
|
||||
let rational = number()? != 0.0;
|
||||
let dim = count(number()?)?;
|
||||
let overflow = || error("count overflow");
|
||||
let points = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let knots = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let stride = 2_usize
|
||||
.checked_add(usize::from(dim == 3))
|
||||
.and_then(|v| v.checked_add(usize::from(rational)))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = points
|
||||
.checked_mul(stride)
|
||||
.and_then(|v| v.checked_add(knots))
|
||||
.ok_or_else(overflow)?;
|
||||
// Preflight numeric availability before any header-driven output growth.
|
||||
if numbers.len() < payload {
|
||||
return Err(error("truncated"));
|
||||
}
|
||||
let mut number = || numbers.next().ok_or_else(|| error("truncated"));
|
||||
let mut cpts = Vec::new();
|
||||
for _ in 0..points {
|
||||
let (x, y) = (number()?, number()?);
|
||||
let z = if dim == 3 { number()? } else { NOZ };
|
||||
let w = if rational { number()? } else { NOW };
|
||||
cpts.push([x, y, z, w]);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..knots {
|
||||
u.push(number()?);
|
||||
}
|
||||
Ok(SmcCurve { cpts, p, u })
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//! Allocation-checked SMS decoding shared by the runner and non-C++ regression tests.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
pub(super) struct SmsSurface {
|
||||
pub(super) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(super) p: usize,
|
||||
pub(super) q: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
pub(super) v: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(super) fn decode(text: &str, path: &str) -> Result<SmsSurface, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
// Lexically validate the whole file, including ignored numeric tails, before
|
||||
// interpreting headers in their original n, m, p, q, rational order.
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|token| token.parse::<f64>().map_err(|e| error(&e.to_string())))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut numbers = values.into_iter();
|
||||
let mut number = || numbers.next().ok_or_else(|| error("truncated"));
|
||||
let count = |value: f64| {
|
||||
// usize::MAX rounds up on 64-bit targets: test the exclusive bound
|
||||
// before casting, rather than allowing a saturating conversion.
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value >= exclusive {
|
||||
return Err(error(&format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = count(number()?)?;
|
||||
let m = count(number()?)?;
|
||||
let p = count(number()?)?;
|
||||
let q = count(number()?)?;
|
||||
let rational = number()? != 0.0;
|
||||
let overflow = || error("count overflow");
|
||||
let rows = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let cols = m.checked_add(1).ok_or_else(overflow)?;
|
||||
let u_knots = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let v_knots = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let stride = 3_usize
|
||||
.checked_add(usize::from(rational))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(stride))
|
||||
.and_then(|v| v.checked_add(u_knots))
|
||||
.and_then(|v| v.checked_add(v_knots))
|
||||
.ok_or_else(overflow)?;
|
||||
// Actual numeric availability, not the header, bounds all output growth.
|
||||
if numbers.len() < payload {
|
||||
return Err(error("truncated"));
|
||||
}
|
||||
let mut number = || numbers.next().ok_or_else(|| error("truncated"));
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (number()?, number()?, number()?);
|
||||
let w = if rational { number()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_knots {
|
||||
u.push(number()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_knots {
|
||||
v.push(number()?);
|
||||
}
|
||||
Ok(SmsSurface { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Allocation-checked SMC decoding shared by the runner and non-C++ regression tests.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::{NOW, NOZ};
|
||||
|
||||
pub(super) struct SmcCurve {
|
||||
pub(super) cpts: Vec<[f64; 4]>,
|
||||
pub(super) p: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(super) fn decode(text: &str, path: &str) -> Result<SmcCurve, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
// Preserve eager lexical validation of the entire file, even ignored tails,
|
||||
// before inspecting headers in their original n, p, rational, dim order.
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|token| token.parse::<f64>().map_err(|e| error(&e.to_string())))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut numbers = values.into_iter();
|
||||
let mut number = || numbers.next().ok_or_else(|| error("truncated"));
|
||||
let count = |value: f64| {
|
||||
// usize::MAX rounds up on 64-bit targets; reject the exclusive 2^BITS
|
||||
// boundary before casting rather than accepting a saturating conversion.
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value >= exclusive {
|
||||
return Err(error(&format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = count(number()?)?;
|
||||
let p = count(number()?)?;
|
||||
let rational = number()? != 0.0;
|
||||
let dim = count(number()?)?;
|
||||
let overflow = || error("count overflow");
|
||||
let points = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let knots = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let stride = 2_usize
|
||||
.checked_add(usize::from(dim == 3))
|
||||
.and_then(|v| v.checked_add(usize::from(rational)))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = points
|
||||
.checked_mul(stride)
|
||||
.and_then(|v| v.checked_add(knots))
|
||||
.ok_or_else(overflow)?;
|
||||
// Preflight numeric availability before any header-driven output growth.
|
||||
if numbers.len() < payload {
|
||||
return Err(error("truncated"));
|
||||
}
|
||||
let mut number = || numbers.next().ok_or_else(|| error("truncated"));
|
||||
let mut cpts = Vec::new();
|
||||
for _ in 0..points {
|
||||
let (x, y) = (number()?, number()?);
|
||||
let z = if dim == 3 { number()? } else { NOZ };
|
||||
let w = if rational { number()? } else { NOW };
|
||||
cpts.push([x, y, z, w]);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..knots {
|
||||
u.push(number()?);
|
||||
}
|
||||
Ok(SmcCurve { cpts, p, u })
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Allocation-checked SMC decoding shared by the runner and non-C++ regression tests.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::{NOW, NOZ};
|
||||
|
||||
pub(super) struct SmcCurve {
|
||||
pub(super) cpts: Vec<[f64; 4]>,
|
||||
pub(super) p: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(super) fn decode(text: &str, path: &str) -> Result<SmcCurve, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
// The original reader eagerly parsed the entire file before inspecting any
|
||||
// headers. Keep malformed-tail precedence, including ignored numeric tails.
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|token| token.parse::<f64>().map_err(|e| error(&e.to_string())))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut numbers = values.into_iter();
|
||||
let mut number = || numbers.next().ok_or_else(|| error("truncated"));
|
||||
let count = |value: f64| {
|
||||
// usize::MAX rounds up on 64-bit targets: check the exclusive 2^BITS
|
||||
// bound before casting, rather than allowing a saturating conversion.
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value >= exclusive {
|
||||
return Err(error(&format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = count(number()?)?;
|
||||
let p = count(number()?)?;
|
||||
let rational = number()? != 0.0;
|
||||
let dim = count(number()?)?;
|
||||
let overflow = || error("count overflow");
|
||||
let points = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let knots = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let stride = 2_usize
|
||||
.checked_add(usize::from(dim == 3))
|
||||
.and_then(|v| v.checked_add(usize::from(rational)))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = points
|
||||
.checked_mul(stride)
|
||||
.and_then(|v| v.checked_add(knots))
|
||||
.ok_or_else(overflow)?;
|
||||
// Preflight already-parsed numeric availability before growing any output.
|
||||
// No output reservation or allocation is driven by an unchecked header.
|
||||
if numbers.len() < payload {
|
||||
return Err(error("truncated"));
|
||||
}
|
||||
let mut number = || numbers.next().ok_or_else(|| error("truncated"));
|
||||
let mut cpts = Vec::new();
|
||||
for _ in 0..points {
|
||||
let (x, y) = (number()?, number()?);
|
||||
let z = if dim == 3 { number()? } else { NOZ };
|
||||
let w = if rational { number()? } else { NOW };
|
||||
cpts.push([x, y, z, w]);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..knots {
|
||||
u.push(number()?);
|
||||
}
|
||||
Ok(SmcCurve { cpts, p, u })
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
//! Pure SMS/SMC fixture readers for `nlib.srf_adv_p1`.
|
||||
//!
|
||||
//! Deviations: reject unrepresentable counts and checked size overflow, and preflight
|
||||
//! the numeric payload before building geometry. All tokens are parsed eagerly,
|
||||
//! including ignored numeric tails, preserving float-error precedence.
|
||||
|
||||
use solidcore::nurbs::math::{NOW, NOZ};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Surface {
|
||||
pub(crate) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(crate) p: usize,
|
||||
pub(crate) q: usize,
|
||||
pub(crate) u: Vec<f64>,
|
||||
pub(crate) v: Vec<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Curve {
|
||||
pub(crate) cpts: Vec<[f64; 4]>,
|
||||
pub(crate) p: usize,
|
||||
pub(crate) u: Vec<f64>,
|
||||
}
|
||||
|
||||
struct Numbers<'a> {
|
||||
path: &'a str,
|
||||
values: std::vec::IntoIter<f64>,
|
||||
}
|
||||
|
||||
impl<'a> Numbers<'a> {
|
||||
fn new(path: &'a str, source: &str) -> Result<Self, String> {
|
||||
let values = source
|
||||
.split_whitespace()
|
||||
.map(|t| t.parse::<f64>().map_err(|e| format!("{path}: {e}")))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(Self {
|
||||
path,
|
||||
values: values.into_iter(),
|
||||
})
|
||||
}
|
||||
|
||||
fn num(&mut self) -> Result<f64, String> {
|
||||
self.values
|
||||
.next()
|
||||
.ok_or_else(|| format!("{}: truncated", self.path))
|
||||
}
|
||||
|
||||
fn count(&mut self) -> Result<usize, String> {
|
||||
let v = self.num()?;
|
||||
// usize::MAX rounds up on 64-bit targets; the upper bound is exclusive.
|
||||
if !v.is_finite() || v.fract() != 0.0 || v < 0.0 || v >= 2.0_f64.powi(usize::BITS as i32) {
|
||||
return Err(format!("{}: bad count {v}", self.path));
|
||||
}
|
||||
Ok(v as usize)
|
||||
}
|
||||
|
||||
fn size(&self, size: Option<usize>) -> Result<usize, String> {
|
||||
size.ok_or_else(|| format!("{}: count overflow", self.path))
|
||||
}
|
||||
|
||||
fn preflight(&self, size: usize) -> Result<(), String> {
|
||||
if size > self.values.len() {
|
||||
return Err(format!("{}: truncated", self.path));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn knots(&mut self, count: usize) -> Result<Vec<f64>, String> {
|
||||
let mut knots = Vec::new();
|
||||
for _ in 0..count {
|
||||
knots.push(self.num()?);
|
||||
}
|
||||
Ok(knots)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_sms(path: &str, source: &str) -> Result<Surface, String> {
|
||||
let mut nums = Numbers::new(path, source)?;
|
||||
let n = nums.count()?;
|
||||
let m = nums.count()?;
|
||||
let p = nums.count()?;
|
||||
let q = nums.count()?;
|
||||
let rat = nums.num()? != 0.0;
|
||||
let rows = nums.size(n.checked_add(1))?;
|
||||
let cols = nums.size(m.checked_add(1))?;
|
||||
let u_count = nums.size(n.checked_add(p).and_then(|v| v.checked_add(2)))?;
|
||||
let v_count = nums.size(m.checked_add(q).and_then(|v| v.checked_add(2)))?;
|
||||
let stride = nums.size(3_usize.checked_add(usize::from(rat)))?;
|
||||
let payload = nums.size(
|
||||
rows.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(stride))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count)),
|
||||
)?;
|
||||
nums.preflight(payload)?;
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (nums.num()?, nums.num()?, nums.num()?);
|
||||
let w = if rat { nums.num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let u = nums.knots(u_count)?;
|
||||
let v = nums.knots(v_count)?;
|
||||
Ok(Surface { net, p, q, u, v })
|
||||
}
|
||||
|
||||
pub(crate) fn parse_smc(path: &str, source: &str) -> Result<Curve, String> {
|
||||
let mut nums = Numbers::new(path, source)?;
|
||||
let n = nums.count()?;
|
||||
let p = nums.count()?;
|
||||
let rat = nums.num()? != 0.0;
|
||||
let dim = nums.count()?;
|
||||
let points = nums.size(n.checked_add(1))?;
|
||||
let u_count = nums.size(n.checked_add(p).and_then(|v| v.checked_add(2)))?;
|
||||
let stride = nums.size(
|
||||
2_usize
|
||||
.checked_add(usize::from(dim == 3))
|
||||
.and_then(|v| v.checked_add(usize::from(rat))),
|
||||
)?;
|
||||
let payload = nums.size(
|
||||
points
|
||||
.checked_mul(stride)
|
||||
.and_then(|v| v.checked_add(u_count)),
|
||||
)?;
|
||||
nums.preflight(payload)?;
|
||||
let mut cpts = Vec::new();
|
||||
for _ in 0..points {
|
||||
let (x, y) = (nums.num()?, nums.num()?);
|
||||
let z = if dim == 3 { nums.num()? } else { NOZ };
|
||||
let w = if rat { nums.num()? } else { NOW };
|
||||
cpts.push([x, y, z, w]);
|
||||
}
|
||||
let u = nums.knots(u_count)?;
|
||||
Ok(Curve { cpts, p, u })
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//! Lazy SMS decoding shared by the surface-advanced runner and non-C++ tests.
|
||||
//!
|
||||
//! Deviations: counts and payload sizes are checked before incremental allocation.
|
||||
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
pub(super) struct SmsSurface {
|
||||
pub net: Vec<Vec<[f64; 4]>>,
|
||||
pub p: usize,
|
||||
pub q: usize,
|
||||
pub u: Vec<f64>,
|
||||
pub v: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(super) fn parse_sms(text: &str, path: &str) -> Result<SmsSurface, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
let mut tokens = text.split_whitespace();
|
||||
let number = |tokens: &mut std::str::SplitWhitespace<'_>| {
|
||||
tokens
|
||||
.next()
|
||||
.ok_or_else(|| error("truncated"))?
|
||||
.parse::<f64>()
|
||||
.map_err(|e| error(&e.to_string()))
|
||||
};
|
||||
let count = |value: f64| {
|
||||
// usize::MAX as f64 rounds up on 64-bit hosts: the limit is exclusive.
|
||||
if !value.is_finite()
|
||||
|| value.fract() != 0.0
|
||||
|| value < 0.0
|
||||
|| value >= 2.0_f64.powi(usize::BITS as i32)
|
||||
{
|
||||
return Err(error(&format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = count(number(&mut tokens)?)?;
|
||||
let m = count(number(&mut tokens)?)?;
|
||||
let p = count(number(&mut tokens)?)?;
|
||||
let q = count(number(&mut tokens)?)?;
|
||||
let rat = number(&mut tokens)? != 0.0;
|
||||
let overflow = || error("SMS size overflow");
|
||||
let rows = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let cols = m.checked_add(1).ok_or_else(overflow)?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rat { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(overflow)?;
|
||||
|
||||
if tokens.clone().count() < payload {
|
||||
// Preserve the lazy reader's first numeric error before truncation, but
|
||||
// never grow a net from a short file declaring huge dimensions.
|
||||
while tokens.clone().next().is_some() {
|
||||
number(&mut tokens)?;
|
||||
}
|
||||
return Err(error("truncated"));
|
||||
}
|
||||
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (
|
||||
number(&mut tokens)?,
|
||||
number(&mut tokens)?,
|
||||
number(&mut tokens)?,
|
||||
);
|
||||
let w = if rat { number(&mut tokens)? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(number(&mut tokens)?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(number(&mut tokens)?);
|
||||
}
|
||||
// Trailing tokens have always been ignored, even when they are not numbers.
|
||||
Ok(SmsSurface { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Pure SMS fixture decoding for `nlib.srf_adv_p3`.
|
||||
//!
|
||||
//! Deviations: reject counts at the exclusive usize bound before casting; check
|
||||
//! all size arithmetic and actual numeric payload availability before growing
|
||||
//! geometry incrementally. No geometry-validity or degree restrictions are added.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
pub(super) struct ParsedSurface {
|
||||
pub net: Vec<Vec<[f64; 4]>>,
|
||||
pub p: usize,
|
||||
pub q: usize,
|
||||
pub u: Vec<f64>,
|
||||
pub v: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(super) fn parse_sms(text: &str, path: &str) -> Result<ParsedSurface, HarnessError> {
|
||||
// Parse the entire text first: even an ignored malformed tail must win.
|
||||
let all = text
|
||||
.split_whitespace()
|
||||
.map(|t| {
|
||||
t.parse::<f64>()
|
||||
.map_err(|e| HarnessError::new(format!("{path}: {e}")))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut it = all.into_iter();
|
||||
let next = |it: &mut std::vec::IntoIter<f64>| {
|
||||
it.next()
|
||||
.ok_or_else(|| HarnessError::new(format!("{path}: truncated")))
|
||||
};
|
||||
let count = |v: f64| {
|
||||
// Unlike usize::MAX as f64, this bound is exact on both 32 and 64 bits.
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !v.is_finite() || v.fract() != 0.0 || v < 0.0 || v >= exclusive {
|
||||
Err(HarnessError::new(format!("{path}: bad count {v}")))
|
||||
} else {
|
||||
Ok(v as usize)
|
||||
}
|
||||
};
|
||||
let n = count(next(&mut it)?)?;
|
||||
let m = count(next(&mut it)?)?;
|
||||
let p = count(next(&mut it)?)?;
|
||||
let q = count(next(&mut it)?)?;
|
||||
let rational = next(&mut it)? != 0.0;
|
||||
let overflow = || HarnessError::new(format!("{path}: count overflow"));
|
||||
let rows = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let cols = m.checked_add(1).ok_or_else(overflow)?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rational { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(overflow)?;
|
||||
if payload > it.len() {
|
||||
return Err(HarnessError::new(format!("{path}: truncated")));
|
||||
}
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (next(&mut it)?, next(&mut it)?, next(&mut it)?);
|
||||
let w = if rational { next(&mut it)? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(next(&mut it)?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(next(&mut it)?);
|
||||
}
|
||||
Ok(ParsedSurface { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//! SMS decoding shared by the surface-basic runner and non-C++ regression tests.
|
||||
//!
|
||||
//! Deviations: counts and payload sizes are checked before incremental allocation.
|
||||
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
pub(super) struct SmsSurface {
|
||||
pub net: Vec<Vec<[f64; 4]>>,
|
||||
pub p: usize,
|
||||
pub q: usize,
|
||||
pub u: Vec<f64>,
|
||||
pub v: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(super) fn parse_sms(text: &str, path: &str) -> Result<SmsSurface, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
let mut tokens = text.split_whitespace();
|
||||
let number = |tokens: &mut std::str::SplitWhitespace<'_>| {
|
||||
tokens
|
||||
.next()
|
||||
.ok_or_else(|| error("truncated"))?
|
||||
.parse::<f64>()
|
||||
.map_err(|e| error(&e.to_string()))
|
||||
};
|
||||
let count = |value: f64, name: &str| {
|
||||
// usize::MAX as f64 rounds up on 64-bit hosts: the limit is exclusive.
|
||||
if !value.is_finite()
|
||||
|| value.fract() != 0.0
|
||||
|| value < 0.0
|
||||
|| value >= 2.0_f64.powi(usize::BITS as i32)
|
||||
{
|
||||
return Err(error(&format!("bad {name} count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = count(number(&mut tokens)?, "n")?;
|
||||
let m = count(number(&mut tokens)?, "m")?;
|
||||
let p = count(number(&mut tokens)?, "p")?;
|
||||
let q = count(number(&mut tokens)?, "q")?;
|
||||
let rat = number(&mut tokens)? != 0.0;
|
||||
let overflow = || error("SMS size overflow");
|
||||
let rows = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let cols = m.checked_add(1).ok_or_else(overflow)?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rat { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(overflow)?;
|
||||
|
||||
if tokens.clone().count() < payload {
|
||||
// Preserve lazy parsing's first error: a bad available number precedes
|
||||
// truncation. Do not allocate even when a short file declares a huge net.
|
||||
while tokens.clone().next().is_some() {
|
||||
number(&mut tokens)?;
|
||||
}
|
||||
return Err(error("truncated"));
|
||||
}
|
||||
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (
|
||||
number(&mut tokens)?,
|
||||
number(&mut tokens)?,
|
||||
number(&mut tokens)?,
|
||||
);
|
||||
let w = if rat { number(&mut tokens)? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(number(&mut tokens)?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(number(&mut tokens)?);
|
||||
}
|
||||
// Trailing tokens have always been ignored, even when they are not numbers.
|
||||
Ok(SmsSurface { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//! Runner-local SMS decoding, shared with non-C++ regression tests.
|
||||
//!
|
||||
//! Deviations: reject unrepresentable counts and arithmetic overflow; preflight
|
||||
//! the consumed numeric payload without allocation before growing storage.
|
||||
//! Parsing remains lazy with respect to ignored trailing tokens.
|
||||
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
/// Decoded surface storage, with the v index fastest.
|
||||
pub(super) struct ParsedSrf {
|
||||
pub(super) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(super) p: usize,
|
||||
pub(super) q: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
pub(super) v: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Decodes the header, homogeneous or polynomial points, and u/v knots.
|
||||
pub(super) fn parse_sms(text: &str, path: &str) -> Result<ParsedSrf, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
let parse = |token: &str| token.parse::<f64>().map_err(|e| error(&e.to_string()));
|
||||
let mut it = text.split_whitespace();
|
||||
let mut num = || parse(it.next().ok_or_else(|| error("truncated"))?);
|
||||
let int = |value: f64| -> Result<usize, HarnessError> {
|
||||
// usize::MAX as f64 rounds up on 64-bit hosts: use an exclusive bound.
|
||||
let upper = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value >= upper {
|
||||
return Err(error(&format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = int(num()?)?;
|
||||
let m = int(num()?)?;
|
||||
let p = int(num()?)?;
|
||||
let q = int(num()?)?;
|
||||
let rat = num()? != 0.0;
|
||||
let rows = n
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error("row count overflow"))?;
|
||||
let cols = m
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error("column count overflow"))?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error("u knot count overflow"))?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error("v knot count overflow"))?;
|
||||
let required = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rat { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(|| error("required token count overflow"))?;
|
||||
|
||||
// Validate even a short prefix before reporting truncation, preserving the
|
||||
// original first numeric error. Never parse ignored trailing text or allocate
|
||||
// storage from declared sizes, including when the payload is incomplete.
|
||||
let mut available = 0;
|
||||
for token in it.clone().take(required) {
|
||||
parse(token)?;
|
||||
available += 1;
|
||||
}
|
||||
if available < required {
|
||||
return Err(error("truncated"));
|
||||
}
|
||||
let mut num = || parse(it.next().ok_or_else(|| error("truncated"))?);
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rat { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(ParsedSrf { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Runner-local SMS decoding, also included by non-C++ regression tests.
|
||||
//!
|
||||
//! Deviations: validate counts, check arithmetic and available numeric tokens
|
||||
//! before constructing the net, and grow storage only as points/knots are read.
|
||||
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
/// Decoded surface storage, with the v index fastest.
|
||||
pub(super) struct ParsedSrf {
|
||||
pub(super) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(super) p: usize,
|
||||
pub(super) q: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
pub(super) v: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Decodes `n m p q`, a rational flag, points, and u/v knots.
|
||||
pub(super) fn parse_sms(text: &str, path: &str) -> Result<ParsedSrf, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
// Preserve the original eager float parsing, including trailing tokens.
|
||||
// This allocation depends only on actual input, never declared dimensions.
|
||||
let all = text
|
||||
.split_whitespace()
|
||||
.map(|t| t.parse::<f64>().map_err(|e| error(&e.to_string())))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let available = all.len().saturating_sub(5);
|
||||
let mut it = all.into_iter();
|
||||
let mut num = || it.next().ok_or_else(|| error("truncated"));
|
||||
let int = |value: f64, name: &str| -> Result<usize, HarnessError> {
|
||||
// usize::MAX rounds up to 2^BITS as f64 on 64-bit hosts. Use the
|
||||
// exclusive bound so that rounded, unrepresentable value is rejected too.
|
||||
let upper = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value >= upper {
|
||||
return Err(error(&format!("bad {name} count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = int(num()?, "n")?;
|
||||
let m = int(num()?, "m")?;
|
||||
let p = int(num()?, "p")?;
|
||||
let q = int(num()?, "q")?;
|
||||
let rat = num()? != 0.0;
|
||||
let rows = n
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error("row count overflow"))?;
|
||||
let cols = m
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error("column count overflow"))?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error("u knot count overflow"))?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error("v knot count overflow"))?;
|
||||
let required = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rat { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(|| error("required token count overflow"))?;
|
||||
if required > available {
|
||||
return Err(error(&format!(
|
||||
"truncated: need {required} data tokens, have {available}"
|
||||
)));
|
||||
}
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rat { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(ParsedSrf { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Allocation-checked SMS decoding shared by the runner and non-C++ regression tests.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
pub(super) struct SmsSurface {
|
||||
pub(super) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(super) p: usize,
|
||||
pub(super) q: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
pub(super) v: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(super) fn decode(text: &str, path: &str) -> Result<SmsSurface, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
let number = |tokens: &mut std::str::SplitWhitespace<'_>| {
|
||||
tokens
|
||||
.next()
|
||||
.ok_or_else(|| error("truncated"))?
|
||||
.parse::<f64>()
|
||||
.map_err(|e| error(&e.to_string()))
|
||||
};
|
||||
let count = |value: f64| {
|
||||
// usize::MAX rounds up to 2^BITS as f64 on 64-bit targets. Use the
|
||||
// exclusive power-of-two bound, not that rounded maximum, before casting.
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value >= exclusive {
|
||||
return Err(error(&format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let mut tokens = text.split_whitespace();
|
||||
let n = count(number(&mut tokens)?)?;
|
||||
let m = count(number(&mut tokens)?)?;
|
||||
let p = count(number(&mut tokens)?)?;
|
||||
let q = count(number(&mut tokens)?)?;
|
||||
let rational = number(&mut tokens)? != 0.0;
|
||||
let overflow = || error("count overflow");
|
||||
let rows = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let cols = m.checked_add(1).ok_or_else(overflow)?;
|
||||
let u_len = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let v_len = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rational { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_len))
|
||||
.and_then(|v| v.checked_add(v_len))
|
||||
.ok_or_else(overflow)?;
|
||||
|
||||
// No header-sized allocation until the entire consumed payload is present.
|
||||
// Parse the available prefix even on truncation, preserving the old lazy
|
||||
// parser's first numeric error. Never inspect ignored trailing tokens.
|
||||
let mut preflight = tokens.clone();
|
||||
for _ in 0..payload {
|
||||
number(&mut preflight)?;
|
||||
}
|
||||
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (
|
||||
number(&mut tokens)?,
|
||||
number(&mut tokens)?,
|
||||
number(&mut tokens)?,
|
||||
);
|
||||
let w = if rational { number(&mut tokens)? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_len {
|
||||
u.push(number(&mut tokens)?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_len {
|
||||
v.push(number(&mut tokens)?);
|
||||
}
|
||||
Ok(SmsSurface { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//! Lazy SMS decoding shared by the surface-tool-p2 runner and non-C++ tests.
|
||||
//!
|
||||
//! Deviations: validate counts and checked payload sizes before incremental growth.
|
||||
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
pub(super) struct SmsSurface {
|
||||
pub net: Vec<Vec<[f64; 4]>>,
|
||||
pub p: usize,
|
||||
pub q: usize,
|
||||
pub u: Vec<f64>,
|
||||
pub v: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(super) fn parse_sms(text: &str, path: &str) -> Result<SmsSurface, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
let mut tokens = text.split_whitespace();
|
||||
let number = |tokens: &mut std::str::SplitWhitespace<'_>| {
|
||||
tokens
|
||||
.next()
|
||||
.ok_or_else(|| error("truncated"))?
|
||||
.parse::<f64>()
|
||||
.map_err(|e| error(&e.to_string()))
|
||||
};
|
||||
let count = |value: f64| {
|
||||
// On 64-bit hosts usize::MAX rounds up as f64; use an exclusive limit.
|
||||
if !value.is_finite()
|
||||
|| value.fract() != 0.0
|
||||
|| value < 0.0
|
||||
|| value >= 2.0_f64.powi(usize::BITS as i32)
|
||||
{
|
||||
return Err(error(&format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = count(number(&mut tokens)?)?;
|
||||
let m = count(number(&mut tokens)?)?;
|
||||
let p = count(number(&mut tokens)?)?;
|
||||
let q = count(number(&mut tokens)?)?;
|
||||
let rat = number(&mut tokens)? != 0.0;
|
||||
let overflow = || error("SMS size overflow");
|
||||
let rows = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let cols = m.checked_add(1).ok_or_else(overflow)?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rat { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(overflow)?;
|
||||
|
||||
// Count whitespace tokens, not parsed numbers: unused tails remain ignored.
|
||||
// A short payload is scanned without allocating, preserving the first bad
|
||||
// consumed number instead of hiding it behind the later truncation.
|
||||
if tokens.clone().count() < payload {
|
||||
while tokens.clone().next().is_some() {
|
||||
number(&mut tokens)?;
|
||||
}
|
||||
return Err(error("truncated"));
|
||||
}
|
||||
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (
|
||||
number(&mut tokens)?,
|
||||
number(&mut tokens)?,
|
||||
number(&mut tokens)?,
|
||||
);
|
||||
let w = if rat { number(&mut tokens)? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(number(&mut tokens)?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(number(&mut tokens)?);
|
||||
}
|
||||
Ok(SmsSurface { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Pure SMS fixture decoder shared by the runner and non-C++ regression tests.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
pub(super) struct ParsedSrf {
|
||||
pub net: Vec<Vec<[f64; 4]>>,
|
||||
pub p: usize,
|
||||
pub q: usize,
|
||||
pub u: Vec<f64>,
|
||||
pub v: Vec<f64>,
|
||||
}
|
||||
|
||||
fn error(path: &str, message: impl std::fmt::Display) -> HarnessError {
|
||||
HarnessError::new(format!("{path}: {message}"))
|
||||
}
|
||||
|
||||
fn count(value: f64, path: &str) -> Result<usize, HarnessError> {
|
||||
if !value.is_finite()
|
||||
|| value < 0.0
|
||||
|| value.fract() != 0.0
|
||||
|| value >= 2.0_f64.powi(usize::BITS as i32)
|
||||
{
|
||||
return Err(error(path, format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
}
|
||||
|
||||
pub(super) fn decode(text: &str, path: &str) -> Result<ParsedSrf, HarnessError> {
|
||||
// Eager lexical validation deliberately includes even the ignored numeric tail.
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|token| token.parse::<f64>().map_err(|e| error(path, e)))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut values = values.into_iter();
|
||||
let mut num = || values.next().ok_or_else(|| error(path, "truncated"));
|
||||
let n = count(num()?, path)?;
|
||||
let m = count(num()?, path)?;
|
||||
let p = count(num()?, path)?;
|
||||
let q = count(num()?, path)?;
|
||||
let rational = num()? != 0.0;
|
||||
let overflow = || error(path, "count overflow");
|
||||
let rows = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let cols = m.checked_add(1).ok_or_else(overflow)?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rational { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(overflow)?;
|
||||
if payload > values.len() {
|
||||
return Err(error(path, "truncated"));
|
||||
}
|
||||
let mut num = || values.next().ok_or_else(|| error(path, "truncated"));
|
||||
// Never reserve from declared counts, even after the numeric payload preflight.
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rational { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(ParsedSrf { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Pure SMS fixture decoder for the B-spline surface NLib p2 runner.
|
||||
//!
|
||||
//! Retains eager lexical parsing and stored homogeneous coordinates. Header sizes
|
||||
//! are checked against actual numeric payload before any decoded output grows;
|
||||
//! geometric validity remains the downstream surface constructor's responsibility.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Decoded {
|
||||
pub net: Vec<Vec<[f64; 4]>>,
|
||||
pub p: usize,
|
||||
pub q: usize,
|
||||
pub u: Vec<f64>,
|
||||
pub v: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(super) fn decode(text: &str, path: &str) -> Result<Decoded, HarnessError> {
|
||||
// Parse even unused tails before inspecting headers, as the original reader did.
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|t| {
|
||||
t.parse::<f64>()
|
||||
.map_err(|e| HarnessError::new(format!("{path}: {e}")))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut it = values.into_iter();
|
||||
let truncated = || HarnessError::new(format!("{path}: truncated"));
|
||||
let overflow = || HarnessError::new(format!("{path}: count overflow"));
|
||||
let count = |v: f64| {
|
||||
// usize::MAX as f64 rounds up on 64-bit: the boundary must be exclusive.
|
||||
if !v.is_finite() || v.fract() != 0.0 || v < 0.0 || v >= 2.0_f64.powi(usize::BITS as i32) {
|
||||
Err(HarnessError::new(format!("{path}: bad count {v}")))
|
||||
} else {
|
||||
Ok(v as usize)
|
||||
}
|
||||
};
|
||||
let n = count(it.next().ok_or_else(truncated)?)?;
|
||||
let m = count(it.next().ok_or_else(truncated)?)?;
|
||||
let p = count(it.next().ok_or_else(truncated)?)?;
|
||||
let q = count(it.next().ok_or_else(truncated)?)?;
|
||||
let rat = it.next().ok_or_else(truncated)? != 0.0;
|
||||
let rows = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let cols = m.checked_add(1).ok_or_else(overflow)?;
|
||||
let points = rows.checked_mul(cols).ok_or_else(overflow)?;
|
||||
let u_len = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let v_len = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let coordinates = points
|
||||
.checked_mul(3 + usize::from(rat))
|
||||
.ok_or_else(overflow)?;
|
||||
let payload = coordinates
|
||||
.checked_add(u_len)
|
||||
.and_then(|v| v.checked_add(v_len))
|
||||
.ok_or_else(overflow)?;
|
||||
if payload > it.len() {
|
||||
return Err(truncated());
|
||||
}
|
||||
let mut num = || it.next().ok_or_else(truncated);
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
row.push([num()?, num()?, num()?, if rat { num()? } else { NOW }]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_len {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_len {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(Decoded { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//! Pure, lazy numeric decoder for the p1 surface runner's SMS fixtures.
|
||||
//!
|
||||
//! Shape arithmetic and token availability are checked before output allocation.
|
||||
//! Only consumed numbers are parsed; arbitrary trailing tokens are ignored.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
/// Stored (not dehomogenized) surface data, with the v index varying fastest.
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedSms {
|
||||
/// Stored coordinates and weights, with rows in u.
|
||||
pub net: Vec<Vec<[f64; 4]>>,
|
||||
/// Degree in u, without geometry-policy restrictions.
|
||||
pub p: usize,
|
||||
/// Degree in v, without geometry-policy restrictions.
|
||||
pub q: usize,
|
||||
/// Knots in u.
|
||||
pub u: Vec<f64>,
|
||||
/// Knots in v.
|
||||
pub v: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Decode the consumed SMS header and payload, qualifying errors with `path`.
|
||||
pub fn parse_sms(text: &str, path: &str) -> Result<ParsedSms, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
let mut tokens = text.split_whitespace();
|
||||
let number = |tokens: &mut std::str::SplitWhitespace<'_>| {
|
||||
tokens
|
||||
.next()
|
||||
.ok_or_else(|| error("truncated"))?
|
||||
.parse::<f64>()
|
||||
.map_err(|e| error(&e.to_string()))
|
||||
};
|
||||
let count = |value: f64| {
|
||||
// usize::MAX rounds up on 64-bit targets, so this bound is exclusive.
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value >= exclusive {
|
||||
Err(error(&format!("bad count {value}")))
|
||||
} else {
|
||||
Ok(value as usize)
|
||||
}
|
||||
};
|
||||
let n = count(number(&mut tokens)?)?;
|
||||
let m = count(number(&mut tokens)?)?;
|
||||
let p = count(number(&mut tokens)?)?;
|
||||
let q = count(number(&mut tokens)?)?;
|
||||
let rational = number(&mut tokens)? != 0.0;
|
||||
let overflow = || error("SMS size overflow");
|
||||
let rows = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let cols = m.checked_add(1).ok_or_else(overflow)?;
|
||||
let u_len = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let v_len = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let points = rows.checked_mul(cols).ok_or_else(overflow)?;
|
||||
let coordinates = points
|
||||
.checked_mul(if rational { 4 } else { 3 })
|
||||
.ok_or_else(overflow)?;
|
||||
let required = coordinates
|
||||
.checked_add(u_len)
|
||||
.and_then(|v| v.checked_add(v_len))
|
||||
.ok_or_else(overflow)?;
|
||||
if tokens.clone().count() < required {
|
||||
// Preserve the first consumed malformed number ahead of a later EOF,
|
||||
// without allocating any output for a short body.
|
||||
for token in tokens {
|
||||
token.parse::<f64>().map_err(|e| error(&e.to_string()))?;
|
||||
}
|
||||
return Err(error("truncated"));
|
||||
}
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let x = number(&mut tokens)?;
|
||||
let y = number(&mut tokens)?;
|
||||
let z = number(&mut tokens)?;
|
||||
let w = if rational { number(&mut tokens)? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_len {
|
||||
u.push(number(&mut tokens)?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_len {
|
||||
v.push(number(&mut tokens)?);
|
||||
}
|
||||
Ok(ParsedSms { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//! Lazy SMS surface decoding shared by the runner and non-C++ regression tests.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
/// Raw SMS data; geometry validation remains the surface constructor's job.
|
||||
#[derive(Debug)]
|
||||
pub struct SmsSurface {
|
||||
/// Homogeneous control net, v index fastest.
|
||||
pub net: Vec<Vec<[f64; 4]>>,
|
||||
/// U degree.
|
||||
pub p: usize,
|
||||
/// V degree.
|
||||
pub q: usize,
|
||||
/// U knots.
|
||||
pub u: Vec<f64>,
|
||||
/// V knots.
|
||||
pub v: Vec<f64>,
|
||||
}
|
||||
|
||||
fn number(it: &mut std::str::SplitWhitespace<'_>, path: &str) -> Result<f64, HarnessError> {
|
||||
it.next()
|
||||
.ok_or_else(|| HarnessError::new(format!("{path}: truncated")))?
|
||||
.parse::<f64>()
|
||||
.map_err(|e| HarnessError::new(format!("{path}: {e}")))
|
||||
}
|
||||
|
||||
fn count(it: &mut std::str::SplitWhitespace<'_>, path: &str) -> Result<usize, HarnessError> {
|
||||
let value = number(it, path)?;
|
||||
// usize::MAX rounds up on 64-bit hosts: the upper bound must be exclusive.
|
||||
if !value.is_finite()
|
||||
|| value.fract() != 0.0
|
||||
|| value < 0.0
|
||||
|| value >= 2.0_f64.powi(usize::BITS as i32)
|
||||
{
|
||||
return Err(HarnessError::new(format!("{path}: bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
}
|
||||
|
||||
/// Decode only the consumed prefix, ignoring even malformed trailing tokens.
|
||||
pub fn parse_sms(text: &str, path: &str) -> Result<SmsSurface, HarnessError> {
|
||||
let mut it = text.split_whitespace();
|
||||
let n = count(&mut it, path)?;
|
||||
let m = count(&mut it, path)?;
|
||||
let p = count(&mut it, path)?;
|
||||
let q = count(&mut it, path)?;
|
||||
let rational = number(&mut it, path)? != 0.0;
|
||||
let overflow = || HarnessError::new(format!("{path}: SMS size overflow"));
|
||||
let rows = n.checked_add(1).ok_or_else(overflow)?;
|
||||
let cols = m.checked_add(1).ok_or_else(overflow)?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(overflow)?;
|
||||
let required = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rational { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(overflow)?;
|
||||
|
||||
// Count without allocating or parsing. On short input, preserve the first
|
||||
// available bad float ahead of truncation, still before allocating output.
|
||||
if it.clone().count() < required {
|
||||
while it.clone().next().is_some() {
|
||||
number(&mut it, path)?;
|
||||
}
|
||||
return Err(HarnessError::new(format!("{path}: truncated")));
|
||||
}
|
||||
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let x = number(&mut it, path)?;
|
||||
let y = number(&mut it, path)?;
|
||||
let z = number(&mut it, path)?;
|
||||
let w = if rational {
|
||||
number(&mut it, path)?
|
||||
} else {
|
||||
NOW
|
||||
};
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(number(&mut it, path)?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(number(&mut it, path)?);
|
||||
}
|
||||
Ok(SmsSurface { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Pure text reader for query.tess SMS fixtures; no geometry validation is performed.
|
||||
//!
|
||||
//! Counts are bounded by usize's exclusive upper limit. Payload checks are staged:
|
||||
//! net truncation precedes U arithmetic, and U consumption precedes V arithmetic.
|
||||
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
/// Surface data in the original homogeneous, v-fastest storage order.
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedSurface {
|
||||
/// Control net, with rows in u.
|
||||
pub net: Vec<Vec<[f64; 4]>>,
|
||||
/// U degree.
|
||||
pub p: usize,
|
||||
/// V degree.
|
||||
pub q: usize,
|
||||
/// U knots.
|
||||
pub u: Vec<f64>,
|
||||
/// V knots.
|
||||
pub v: Vec<f64>,
|
||||
}
|
||||
|
||||
fn number(it: &mut std::vec::IntoIter<f64>) -> Result<f64, String> {
|
||||
it.next().ok_or_else(|| "truncated".to_owned())
|
||||
}
|
||||
|
||||
fn count(it: &mut std::vec::IntoIter<f64>) -> Result<usize, String> {
|
||||
let value = number(it)?;
|
||||
// usize::MAX rounds UP on 64-bit, so comparison with MAX as f64 is unsafe.
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value >= exclusive {
|
||||
return Err(format!("bad count {value}"));
|
||||
}
|
||||
Ok(value as usize)
|
||||
}
|
||||
|
||||
fn knots(it: &mut std::vec::IntoIter<f64>, last: usize, degree: usize) -> Result<Vec<f64>, String> {
|
||||
let len = last
|
||||
.checked_add(degree)
|
||||
.and_then(|n| n.checked_add(2))
|
||||
.ok_or_else(|| "count overflow".to_owned())?;
|
||||
if len > it.len() {
|
||||
return Err("truncated".to_owned());
|
||||
}
|
||||
let mut values = Vec::new();
|
||||
for _ in 0..len {
|
||||
values.push(number(it)?);
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
/// Parse all numeric tokens eagerly, including ignored trailing tokens, before headers.
|
||||
pub fn parse(text: &str) -> Result<ParsedSurface, String> {
|
||||
let mut values = Vec::new();
|
||||
for token in text.split_whitespace() {
|
||||
values.push(token.parse::<f64>().map_err(|e| e.to_string())?);
|
||||
}
|
||||
let mut it = values.into_iter();
|
||||
let n = count(&mut it)?;
|
||||
let m = count(&mut it)?;
|
||||
let p = count(&mut it)?;
|
||||
let q = count(&mut it)?;
|
||||
let rational = number(&mut it)? != 0.0;
|
||||
// Only preflight the net here: later knot arithmetic must not mask its EOF.
|
||||
let net_len = n
|
||||
.checked_add(1)
|
||||
.and_then(|rows| m.checked_add(1).and_then(|cols| rows.checked_mul(cols)))
|
||||
.and_then(|points| points.checked_mul(if rational { 4 } else { 3 }));
|
||||
if net_len.is_none_or(|len| len > it.len()) {
|
||||
return Err("truncated".to_owned());
|
||||
}
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..=n {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..=m {
|
||||
let (x, y, z) = (number(&mut it)?, number(&mut it)?, number(&mut it)?);
|
||||
let w = if rational { number(&mut it)? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let u = knots(&mut it, n, p)?;
|
||||
let v = knots(&mut it, m, q)?;
|
||||
Ok(ParsedSurface { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
//! Runner-local SMS/SMC decoding, included by non-C++ regression tests.
|
||||
//!
|
||||
//! Deviations: validate counts, check arithmetic and available numeric tokens
|
||||
//! before constructing geometry storage, and grow it only as values are decoded.
|
||||
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
/// Decoded surface storage, with the v index fastest.
|
||||
pub(super) struct ParsedSrf {
|
||||
pub(super) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(super) p: usize,
|
||||
pub(super) q: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
pub(super) v: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Decoded curve storage, with coordinates and weights stored verbatim.
|
||||
pub(super) struct ParsedCrv {
|
||||
pub(super) dim: u64,
|
||||
pub(super) cpts: Vec<[f64; 4]>,
|
||||
pub(super) p: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
}
|
||||
|
||||
fn error(path: &str, message: impl std::fmt::Display) -> HarnessError {
|
||||
HarnessError::new(format!("{path}: {message}"))
|
||||
}
|
||||
|
||||
fn numbers(text: &str, path: &str) -> Result<Vec<f64>, HarnessError> {
|
||||
// Preserve eager numeric parsing, including otherwise-unused trailing tokens.
|
||||
// This allocation depends only on actual input, never declared dimensions.
|
||||
text.split_whitespace()
|
||||
.map(|t| t.parse::<f64>().map_err(|e| error(path, e)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn int(path: &str, value: f64, name: &str) -> Result<usize, HarnessError> {
|
||||
// usize::MAX rounds up to 2^BITS as f64 on 64-bit hosts. The exclusive
|
||||
// power-of-two bound rejects that unrepresentable value without saturation.
|
||||
let upper = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value >= upper {
|
||||
return Err(error(path, format!("bad {name} count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
}
|
||||
|
||||
fn ensure_available(path: &str, required: usize, available: usize) -> Result<(), HarnessError> {
|
||||
if required > available {
|
||||
return Err(error(
|
||||
path,
|
||||
format!("truncated: need {required} data tokens, have {available}"),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decodes `n m p q`, a rational flag, points, and u/v knots.
|
||||
pub(super) fn parse_sms(text: &str, path: &str) -> Result<ParsedSrf, HarnessError> {
|
||||
let all = numbers(text, path)?;
|
||||
let available = all.len().saturating_sub(5);
|
||||
let mut it = all.into_iter();
|
||||
let mut num = || it.next().ok_or_else(|| error(path, "truncated"));
|
||||
let n = int(path, num()?, "n")?;
|
||||
let m = int(path, num()?, "m")?;
|
||||
let p = int(path, num()?, "p")?;
|
||||
let q = int(path, num()?, "q")?;
|
||||
let rat = num()? != 0.0;
|
||||
let rows = n
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error(path, "row count overflow"))?;
|
||||
let cols = m
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error(path, "column count overflow"))?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error(path, "u knot count overflow"))?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error(path, "v knot count overflow"))?;
|
||||
let points = rows
|
||||
.checked_mul(cols)
|
||||
.ok_or_else(|| error(path, "point count overflow"))?;
|
||||
let required = points
|
||||
.checked_mul(if rat { 4 } else { 3 })
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(|| error(path, "required token count overflow"))?;
|
||||
ensure_available(path, required, available)?;
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rat { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(ParsedSrf { net, p, q, u, v })
|
||||
}
|
||||
|
||||
/// Decodes `n p`, a rational flag, dimension, points, and knots (version <= 32).
|
||||
pub(super) fn parse_smc(text: &str, path: &str) -> Result<ParsedCrv, HarnessError> {
|
||||
let all = numbers(text, path)?;
|
||||
let available = all.len().saturating_sub(4);
|
||||
let mut it = all.into_iter();
|
||||
let mut num = || it.next().ok_or_else(|| error(path, "truncated"));
|
||||
let n = int(path, num()?, "n")?;
|
||||
let p = int(path, num()?, "p")?;
|
||||
let rat = num()? != 0.0;
|
||||
let dim = int(path, num()?, "dimension")? as u64;
|
||||
let points = n
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error(path, "point count overflow"))?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error(path, "u knot count overflow"))?;
|
||||
// Preserve the original dimension branch: exactly 2 omits z; all others read z.
|
||||
let width = (if dim == 2 { 2_usize } else { 3_usize })
|
||||
.checked_add(usize::from(rat))
|
||||
.ok_or_else(|| error(path, "coordinate width overflow"))?;
|
||||
let required = points
|
||||
.checked_mul(width)
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.ok_or_else(|| error(path, "required token count overflow"))?;
|
||||
ensure_available(path, required, available)?;
|
||||
let mut cpts = Vec::new();
|
||||
for _ in 0..points {
|
||||
let x = num()?;
|
||||
let y = num()?;
|
||||
let z = if dim == 2 { 0.0 } else { num()? };
|
||||
let w = if rat { num()? } else { NOW };
|
||||
cpts.push([x, y, z, w]);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(num()?);
|
||||
}
|
||||
Ok(ParsedCrv { dim, cpts, p, u })
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//! Pure decoder for offset-surface SMS fixtures.
|
||||
//!
|
||||
//! This preserves the runner's eager lexical pass, raw weighted points and
|
||||
//! net → U → V error order. Counts have an exclusive usize boundary and knot
|
||||
//! additions are checked at their original consumption positions. Storage grows
|
||||
//! only after reads; there are no header-derived reservations or geometry caps.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
/// Raw SMS data, without downstream geometry validation or adapter defaults.
|
||||
pub(crate) struct ParsedSrf {
|
||||
pub(crate) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(crate) p: usize,
|
||||
pub(crate) q: usize,
|
||||
pub(crate) u: Vec<f64>,
|
||||
pub(crate) v: Vec<f64>,
|
||||
}
|
||||
|
||||
fn count(path: &str, value: f64) -> Result<usize, HarnessError> {
|
||||
let exclusive_upper = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value >= exclusive_upper {
|
||||
return Err(HarnessError::new(format!("{path}: bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
}
|
||||
|
||||
/// Checked knot length, also callable by bounded arithmetic regression tests.
|
||||
pub(crate) fn knot_count(
|
||||
index: usize,
|
||||
degree: usize,
|
||||
path: &str,
|
||||
axis: &str,
|
||||
) -> Result<usize, HarnessError> {
|
||||
index
|
||||
.checked_add(degree)
|
||||
.and_then(|sum| sum.checked_add(2))
|
||||
.ok_or_else(|| HarnessError::new(format!("{path}: {axis} knot count overflow")))
|
||||
}
|
||||
|
||||
/// Decode all numeric tokens eagerly; extra numeric values remain ignored.
|
||||
pub(crate) fn parse_sms(text: &str, path: &str) -> Result<ParsedSrf, HarnessError> {
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|token| {
|
||||
token
|
||||
.parse::<f64>()
|
||||
.map_err(|error| HarnessError::new(format!("{path}: {error}")))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut values = values.into_iter();
|
||||
let mut num = || {
|
||||
values
|
||||
.next()
|
||||
.ok_or_else(|| HarnessError::new(format!("{path}: truncated")))
|
||||
};
|
||||
let n = count(path, num()?)?;
|
||||
let m = count(path, num()?)?;
|
||||
let p = count(path, num()?)?;
|
||||
let q = count(path, num()?)?;
|
||||
let rational = num()? != 0.0;
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..=n {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..=m {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rational { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..knot_count(n, p, path, "u")? {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..knot_count(m, q, path, "v")? {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(ParsedSrf { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Pure runner-local SMS decoding, shared with non-C++ regression tests.
|
||||
//!
|
||||
//! Deviations: checked counts/arithmetic and actual numeric payload preflight before
|
||||
//! incremental net/knot construction. Preserve eager float parsing (including tails)
|
||||
//! before header validation; numeric tails remain ignored. No geometry validity caps.
|
||||
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
/// Decoded storage, independent of the C++-gated runner; v index is fastest.
|
||||
pub(super) struct ParsedSurface {
|
||||
pub(super) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(super) p: usize,
|
||||
pub(super) q: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
pub(super) v: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Parses all floats before interpreting `n m p q rat`, points, and u/v knots.
|
||||
pub(super) fn parse_sms(text: &str, path: &str) -> Result<ParsedSurface, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|token| token.parse::<f64>().map_err(|e| error(&e.to_string())))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let available = values.len().saturating_sub(5);
|
||||
let mut it = values.into_iter();
|
||||
let mut num = || it.next().ok_or_else(|| error("truncated"));
|
||||
let count = |value: f64| {
|
||||
// usize::MAX rounds upward on 64-bit hosts. The upper bound is exclusive.
|
||||
let upper = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value >= upper {
|
||||
return Err(error(&format!("bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = count(num()?)?;
|
||||
let m = count(num()?)?;
|
||||
let p = count(num()?)?;
|
||||
let q = count(num()?)?;
|
||||
let rat = num()? != 0.0;
|
||||
let rows = n
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error("row count overflow"))?;
|
||||
let cols = m
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error("column count overflow"))?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error("u knot count overflow"))?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error("v knot count overflow"))?;
|
||||
let required = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rat { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(|| error("required token count overflow"))?;
|
||||
if required > available {
|
||||
return Err(error("truncated"));
|
||||
}
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rat { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(ParsedSurface { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//! Private SMS fixture reader shared by the P5 runner and non-C++ tests.
|
||||
//!
|
||||
//! Deviations: reject unrepresentable counts and overflowing knot lengths rather
|
||||
//! than saturating casts or overflowing arithmetic. Preserve eager float parsing
|
||||
//! and the original net, U-knot, V-knot processing/error order.
|
||||
|
||||
use crate::HarnessError;
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
pub(crate) struct ParsedSurface {
|
||||
pub(crate) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(crate) p: usize,
|
||||
pub(crate) q: usize,
|
||||
pub(crate) u: Vec<f64>,
|
||||
pub(crate) v: Vec<f64>,
|
||||
}
|
||||
|
||||
pub(crate) fn read_sms(path: &str) -> Result<ParsedSurface, HarnessError> {
|
||||
let text =
|
||||
std::fs::read_to_string(path).map_err(|e| HarnessError::new(format!("{path}: {e}")))?;
|
||||
parse_sms(path, &text)
|
||||
}
|
||||
|
||||
fn count(path: &str, value: f64) -> Result<usize, HarnessError> {
|
||||
// usize::MAX rounds up on 64-bit hosts. Use the exclusive power-of-two
|
||||
// bound instead, so a saturating cast can never accept that rounded value.
|
||||
let exclusive_bound = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value >= exclusive_bound {
|
||||
return Err(HarnessError::new(format!("{path}: bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
}
|
||||
|
||||
fn knot_count(path: &str, n: usize, degree: usize) -> Result<usize, HarnessError> {
|
||||
n.checked_add(degree)
|
||||
.and_then(|sum| sum.checked_add(2))
|
||||
.ok_or_else(|| HarnessError::new(format!("{path}: knot count overflow")))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_sms(path: &str, text: &str) -> Result<ParsedSurface, HarnessError> {
|
||||
// Even ignored tails must be lexically valid, before any header validation.
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|token| {
|
||||
token
|
||||
.parse::<f64>()
|
||||
.map_err(|e| HarnessError::new(format!("{path}: {e}")))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut it = values.into_iter();
|
||||
let mut num = || {
|
||||
it.next()
|
||||
.ok_or_else(|| HarnessError::new(format!("{path}: truncated")))
|
||||
};
|
||||
let n = count(path, num()?)?;
|
||||
let m = count(path, num()?)?;
|
||||
let p = count(path, num()?)?;
|
||||
let q = count(path, num()?)?;
|
||||
let rational = num()? != 0.0;
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..=n {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..=m {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rational { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
// Compute each length only at its original processing position. In
|
||||
// particular, V overflow must not mask missing U data (nor net truncation).
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..knot_count(path, n, p)? {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..knot_count(path, m, q)? {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(ParsedSurface { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Pure SMS reader shared by the surface P6 runner and non-C++ tests.
|
||||
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
pub(crate) struct ParsedSurface {
|
||||
pub(crate) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(crate) p: usize,
|
||||
pub(crate) q: usize,
|
||||
pub(crate) u: Vec<f64>,
|
||||
pub(crate) v: Vec<f64>,
|
||||
}
|
||||
|
||||
fn count(path: &str, value: f64) -> Result<usize, HarnessError> {
|
||||
// usize::MAX rounds up to 2^BITS on 64-bit targets: compare exclusively.
|
||||
let upper = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value >= upper {
|
||||
return Err(HarnessError::new(format!("{path}: bad count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
}
|
||||
|
||||
pub(crate) fn knot_count(
|
||||
last: usize,
|
||||
degree: usize,
|
||||
axis: &str,
|
||||
path: &str,
|
||||
) -> Result<usize, HarnessError> {
|
||||
last.checked_add(degree)
|
||||
.and_then(|sum| sum.checked_add(2))
|
||||
.ok_or_else(|| HarnessError::new(format!("{path}: {axis} knot count overflow")))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_sms(text: &str, path: &str) -> Result<ParsedSurface, HarnessError> {
|
||||
// Deliberately eager: even a malformed ignored tail precedes header errors.
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|token| {
|
||||
token
|
||||
.parse::<f64>()
|
||||
.map_err(|error| HarnessError::new(format!("{path}: {error}")))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut values = values.into_iter();
|
||||
let mut num = || {
|
||||
values
|
||||
.next()
|
||||
.ok_or_else(|| HarnessError::new(format!("{path}: truncated")))
|
||||
};
|
||||
let n = count(path, num()?)?;
|
||||
let m = count(path, num()?)?;
|
||||
let p = count(path, num()?)?;
|
||||
let q = count(path, num()?)?;
|
||||
let rational = num()? != 0.0;
|
||||
// Grow only after actual reads, never from declared sizes or iterator hints.
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..=n {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..=m {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rational { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
// Preserve net-before-U-before-V error precedence, including arithmetic.
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..knot_count(n, p, "u", path)? {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..knot_count(m, q, "v", path)? {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(ParsedSurface { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Standalone decoder for the P7 runner's SMS text input.
|
||||
//!
|
||||
//! Counts are rounded through f64 as before, but must fit the exclusive usize
|
||||
//! bound. Knot additions are checked at their original processing positions:
|
||||
//! net first, then u sizing/consumption, then v sizing/consumption. These formerly
|
||||
//! overflowing sizes now return errors. Geometry validation remains downstream.
|
||||
|
||||
/// Text payload, before the adapter supplies the B-spline form and oob defaults.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Parsed {
|
||||
pub(crate) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(crate) p: usize,
|
||||
pub(crate) q: usize,
|
||||
pub(crate) u: Vec<f64>,
|
||||
pub(crate) v: Vec<f64>,
|
||||
}
|
||||
|
||||
fn count(path: &str, value: f64) -> Result<usize, String> {
|
||||
// Unlike usize::MAX as f64, this is exclusive even when MAX rounds upward.
|
||||
let exclusive = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value >= exclusive {
|
||||
return Err(format!("{path}: bad count {value}"));
|
||||
}
|
||||
Ok(value as usize)
|
||||
}
|
||||
|
||||
/// Checked left-to-right knot sizing, shared with the focused arithmetic tests.
|
||||
pub(crate) fn knot_count(path: &str, n: usize, p: usize) -> Result<usize, String> {
|
||||
n.checked_add(p)
|
||||
.and_then(|sum| sum.checked_add(2))
|
||||
.ok_or_else(|| format!("{path}: knot count overflow"))
|
||||
}
|
||||
|
||||
/// Decode all floats eagerly, retaining path-qualified errors and numeric tails.
|
||||
/// `now` is the production NOW sentinel for nonrational control points.
|
||||
pub(crate) fn parse(path: &str, text: &str, now: f64) -> Result<Parsed, String> {
|
||||
let values = text
|
||||
.split_whitespace()
|
||||
.map(|token| token.parse::<f64>().map_err(|e| format!("{path}: {e}")))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut it = values.into_iter();
|
||||
let mut num = || it.next().ok_or_else(|| format!("{path}: truncated"));
|
||||
let n = count(path, num()?)?;
|
||||
let m = count(path, num()?)?;
|
||||
let p = count(path, num()?)?;
|
||||
let q = count(path, num()?)?;
|
||||
let rational = num()? != 0.0;
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..=n {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..=m {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rational { num()? } else { now };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..knot_count(path, n, p)? {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..knot_count(path, m, q)? {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(Parsed { net, p, q, u, v })
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Runner-local native B-rep record decoding, shared with non-C++ regression tests.
|
||||
//!
|
||||
//! Deviations: validate counts before conversion, bound declared records and lists
|
||||
//! by available tokens before iteration, and grow storage incrementally. Valid
|
||||
//! record order, integer conversion and floating-point bits remain unchanged.
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
/// A plain integer, record link, ordered link list, or real field.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Slot {
|
||||
I,
|
||||
R(usize),
|
||||
L(usize),
|
||||
F,
|
||||
}
|
||||
use Slot::{F, I, L, R};
|
||||
|
||||
/// Fields in the shim's `emit_records` order, one layout per record kind.
|
||||
const LAYOUT: [&[Slot]; 10] = [
|
||||
// region: in B-rep, shells
|
||||
&[I, L(1)],
|
||||
// shell: region, type, list head present, face/edge/vertex use heads
|
||||
&[R(0), I, I, R(2), R(6), R(8)],
|
||||
// face use: shell, face, mate, orientation, loop uses
|
||||
&[R(1), R(3), R(2), I, L(4)],
|
||||
// face: face use, domain, trim, tolerance, surface flags, natural domain,
|
||||
// evaluated flag and centre point
|
||||
&[R(2), F, F, F, F, I, F, I, I, F, F, F, F, I, F, F, F],
|
||||
// loop use: face use, loop, mate, start type, edge/vertex use, orientation
|
||||
&[R(2), R(5), R(4), I, R(6), R(8), I],
|
||||
// loop: loop use
|
||||
&[R(4)],
|
||||
// edge use: edge, owner type, loop use, shell, cw, ccw, vertex use,
|
||||
// orientation, UV trim present
|
||||
&[R(7), I, R(4), R(1), R(6), R(6), R(8), I, I],
|
||||
// edge: tolerance, interval, edge uses, curve present, natural interval
|
||||
&[F, F, F, L(6), I, F, F],
|
||||
// vertex use: vertex, owner type, edge use, loop use, shell, UV point
|
||||
&[R(9), I, R(6), R(4), R(1), F, F],
|
||||
// vertex: point, tolerance, vertex uses
|
||||
&[F, F, F, F, L(8)],
|
||||
];
|
||||
|
||||
/// One record, retaining labels, ordered integer links and real values.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(super) struct Rec {
|
||||
pub(super) label: i64,
|
||||
pub(super) ints: Vec<i64>,
|
||||
pub(super) links: Vec<Option<usize>>,
|
||||
pub(super) floats: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Rec {
|
||||
pub(super) fn new(label: i64) -> Self {
|
||||
Self {
|
||||
label,
|
||||
ints: Vec::new(),
|
||||
links: Vec::new(),
|
||||
floats: Vec::new(),
|
||||
}
|
||||
}
|
||||
pub(super) fn i(&mut self, x: i64) {
|
||||
self.ints.push(x);
|
||||
self.links.push(None);
|
||||
}
|
||||
pub(super) fn r(&mut self, kind: usize, x: i64) {
|
||||
self.ints.push(x);
|
||||
self.links.push(Some(kind));
|
||||
}
|
||||
pub(super) fn l(&mut self, kind: usize, xs: &[i64]) {
|
||||
self.i(xs.len() as i64);
|
||||
for &x in xs {
|
||||
self.r(kind, x);
|
||||
}
|
||||
}
|
||||
pub(super) fn f(&mut self, x: f64) {
|
||||
self.floats.push(x);
|
||||
}
|
||||
}
|
||||
|
||||
/// Records in the shim's ten-kind walk order.
|
||||
pub(super) type Model = [Vec<Rec>; 10];
|
||||
|
||||
fn count(value: f64, name: &str) -> Result<usize, HarnessError> {
|
||||
// usize::MAX rounds up on 64-bit hosts; use the exclusive power-of-two bound.
|
||||
let upper = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value >= upper {
|
||||
return Err(HarnessError::new(format!("bad {name} count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
}
|
||||
|
||||
fn next(input: &mut std::slice::Iter<'_, f64>) -> Result<f64, HarnessError> {
|
||||
input
|
||||
.next()
|
||||
.copied()
|
||||
.ok_or_else(|| HarnessError::new("short record output"))
|
||||
}
|
||||
|
||||
/// Decodes the records the shim wrote, rejecting malformed counts and tails.
|
||||
pub(super) fn parse_records(v: &[f64]) -> Result<Model, HarnessError> {
|
||||
let mut input = v.iter();
|
||||
let mut model: Model = Default::default();
|
||||
for (slots, out) in LAYOUT.iter().zip(model.iter_mut()) {
|
||||
let n = count(next(&mut input)?, "record")?;
|
||||
// Each record needs its label and at least one token per layout slot
|
||||
// (including the count of any empty list). Division avoids overflow.
|
||||
if n > input.len() / (slots.len() + 1) {
|
||||
return Err(HarnessError::new("short record output: record count"));
|
||||
}
|
||||
for _ in 0..n {
|
||||
let mut rec = Rec::new(next(&mut input)? as i64);
|
||||
for slot in *slots {
|
||||
match *slot {
|
||||
I => rec.i(next(&mut input)? as i64),
|
||||
R(k) => rec.r(k, next(&mut input)? as i64),
|
||||
L(k) => {
|
||||
let count = count(next(&mut input)?, "list")?;
|
||||
if count > input.len() {
|
||||
return Err(HarnessError::new("short record output: list count"));
|
||||
}
|
||||
rec.i(count as i64);
|
||||
for _ in 0..count {
|
||||
rec.r(k, next(&mut input)? as i64);
|
||||
}
|
||||
}
|
||||
F => rec.f(next(&mut input)?),
|
||||
}
|
||||
}
|
||||
out.push(rec);
|
||||
}
|
||||
}
|
||||
if !input.as_slice().is_empty() {
|
||||
return Err(HarnessError::new("trailing record output"));
|
||||
}
|
||||
Ok(model)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//! Runner-local SMS decoding, also included by non-C++ regression tests.
|
||||
//!
|
||||
//! Deviations: validate counts, check arithmetic and available numeric tokens
|
||||
//! before constructing the net, and grow storage only as points/knots are read.
|
||||
|
||||
use solidcore::nurbs::math::NOW;
|
||||
|
||||
use crate::HarnessError;
|
||||
|
||||
/// Decoded surface storage, with the v index fastest.
|
||||
pub(super) struct ParsedSrf {
|
||||
pub(super) net: Vec<Vec<[f64; 4]>>,
|
||||
pub(super) p: usize,
|
||||
pub(super) q: usize,
|
||||
pub(super) u: Vec<f64>,
|
||||
pub(super) v: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Decodes `n m p q`, a rational flag, points, and u/v knots.
|
||||
pub(super) fn parse_sms(text: &str, path: &str) -> Result<ParsedSrf, HarnessError> {
|
||||
let error = |message: &str| HarnessError::new(format!("{path}: {message}"));
|
||||
// Preserve eager float parsing, including trailing tokens. This allocation
|
||||
// depends only on actual input, never on header-declared dimensions.
|
||||
let all = text
|
||||
.split_whitespace()
|
||||
.map(|t| t.parse::<f64>().map_err(|e| error(&e.to_string())))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let available = all.len().saturating_sub(5);
|
||||
let mut it = all.into_iter();
|
||||
let mut num = || it.next().ok_or_else(|| error("truncated"));
|
||||
let int = |value: f64, name: &str| -> Result<usize, HarnessError> {
|
||||
// usize::MAX rounds up on 64-bit hosts; use the exclusive power-of-two bound.
|
||||
let upper = 2.0_f64.powi(usize::BITS as i32);
|
||||
if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value >= upper {
|
||||
return Err(error(&format!("bad {name} count {value}")));
|
||||
}
|
||||
Ok(value as usize)
|
||||
};
|
||||
let n = int(num()?, "n")?;
|
||||
let m = int(num()?, "m")?;
|
||||
let p = int(num()?, "p")?;
|
||||
let q = int(num()?, "q")?;
|
||||
let rat = num()? != 0.0;
|
||||
let rows = n
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error("row count overflow"))?;
|
||||
let cols = m
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| error("column count overflow"))?;
|
||||
let u_count = n
|
||||
.checked_add(p)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error("u knot count overflow"))?;
|
||||
let v_count = m
|
||||
.checked_add(q)
|
||||
.and_then(|v| v.checked_add(2))
|
||||
.ok_or_else(|| error("v knot count overflow"))?;
|
||||
let required = rows
|
||||
.checked_mul(cols)
|
||||
.and_then(|v| v.checked_mul(if rat { 4 } else { 3 }))
|
||||
.and_then(|v| v.checked_add(u_count))
|
||||
.and_then(|v| v.checked_add(v_count))
|
||||
.ok_or_else(|| error("required token count overflow"))?;
|
||||
if required > available {
|
||||
return Err(error(&format!(
|
||||
"truncated: need {required} data tokens, have {available}"
|
||||
)));
|
||||
}
|
||||
let mut net = Vec::new();
|
||||
for _ in 0..rows {
|
||||
let mut row = Vec::new();
|
||||
for _ in 0..cols {
|
||||
let (x, y, z) = (num()?, num()?, num()?);
|
||||
let w = if rat { num()? } else { NOW };
|
||||
row.push([x, y, z, w]);
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let mut u = Vec::new();
|
||||
for _ in 0..u_count {
|
||||
u.push(num()?);
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..v_count {
|
||||
v.push(num()?);
|
||||
}
|
||||
Ok(ParsedSrf { net, p, q, u, v })
|
||||
}
|
||||
@@ -36,6 +36,8 @@
|
||||
//! - `read_indexed`: skipped unless the stream is good and (ASCII) has a
|
||||
//! non-whitespace byte left, or (binary) holds a whole record with count and
|
||||
//! indices `<= 2^20` (upstream reads uninitialised values otherwise).
|
||||
//! Failed ASCII index extraction is compared as `SM_ERR`, not skipped: upstream's
|
||||
//! line skip returns before using the indices, and the shim discards them.
|
||||
//!
|
||||
//! C++ does not expose deletion, so the shim *predicts* which handles die from the
|
||||
//! state before each deleting op (the Rust side reports its store's real liveness).
|
||||
@@ -642,6 +644,23 @@ impl RustRun {
|
||||
let res = self
|
||||
.store
|
||||
.read_indexed_attributes_from_db(&mut self.all, reader);
|
||||
// The safe reader rejects an index that C++ leaves unwritten (or
|
||||
// writes with failbit). Upstream then reaches GOTO_NEXT_ATTLINE:
|
||||
// get() fails and returns SM_ERR before any index is used. The
|
||||
// shim discards those indices on error, so compare that defined
|
||||
// error path rather than skipping it or inventing index values.
|
||||
if matches!(
|
||||
&res,
|
||||
Err(SolidError::InvalidInput {
|
||||
operation: "geom::attribute::AttributeStore::read_indexed_attributes_from_db",
|
||||
reason: "attribute index could not be extracted",
|
||||
})
|
||||
) {
|
||||
if let Some(stream) = reader.in_stream_mut().filter(|s| s.fail()) {
|
||||
let _ = stream.get();
|
||||
return Ok(Step::Status(1001));
|
||||
}
|
||||
}
|
||||
for (i, slot) in self.all.iter().enumerate() {
|
||||
if let Some(h) = slot {
|
||||
if before.get(i).copied().flatten().is_none() {
|
||||
@@ -1424,6 +1443,22 @@ impl UnitDiff for Runner {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn indexed_extraction_failure_compares_native_error_without_placeholders() {
|
||||
for data in ["2 4\nabc\n", "2 4\n", "3 1 tail\n"] {
|
||||
let v = serde_json::json!({
|
||||
"n_objects": 1,
|
||||
"ops": [["read_indexed"], ["read_indexed"]],
|
||||
"stream": {"data": data.as_bytes(), "file_type": 0, "byte_swap": false},
|
||||
});
|
||||
let rust = run_rust_case(&v).unwrap();
|
||||
assert_eq!(rust.ints["status"], vec![1001, ST_SKIP], "{data:?}");
|
||||
assert!(rust.ints["placeholders"].is_empty(), "{data:?}");
|
||||
let cpp = run_cpp_case(&v).unwrap();
|
||||
assert_eq!(rust, cpp, "{data:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every op runs (status 0) in a good share of generated scripts, so skips and
|
||||
/// unresolved references do not hide parts of the unit.
|
||||
#[test]
|
||||
|
||||
@@ -637,6 +637,10 @@ fn apply(m: &mut Model, upd: &mut Vec<usize>, op: &Op) -> solidcore::Result<()>
|
||||
}
|
||||
|
||||
impl UnitDiff for Runner {
|
||||
fn reference_platform(&self) -> Option<crate::platform::ReferencePlatform> {
|
||||
Some(crate::platform::X86_64_HASH_CASTS)
|
||||
}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"geom.hash_table"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
//! degrees and knots selected by `first_patch`). Construction patches and temporary
|
||||
//! subdivision surfaces carry distinct rational bilinear payloads; Rust supplies
|
||||
//! those same payloads through the tree's existing caller-owned resolvers.
|
||||
//! Every run has at least [`MIN_GENERATED`] generated cases.
|
||||
//! Every run has at least [`MIN_GENERATED`] generated cases. Additional
|
||||
//! `patch_lookup` fixtures compare the selected pool identity and the actual NURB,
|
||||
//! including distinct patches with identical geometry.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -107,6 +109,14 @@ enum DOp {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PatchNode {
|
||||
parent: i64,
|
||||
aux: i64,
|
||||
patch: i64,
|
||||
surface: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
enum Inputs {
|
||||
@@ -120,6 +130,129 @@ enum Inputs {
|
||||
#[serde(default = "default_patch_scale")]
|
||||
patch_scale: f64,
|
||||
},
|
||||
PatchLookup {
|
||||
surfaces: Vec<[[f64; 4]; 4]>,
|
||||
owner: i64,
|
||||
nodes: Vec<PatchNode>,
|
||||
queries: Vec<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
fn patch_program(
|
||||
surfaces: &[[[f64; 4]; 4]],
|
||||
owner: i64,
|
||||
nodes: &[PatchNode],
|
||||
queries: &[usize],
|
||||
) -> Vec<f64> {
|
||||
let mut out = vec![surfaces.len() as f64, owner as f64];
|
||||
out.extend(surfaces.iter().flatten().flatten().copied());
|
||||
out.push(nodes.len() as f64);
|
||||
for n in nodes {
|
||||
out.extend([
|
||||
n.parent as f64,
|
||||
n.aux as f64,
|
||||
n.patch as f64,
|
||||
n.surface as f64,
|
||||
]);
|
||||
}
|
||||
out.push(queries.len() as f64);
|
||||
out.extend(queries.iter().map(|&q| q as f64));
|
||||
out
|
||||
}
|
||||
|
||||
fn rust_patch_lookup(
|
||||
surfaces: &[[[f64; 4]; 4]],
|
||||
owner: i64,
|
||||
nodes: &[PatchNode],
|
||||
queries: &[usize],
|
||||
) -> solidcore::Result<Vec<f64>> {
|
||||
use solidcore::nurbs::{math::CPoint, srf_basic::Surface};
|
||||
let surfaces = surfaces
|
||||
.iter()
|
||||
.map(|points| {
|
||||
Surface::new(
|
||||
points
|
||||
.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.map(|p| CPoint::new(p[0], p[1], p[2], p[3]))
|
||||
.collect()
|
||||
})
|
||||
.collect(),
|
||||
1,
|
||||
1,
|
||||
vec![0., 0., 1., 1.],
|
||||
vec![0., 0., 1., 1.],
|
||||
)
|
||||
})
|
||||
.collect::<solidcore::Result<Vec<_>>>()?;
|
||||
let mut tree: Tree<i64> = Tree::new(None);
|
||||
tree.set_owner_object(obj(owner).map(|_| TreeOwner::SurfaceCache));
|
||||
tree.set_owner_surface(obj(owner));
|
||||
for n in nodes {
|
||||
let id = tree.add_node(TreeNode {
|
||||
parent: link(n.parent),
|
||||
aux_type: AuxDataType::from_code(n.aux as i32).unwrap_or_default(),
|
||||
data: NodeData::Surface(SurfaceAux {
|
||||
surface: obj(n.surface),
|
||||
..SurfaceAux::default()
|
||||
}),
|
||||
..TreeNode::new()
|
||||
});
|
||||
if let Some(parent) = link(n.parent) {
|
||||
let p = tree.node_mut(parent)?;
|
||||
if p.child1.is_none() {
|
||||
p.child1 = Some(id);
|
||||
} else {
|
||||
p.child2 = Some(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
tree.set_top_node(Some(NodeId(0)));
|
||||
let mut out = Vec::new();
|
||||
for &query in queries {
|
||||
let selected = tree.first_bezier_patch(
|
||||
NodeId(query),
|
||||
|n| {
|
||||
let node = nodes.get(n.0).ok_or_else(|| {
|
||||
SolidError::invalid_input("difftest::patch_lookup", "node index")
|
||||
})?;
|
||||
obj(node.patch)
|
||||
.map(|i| {
|
||||
surfaces.get(i as usize).map(|s| (i, s)).ok_or_else(|| {
|
||||
SolidError::invalid_input("difftest::patch_lookup", "patch index")
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
},
|
||||
|&i| {
|
||||
surfaces
|
||||
.get(i as usize)
|
||||
.map(|s| Some((i, s)))
|
||||
.ok_or_else(|| {
|
||||
SolidError::invalid_input("difftest::patch_lookup", "surface index")
|
||||
})
|
||||
},
|
||||
)?;
|
||||
let Some((id, surface)) = selected else {
|
||||
out.push(-1.0);
|
||||
continue;
|
||||
};
|
||||
out.push(id as f64);
|
||||
let (n, m, r, s) = surface.array_sizes();
|
||||
let (p, q) = surface.degrees();
|
||||
out.extend([n as f64, m as f64, p as f64, q as f64, r as f64, s as f64]);
|
||||
for row in surface.rows() {
|
||||
for p in row {
|
||||
out.extend([p.x, p.y, p.z, p.w]);
|
||||
}
|
||||
}
|
||||
out.extend(surface.u_knots());
|
||||
out.extend(surface.v_knots());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn default_patch_scale() -> f64 {
|
||||
@@ -730,6 +863,16 @@ impl UnitDiff for Runner {
|
||||
|
||||
fn run_cpp(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
match case.parse::<Inputs>()? {
|
||||
Inputs::PatchLookup {
|
||||
surfaces,
|
||||
owner,
|
||||
nodes,
|
||||
queries,
|
||||
} => {
|
||||
let out = ffi::patch_lookup(&patch_program(&surfaces, owner, &nodes, &queries))
|
||||
.map_err(|e| ffi_err(&e))?;
|
||||
Ok(Output::new().values("patches", out))
|
||||
}
|
||||
Inputs::Spatial { ctor, ops } => {
|
||||
let (oi, of) = ffi::spatial(&spatial_prog(&ctor, &ops)).map_err(|e| ffi_err(&e))?;
|
||||
Ok(Output::new().ints("ints", oi).values("floats", of))
|
||||
@@ -751,6 +894,19 @@ impl UnitDiff for Runner {
|
||||
|
||||
fn run_rust(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
match case.parse::<Inputs>()? {
|
||||
Inputs::PatchLookup {
|
||||
surfaces,
|
||||
owner,
|
||||
nodes,
|
||||
queries,
|
||||
} => {
|
||||
ffi::validate_patch_lookup(&patch_program(&surfaces, owner, &nodes, &queries))
|
||||
.map_err(|e| ffi_err(&e))?;
|
||||
match rust_patch_lookup(&surfaces, owner, &nodes, &queries) {
|
||||
Ok(out) => Ok(Output::new().values("patches", out)),
|
||||
Err(e) => Ok(rust_err(&e)),
|
||||
}
|
||||
}
|
||||
Inputs::Spatial { ctor, ops } => match run_spatial_rust(&ctor, &ops) {
|
||||
Ok((oi, of)) => Ok(Output::new().ints("ints", oi).values("floats", of)),
|
||||
Err(e) => Ok(rust_err(&e)),
|
||||
@@ -881,3 +1037,76 @@ impl UnitDiff for Runner {
|
||||
Ok(cases)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod patch_lookup_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn real_patch_identity_geometry_and_owner_boundary() {
|
||||
let net = [
|
||||
[0., 0., 0., 1.],
|
||||
[0., 2., 0., 1.],
|
||||
[3., 0., 1., 1.],
|
||||
[3., 2., 1., 1.],
|
||||
];
|
||||
let surfaces = vec![net, net, net];
|
||||
let nodes = vec![
|
||||
PatchNode {
|
||||
parent: -1,
|
||||
aux: 3,
|
||||
patch: 1,
|
||||
surface: -1,
|
||||
},
|
||||
PatchNode {
|
||||
parent: 0,
|
||||
aux: 3,
|
||||
patch: 2,
|
||||
surface: -1,
|
||||
},
|
||||
PatchNode {
|
||||
parent: 0,
|
||||
aux: 6,
|
||||
patch: -1,
|
||||
surface: 0,
|
||||
},
|
||||
];
|
||||
let queries = [0, 1, 2];
|
||||
let native = ffi::patch_lookup(&patch_program(&surfaces, 0, &nodes, &queries)).unwrap();
|
||||
let mut actual = rust_patch_lookup(&surfaces, 0, &nodes, &queries).unwrap();
|
||||
assert_eq!(native, actual);
|
||||
assert_eq!(actual.len(), 63);
|
||||
assert_eq!([actual[0], actual[31], actual[62]], [1., 2., -1.]);
|
||||
assert_eq!(&actual[1..31], &actual[32..62]);
|
||||
actual[0] = 2.;
|
||||
assert!(!crate::compare::compare(
|
||||
&Output::new().values("patches", native),
|
||||
&Output::new().values("patches", actual),
|
||||
&[]
|
||||
)
|
||||
.pass());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_patch_programs_are_refused_before_native_access() {
|
||||
let net = [[0., 0., 0., 1.]; 4];
|
||||
let mut nodes = vec![PatchNode {
|
||||
parent: -1,
|
||||
aux: 3,
|
||||
patch: 0,
|
||||
surface: -1,
|
||||
}];
|
||||
let good = patch_program(&[net], 0, &nodes, &[0]);
|
||||
assert!(ffi::validate_patch_lookup(&good).is_ok());
|
||||
for len in 0..good.len() {
|
||||
assert!(ffi::validate_patch_lookup(&good[..len]).is_err());
|
||||
}
|
||||
nodes[0].parent = 0;
|
||||
assert!(ffi::patch_lookup(&patch_program(&[net], 0, &nodes, &[0])).is_err());
|
||||
nodes[0].parent = -1;
|
||||
assert!(ffi::patch_lookup(&patch_program(&[net], 0, &nodes, &[1])).is_err());
|
||||
let mut bad_weight = net;
|
||||
bad_weight[0][3] = 0.;
|
||||
assert!(ffi::patch_lookup(&patch_program(&[bad_weight], 0, &nodes, &[0])).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1641,6 +1641,25 @@ impl UnitDiff for Runner {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// HANG-29: fuzz14876, a topology op on a face whose B-spline surface has a control
|
||||
/// point of `-1e300` (no native result in 60 s). Both sides refuse its block through
|
||||
/// the `topology.vertexuse` export's HANG-29 bound (`1e150`), which both run. It
|
||||
/// cannot be a fixture (a harness error always fails a case).
|
||||
#[test]
|
||||
fn both_sides_refuse_a_topo_block_in_the_overflow_range() {
|
||||
let recorded: Value = serde_json::from_str(include_str!(
|
||||
"../../tests/fuzz_regressions/heal.gap.json"
|
||||
))
|
||||
.unwrap();
|
||||
for recorded in recorded["cases"].as_array().unwrap() {
|
||||
let i: Inputs = serde_json::from_value(recorded["inputs"].clone()).unwrap();
|
||||
for error in [run_rust(&i), run_cpp(&i)] {
|
||||
let error = error.unwrap_err().to_string();
|
||||
assert!(error.contains("HANG-29"), "{}: {error}", recorded["name"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
use solidcore::curve::curve::p2::{CIRCLE_TYPE, ELLIPSE_TYPE};
|
||||
use solidcore::curve::curve::CurveKind;
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ use serde_json::Value;
|
||||
|
||||
use solidcore::heal::heal_data::p1::api_heal_brep;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::math::Point3;
|
||||
use solidcore::primitive::api_primitives::create_box;
|
||||
use solidcore::SolidError;
|
||||
use solidcore_ffi::units::heal_heal_data_p1 as ffi;
|
||||
|
||||
@@ -45,6 +47,8 @@ pub const MIN_GENERATED: usize = 320;
|
||||
|
||||
/// `SM_ERR_INVALID_INPUT`.
|
||||
const SM_ERR_INVALID_INPUT: f64 = 1007.0;
|
||||
/// `SM_SUCCESS`.
|
||||
const SM_SUCCESS: f64 = 1000.0;
|
||||
|
||||
/// The difftest runner.
|
||||
pub struct Runner;
|
||||
@@ -123,23 +127,31 @@ fn run_rust(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
}
|
||||
Inputs::Api { api } => {
|
||||
let mut k = Kernel::default();
|
||||
// The C++ side heals a box made by `SmApiCreateBox`, which the Rust side
|
||||
// cannot build yet (unit primitive.api_primitives): healing any stand-in
|
||||
// B-rep would compare a different input. Build the box with the ported
|
||||
// `SmApiCreateBox` when that lands and compare its status here.
|
||||
// The C++ side heals a box made by `SmApiCreateBox(origin, dx, dy, dz)`;
|
||||
// the output is `create_status heal_status`.
|
||||
let mut create_status = 0.0;
|
||||
let r = match api.box_size {
|
||||
None => api_heal_brep(&mut k, None),
|
||||
Some(_) => {
|
||||
return Ok(Output::deferred(
|
||||
"primitive.api_primitives",
|
||||
"SmApiCreateBox",
|
||||
));
|
||||
Some([dx, dy, dz]) => {
|
||||
match create_box(&mut k, Point3::new(0.0, 0.0, 0.0), dx, dy, dz) {
|
||||
Ok(mut b) => {
|
||||
create_status = SM_SUCCESS;
|
||||
api_heal_brep(&mut k, Some(&mut b))
|
||||
}
|
||||
Err(SolidError::InvalidInput { .. }) => {
|
||||
return Ok(Output::new().values("out", vec![SM_ERR_INVALID_INPUT, 0.0]));
|
||||
}
|
||||
Err(e) => {
|
||||
return Ok(Output::unported(&e)
|
||||
.unwrap_or_else(|| Output::err(e.to_string())));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(match r {
|
||||
Ok(()) => Output::new().values("out", vec![0.0, 0.0]),
|
||||
Ok(()) => Output::new().values("out", vec![create_status, SM_SUCCESS]),
|
||||
Err(SolidError::InvalidInput { .. }) => {
|
||||
Output::new().values("out", vec![0.0, SM_ERR_INVALID_INPUT])
|
||||
Output::new().values("out", vec![create_status, SM_ERR_INVALID_INPUT])
|
||||
}
|
||||
Err(e) => Output::unported(&e).unwrap_or_else(|| Output::err(e.to_string())),
|
||||
})
|
||||
@@ -336,6 +348,10 @@ fn gen_api(rng: &mut Rng) -> Inputs {
|
||||
}
|
||||
|
||||
impl UnitDiff for Runner {
|
||||
fn reference_platform(&self) -> Option<crate::platform::ReferencePlatform> {
|
||||
Some(crate::platform::X86_64_HASH_CASTS)
|
||||
}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"heal.heal_data_p1"
|
||||
}
|
||||
|
||||
@@ -2054,6 +2054,10 @@ fn gen_prob(rng: &mut Rng) -> Inputs {
|
||||
}
|
||||
|
||||
impl UnitDiff for Runner {
|
||||
fn reference_platform(&self) -> Option<crate::platform::ReferencePlatform> {
|
||||
Some(crate::platform::X86_64_HASH_CASTS)
|
||||
}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"heal.heal_data_p2"
|
||||
}
|
||||
@@ -2088,6 +2092,25 @@ impl UnitDiff for Runner {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// HANG-29: `--fuzz 30000` fuzz22334, a surface control point of `-1e300` in the
|
||||
/// B-rep block; the healer's caching did not return. Both sides refuse the block
|
||||
/// through the `topology.vertexuse` export's HANG-29 bound (`1e150`; the Rust side
|
||||
/// imports through the same validated export).
|
||||
#[test]
|
||||
fn both_sides_refuse_a_block_in_the_overflow_range() {
|
||||
let recorded: Value = serde_json::from_str(include_str!(
|
||||
"../../tests/fuzz_regressions/heal.heal_data_p2.json"
|
||||
))
|
||||
.unwrap();
|
||||
for recorded in recorded["cases"].as_array().unwrap() {
|
||||
let i: Inputs = serde_json::from_value(recorded["inputs"].clone()).unwrap();
|
||||
for error in [run_cpp(&i), run_rust(&i)] {
|
||||
let error = error.unwrap_err().to_string();
|
||||
assert!(error.contains("HANG-29"), "{}: {error}", recorded["name"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A native analytic face must reach healing as the same surface kind in Rust.
|
||||
/// A plain B-spline copy has the same control net but different UV evaluation.
|
||||
#[test]
|
||||
|
||||
@@ -104,6 +104,10 @@ fn script(rng: &mut Rng) -> Inputs {
|
||||
}
|
||||
|
||||
impl UnitDiff for Runner {
|
||||
fn reference_platform(&self) -> Option<crate::platform::ReferencePlatform> {
|
||||
Some(crate::platform::X86_64_HASH_CASTS)
|
||||
}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
"heal.heal_data_p3"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,16 @@
|
||||
//! `sm_SetXSectEdgeThroughPoint` and `sm_GetCCIGeometry` run on the edge's B-rep record
|
||||
//! (an edited edge curve is written back to the pool curve the shim's edge shares).
|
||||
//!
|
||||
//! Deferred cases: ops that reach other topology members (`sm_ComputeFacePointClass`),
|
||||
//! `sm_ComputeFacePointClass` runs on the face's B-rep record; both sides emit the status and the whole
|
||||
//! point classification (object, face, deviation, parameters). On the one-face B-rep the points sit
|
||||
//! on the corners, edges and interior, lifted off the surface, and strays, so the classes are
|
||||
//! vertex, edge, face and outside. On a bare face (a surface, no loops, an empty UV domain) the
|
||||
//! point solve finds nothing and both sides end in `SM_ERR` with the default record, so those
|
||||
//! cases check the status, not a classification; so do points too far from the one-face
|
||||
//! B-rep's surface. The shim rejects a face whose UV domain has zero size on either axis (the
|
||||
//! C++ crashes on the single-point domain; cause not located).
|
||||
//!
|
||||
//! Inputs the shim rejects, for which the Rust side would defer or that are undefined upstream:
|
||||
//! surface point solves (`ComputePointParameters` on surfaces, `sm_GetCCISurfaceGaps` with
|
||||
//! surfaces), `SmTol::MapTo2d` for a curve of dimension other than 2 or 3 on a surface
|
||||
//! (`SmSurface::DropPoint`) and an `SmCrvOnSurf` over a plane. Generated scripts are filtered by the shim's
|
||||
@@ -1765,9 +1774,35 @@ fn rust_run(s: &Script) -> Result<Vec<f64>, Fail> {
|
||||
}
|
||||
OpIn::FacePointClass { f, p, tol, .. } => {
|
||||
let h = pool.handle(*f)?.ok_or_else(|| harness("no face"))?;
|
||||
let mut pc = PointClassification::new(zone_tol3d(&k, USE_DEFAULT));
|
||||
must(cc_mod::compute_face_point_class(v3(*p), zone_tol3d(&k, *tol), h, &mut pc))?;
|
||||
return Err(harness("face point class ported").into());
|
||||
let Some((_, EntityRef::Face(fid))) = pool.topology_entity(h) else {
|
||||
return Err(harness("face handle without a face").into());
|
||||
};
|
||||
let mut pc = PointClassification::<EntityRef>::new(zone_tol3d(&k, USE_DEFAULT));
|
||||
let brep = match pool.topo.as_mut() {
|
||||
Some(t) => &mut t.brep,
|
||||
None => &mut pool.bare.brep,
|
||||
};
|
||||
let r = cc_mod::p1::compute_face_point_class_in(
|
||||
&k,
|
||||
brep,
|
||||
fid,
|
||||
v3(*p),
|
||||
zone_tol3d(&k, *tol),
|
||||
&mut pc,
|
||||
);
|
||||
// the status, then the whole classification (also on failure, as the shim)
|
||||
o.push(match st(r)? {
|
||||
Ok(()) => 0.0,
|
||||
Err(s) => s,
|
||||
});
|
||||
let pc = pc
|
||||
.map_objects(&mut |e| {
|
||||
pool.topology_handle(e).ok_or_else(|| {
|
||||
SolidError::invalid_input("difftest", "entity without a pool handle")
|
||||
})
|
||||
})
|
||||
.map_err(|e| harness(e.to_string()))?;
|
||||
put_pc(&mut o, &pc);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -2387,7 +2422,14 @@ fn gen_script(rng: &mut Rng) -> Script {
|
||||
let all: Vec<usize> = (0..objects.len()).collect();
|
||||
for _ in 0..=rng.below(6) {
|
||||
let t = rng.below(2) as usize;
|
||||
let op = match rng.below(5) {
|
||||
let op = match rng.below(6) {
|
||||
// sm_ComputeFacePointClass on a bare face (a surface, no loops)
|
||||
5 => OpIn::FacePointClass {
|
||||
t,
|
||||
f: pick(rng, &of_kind(&objects, kind::FACE)),
|
||||
p: rp(rng, 10.0),
|
||||
tol: *rng.choose(&[1e-5, 1e-4, 1e-3, 1e-6, 0.05]).unwrap_or(&1e-5),
|
||||
},
|
||||
0 | 1 => {
|
||||
let c1 = maybe(rng, &curves, 0.05);
|
||||
let c2 = if rng.chance(0.2) { c1 } else { maybe(rng, &curves, 0.05) };
|
||||
@@ -2590,6 +2632,30 @@ fn gen_face_script(rng: &mut Rng) -> Option<Script> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// sm_ComputeFacePointClass over the face (the first topology object): points on its
|
||||
// corners, edges and interior, lifted off its surface, and strays. Not on the heal.face_props
|
||||
// degenerate domain [0, 0, t, t] with t = 1e-300: the shim rejects the op there (single-point
|
||||
// domain) and would drop the whole script with its other ops.
|
||||
if face.domain[2] - face.domain[0] > 1e-200 && rng.chance(0.6) {
|
||||
let n = corners.len() as f64;
|
||||
let centroid = corners.iter().fold([0.0; 3], |s, c| [s[0] + c[0], s[1] + c[1], s[2] + c[2]]);
|
||||
let centroid = [centroid[0] / n, centroid[1] / n, centroid[2] / n];
|
||||
let mid = [(a[0] + c[0]) / 2.0, (a[1] + c[1]) / 2.0, (a[2] + c[2]) / 2.0];
|
||||
for _ in 0..=rng.below(2) {
|
||||
let mut p = match rng.below(5) {
|
||||
0 => a,
|
||||
1 => mid,
|
||||
2 => centroid,
|
||||
3 => rp(rng, 3.0),
|
||||
_ => c,
|
||||
};
|
||||
if rng.chance(0.3) {
|
||||
p[2] += *rng.choose(&[1e-7, 1e-4, 0.05, -0.05]).unwrap_or(&1e-4);
|
||||
}
|
||||
let tol = *rng.choose(&[1e-5, 1e-4, 1e-3, 1e-6, 0.05]).unwrap_or(&1e-5);
|
||||
ops.push(OpIn::FacePointClass { t: 0, f: Some(n_obj), p, tol });
|
||||
}
|
||||
}
|
||||
for _ in 0..=rng.below(2) {
|
||||
ops.push(OpIn::MakeConsistent { t: 0 });
|
||||
}
|
||||
@@ -2742,7 +2808,7 @@ fn edge_cases() -> Vec<(String, Value)> {
|
||||
OpIn::Gaps { t: 0, cp: [0.0; 3], ep: [3.0, 4.0, 0.0], sol: [0.0, 0.0], pairs: vec![], count: 0 },
|
||||
];
|
||||
out.push(("edge_angles".into(), json!(Script { objects: objs, face: None, ops })));
|
||||
// the topology helpers (the face point classification is deferred in the port)
|
||||
// the topology helpers
|
||||
let objs = vec![
|
||||
line([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0]),
|
||||
ObjIn::Plane { o: [0.0, 0.0, 0.0], n: [0.0, 0.0, 1.0] },
|
||||
|
||||
@@ -20,7 +20,10 @@
|
||||
//! flat record documented in the ffi module (per op: code, status, outputs, state of the
|
||||
//! classification; `GetLoopCrossings` also the face props snapshot when they are passed),
|
||||
//! reported as the `out` values. A Rust error maps to its `SmStatus` (`InvalidInput` 1007, `-1`
|
||||
//! for one upstream cannot return); an `Unported` error anywhere defers the case.
|
||||
//! for one upstream cannot return); an `Unported` error anywhere defers the case. What a
|
||||
//! `GetLoopCrossings` leaves on the face's B-rep (the UV trim curves, tolerance growth and
|
||||
//! gap caches of its `SetProps_Stage2` seam check) is kept on the Rust B-rep for the later
|
||||
//! ops, as the shim's B-rep keeps it.
|
||||
//!
|
||||
//! Compared: `GetLoopCrossings` over the face's real loops up to the touch list (the face
|
||||
//! props stage 2, computed by `SetProps_Stage2` or preset by seeds at stage `2a` or later, the
|
||||
@@ -52,6 +55,7 @@ use solidcore::intersect::curve_class::{
|
||||
self as cc_mod, squeeze_out_smallest_interval, ClassificationInit, CurveClassification,
|
||||
CurveInterval, TouchData,
|
||||
};
|
||||
use solidcore::intersect::curve_class::p5::ClassGeometryMut;
|
||||
use solidcore::intersect::point_class::{
|
||||
ClassGeometry, ClassObject, ClassObjectKind, PointClassType, PointClassification,
|
||||
};
|
||||
@@ -726,6 +730,24 @@ impl ClassGeometry<H> for Pool {
|
||||
}
|
||||
}
|
||||
|
||||
impl ClassGeometryMut<H> for Pool {
|
||||
fn curve_mut(&mut self, o: H) -> Option<&mut Curve> {
|
||||
match o {
|
||||
H::Obj(i, _) => match self.objs.get_mut(i) {
|
||||
Some(PoolObj::Curve(c)) => Some(c),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn topology_entity_mut(&mut self, o: H) -> Option<(&mut Brep, EntityRef)> {
|
||||
match (o, &mut self.brep) {
|
||||
(H::Topo(e), Some((b, _))) => Some((b, e)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
fn handle(&self, i: Option<usize>) -> Result<Option<H>, HarnessError> {
|
||||
i.map(|i| {
|
||||
@@ -967,7 +989,7 @@ fn pc_mut(ivl: &mut CurveInterval<H>, w: usize) -> Result<&mut PointClassificati
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn rust_run(s: &Script) -> Result<Vec<f64>, Fail> {
|
||||
let k = Kernel::default();
|
||||
let pool = build_pool(&k, s)?;
|
||||
let mut pool = build_pool(&k, s)?;
|
||||
let mut ccs: [CurveClassification<H>; 2] =
|
||||
[CurveClassification::new(&k), CurveClassification::new(&k)];
|
||||
let mut o: Vec<f64> = Vec::new();
|
||||
@@ -1148,7 +1170,14 @@ fn rust_run(s: &Script) -> Result<Vec<f64>, Fail> {
|
||||
hfp::seed(&mut props, face, sd[0] as i64, sd[1] as i64, sd[2]);
|
||||
}
|
||||
}
|
||||
let r = cc.get_loop_crossings(&k, &pool, &hs, with_props.then_some(&mut props));
|
||||
// in place: what the call leaves on the face's B-rep stays there
|
||||
// for the later ops, as on the shim's B-rep
|
||||
let r = cc.get_loop_crossings_mut(
|
||||
&k,
|
||||
&mut pool,
|
||||
&hs,
|
||||
with_props.then_some(&mut props),
|
||||
);
|
||||
match st(r)? {
|
||||
Ok(lc) => {
|
||||
o.push(if lc.sm_err { 1.0 } else { 0.0 });
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
use crate::{Case, CaseContext, CaseSource, HarnessError, Output, Rng, UnitDiff};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::cell::OnceCell;
|
||||
use std::rc::Rc;
|
||||
use solidcore::curve::curve::p2::Curve;
|
||||
use solidcore::curve::ellipse::{Ellipse, EllipseOptions};
|
||||
use solidcore::curve::line::{BoundedLineOptions, Line};
|
||||
@@ -46,12 +44,17 @@ use solidcore::topology::face::p2 as topo_face;
|
||||
use solidcore::topology::vertex as topo_vertex;
|
||||
use solidcore::topology::Brep;
|
||||
use solidcore::{EntityRef, SolidError};
|
||||
use solidcore_ffi::units::topology_brep_p1::counts;
|
||||
use solidcore_ffi::units::intersect_curve_class_p6 as ffi;
|
||||
use solidcore_ffi::units::topology_brep_p1::counts;
|
||||
use std::cell::OnceCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Curve-classification part 6 oracle.
|
||||
pub struct Runner;
|
||||
|
||||
#[path = "intersect_curve_class_p6/paired_fillet.rs"]
|
||||
mod paired_fillet;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
struct Fixture {
|
||||
#[serde(default)]
|
||||
@@ -564,10 +567,15 @@ impl UnitDiff for Runner {
|
||||
"intersect.curve_class_p6"
|
||||
}
|
||||
fn run_cpp(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
if case.inputs.get("paired_fillet").and_then(Value::as_bool) == Some(true) {
|
||||
return paired_fillet::cpp(paired_fillet::Mode::Metadata);
|
||||
}
|
||||
if let Some(mode) = paired_fillet::Mode::from_input(&case.inputs) {
|
||||
return paired_fillet::cpp(mode);
|
||||
}
|
||||
let i: Fixture = case.parse()?;
|
||||
let result = ffi::run_picked(
|
||||
i.op, i.kind, i.scale, i.tol, i.flags, i.index, &i.a, &i.b, &i.input, i.pick,
|
||||
i.editing,
|
||||
i.op, i.kind, i.scale, i.tol, i.flags, i.index, &i.a, &i.b, &i.input, i.pick, i.editing,
|
||||
);
|
||||
if i.expected_rejection {
|
||||
return rejected(result);
|
||||
@@ -577,6 +585,12 @@ impl UnitDiff for Runner {
|
||||
.map_err(|e| h(e.to_string()))
|
||||
}
|
||||
fn run_rust(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
if case.inputs.get("paired_fillet").and_then(Value::as_bool) == Some(true) {
|
||||
return paired_fillet::rust(paired_fillet::Mode::Metadata);
|
||||
}
|
||||
if let Some(mode) = paired_fillet::Mode::from_input(&case.inputs) {
|
||||
return paired_fillet::rust(mode);
|
||||
}
|
||||
let i: Fixture = case.parse()?;
|
||||
let validation = ffi::validate_picked(
|
||||
i.op, i.kind, i.scale, i.tol, i.flags, i.index, &i.a, &i.b, &i.input, i.pick,
|
||||
@@ -598,7 +612,10 @@ impl UnitDiff for Runner {
|
||||
let [x, y, z] = *point;
|
||||
let mut a = classification(&k, &i.a, i.tol, &g.first)?;
|
||||
let mut b = classification(&k, &i.b, i.tol, &g.first)?;
|
||||
a.set_fillet_data((i.flags & 2 != 0).then_some(Handle::Other), i.flags & 2 != 0);
|
||||
a.set_fillet_data(
|
||||
(i.flags & 2 != 0).then_some(Handle::Other),
|
||||
i.flags & 2 != 0,
|
||||
);
|
||||
let result = match i.op {
|
||||
// a surf/surf/surf point combined into a vertex boundary moves the box's
|
||||
// vertex (SmVertex::SetPoint), as the shim's InsertCurveInterval does
|
||||
@@ -651,6 +668,24 @@ impl UnitDiff for Runner {
|
||||
}
|
||||
fn cases(&self, ctx: &CaseContext) -> Result<Vec<Case>, HarnessError> {
|
||||
let mut out = crate::runner::default_cases(self, ctx)?;
|
||||
out.push(Case {
|
||||
unit: self.id().into(),
|
||||
name: "paired-fillet-real-boxes".into(),
|
||||
source: CaseSource::Inline,
|
||||
inputs: serde_json::json!({"paired_fillet":true}),
|
||||
});
|
||||
for (name, mode) in [
|
||||
("linked", "linked"),
|
||||
("legacy-no-peer", "legacy-none"),
|
||||
("linked-disabled", "linked-disabled"),
|
||||
] {
|
||||
out.push(Case {
|
||||
unit: self.id().into(),
|
||||
name: format!("paired-fillet-real-boxes-{name}"),
|
||||
source: CaseSource::Inline,
|
||||
inputs: serde_json::json!({"paired_fillet":mode}),
|
||||
});
|
||||
}
|
||||
for kind in 0..5 {
|
||||
// Competing classifications exercise receiver/argument order, gap
|
||||
// precedence and saved-point propagation, rather than only assigning
|
||||
@@ -780,7 +815,14 @@ impl UnitDiff for Runner {
|
||||
// a degenerate chord: one vertex loop
|
||||
("face_point", 7, 1e-12, [0, 0, 0, 0], [2, 2, 2], [0.0, 1.0]),
|
||||
// edge -> edge -> edge along the edge (1,0,1)-(1,1,1): its end vertices
|
||||
("edge_coincident", 6, 1.0, [0, 0, 0, 0], [3, 3, 3], [0.0, 1.0]),
|
||||
(
|
||||
"edge_coincident",
|
||||
6,
|
||||
1.0,
|
||||
[0, 0, 0, 0],
|
||||
[3, 3, 3],
|
||||
[0.0, 1.0],
|
||||
),
|
||||
// part of that edge: splits it at both ends
|
||||
("edge_inner", 6, 0.5, [0, 0, 0, 0], [3, 3, 3], [0.0, 0.5]),
|
||||
// region -> region -> region: a wire edge between two shell vertices
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
//! Actual paired classifications; each role resolves into one retained real box.
|
||||
use super::{
|
||||
brep_p1, counts, ffi, h, status, topo_edge, Brep, ClassGeometry, ClassGeometryMut, ClassObject,
|
||||
ClassObjectKind, Curve, CurveClassification, CurveInterval, EntityRef, HarnessError, Kernel,
|
||||
MergeContext, MergeIntervalBrep, Output, PointClassType, PointClassification, Surface, Vec3,
|
||||
Volume,
|
||||
};
|
||||
use solidcore::topology::{EdgeId, VertexId};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Mode {
|
||||
Metadata,
|
||||
Linked,
|
||||
LegacyNone,
|
||||
LinkedDisabled,
|
||||
}
|
||||
impl Mode {
|
||||
pub(super) fn from_input(input: &serde_json::Value) -> Option<Self> {
|
||||
match input.get("paired_fillet")?.as_str()? {
|
||||
"linked" => Some(Self::Linked),
|
||||
"legacy-none" => Some(Self::LegacyNone),
|
||||
"linked-disabled" => Some(Self::LinkedDisabled),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn code(self) -> i32 {
|
||||
match self {
|
||||
Self::Metadata => 0,
|
||||
Self::Linked => 1,
|
||||
Self::LegacyNone => 2,
|
||||
Self::LinkedDisabled => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Side {
|
||||
Target,
|
||||
Other,
|
||||
}
|
||||
impl Side {
|
||||
fn get<T>(self, pair: &[T; 2]) -> &T {
|
||||
let [target, other] = pair;
|
||||
match self {
|
||||
Self::Target => target,
|
||||
Self::Other => other,
|
||||
}
|
||||
}
|
||||
fn get_mut<T>(self, pair: &mut [T; 2]) -> &mut T {
|
||||
let [target, other] = pair;
|
||||
match self {
|
||||
Self::Target => target,
|
||||
Self::Other => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Object {
|
||||
Curve(Side),
|
||||
Topology(Side, EntityRef),
|
||||
Classification(Side),
|
||||
}
|
||||
impl ClassObject for Object {
|
||||
fn class_kind(&self) -> ClassObjectKind {
|
||||
match self {
|
||||
Self::Curve(_) => ClassObjectKind::Curve,
|
||||
Self::Topology(_, EntityRef::Vertex(_)) => ClassObjectKind::Vertex,
|
||||
Self::Topology(_, EntityRef::Edge(_)) => ClassObjectKind::Edge,
|
||||
_ => ClassObjectKind::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
struct Pair {
|
||||
bodies: [Brep; 2],
|
||||
curves: [Curve; 2],
|
||||
}
|
||||
impl ClassGeometry<Object> for Pair {
|
||||
fn curve(&self, o: Object) -> Option<&Curve> {
|
||||
if let Object::Curve(s) = o {
|
||||
Some(s.get(&self.curves))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn surface(&self, _: Object) -> Option<&Surface> {
|
||||
None
|
||||
}
|
||||
fn volume(&self, _: Object) -> Option<&Volume> {
|
||||
None
|
||||
}
|
||||
fn point(&self, o: Object) -> Option<Vec3> {
|
||||
let Object::Topology(s, EntityRef::Vertex(v)) = o else {
|
||||
return None;
|
||||
};
|
||||
Some(s.get(&self.bodies).vertex(v).ok()?.point)
|
||||
}
|
||||
fn classification_vertex_point(&self, o: Object) -> Option<Vec3> {
|
||||
self.point(o)
|
||||
}
|
||||
fn topology_entity(&self, o: Object) -> Option<(&Brep, EntityRef)> {
|
||||
let Object::Topology(s, e) = o else {
|
||||
return None;
|
||||
};
|
||||
Some((s.get(&self.bodies), e))
|
||||
}
|
||||
fn topology_zone_tol_3d(&self, o: Object) -> Option<f64> {
|
||||
let (b, e) = self.topology_entity(o)?;
|
||||
match e {
|
||||
EntityRef::Vertex(v) => Some(b.vertex(v).ok()?.tolerance),
|
||||
EntityRef::Edge(e) => Some(b.edge(e).ok()?.tolerance),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ClassGeometryMut<Object> for Pair {
|
||||
fn curve_mut(&mut self, o: Object) -> Option<&mut Curve> {
|
||||
if let Object::Curve(s) = o {
|
||||
Some(s.get_mut(&mut self.curves))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn topology_entity_mut(&mut self, o: Object) -> Option<(&mut Brep, EntityRef)> {
|
||||
let Object::Topology(s, e) = o else {
|
||||
return None;
|
||||
};
|
||||
Some((s.get_mut(&mut self.bodies), e))
|
||||
}
|
||||
}
|
||||
impl MergeIntervalBrep<Object> for Pair {
|
||||
fn merge_context(&mut self, _: Object) -> Option<MergeContext<'_, Object>> {
|
||||
// This fixed, already-merged interval edits existing entities through p7.
|
||||
// Any unexpected topology-creation route remains unsupported.
|
||||
None
|
||||
}
|
||||
}
|
||||
fn box_at(origin: [f64; 3]) -> Result<Brep, HarnessError> {
|
||||
let ex =
|
||||
super::super::heal_stitch_p1::export(&super::super::heal_stitch_p1::StitchBrep::Box {
|
||||
p: origin,
|
||||
size: [1.; 3],
|
||||
})?;
|
||||
let (_, at) = counts(&ex).ok_or_else(|| h("paired box export failed"))?;
|
||||
Ok(super::super::topology_brep_p1::import(&ex, at)?.0)
|
||||
}
|
||||
fn selected(b: &Brep) -> Result<(EdgeId, [VertexId; 2]), HarnessError> {
|
||||
let e = *brep_p1::edges(b, None)
|
||||
.map_err(|e| h(e.to_string()))?
|
||||
.first()
|
||||
.ok_or_else(|| h("no box edge"))?;
|
||||
let (a, b) = topo_edge::start_end_vertices(b, e).map_err(|e| h(e.to_string()))?;
|
||||
Ok((
|
||||
e,
|
||||
[
|
||||
a.ok_or_else(|| h("missing start vertex"))?,
|
||||
b.ok_or_else(|| h("missing end vertex"))?,
|
||||
],
|
||||
))
|
||||
}
|
||||
fn edge_curve(b: &Brep, e: EdgeId) -> Result<Curve, HarnessError> {
|
||||
b.edge(e)
|
||||
.map_err(|e| h(e.to_string()))?
|
||||
.curve
|
||||
.clone()
|
||||
.ok_or_else(|| h("missing box curve"))
|
||||
}
|
||||
fn snapshot(
|
||||
k: &Kernel,
|
||||
p: &Pair,
|
||||
edges: [EdgeId; 2],
|
||||
vertices: [[VertexId; 2]; 2],
|
||||
) -> Result<Vec<f64>, HarnessError> {
|
||||
let mut out = Vec::new();
|
||||
for ((b, edge), vertices) in p.bodies.iter().zip(edges).zip(vertices) {
|
||||
for v in vertices {
|
||||
let pt = b.vertex(v).map_err(|e| h(e.to_string()))?.point;
|
||||
out.extend([pt.x, pt.y, pt.z]);
|
||||
}
|
||||
let e = b.edge(edge).map_err(|e| h(e.to_string()))?;
|
||||
let curve = e.curve.as_ref().ok_or_else(|| h("missing edge curve"))?;
|
||||
let d = curve.natural_interval();
|
||||
out.extend([
|
||||
curve.type_code() as f64,
|
||||
e.interval.min(),
|
||||
e.interval.max(),
|
||||
d.min(),
|
||||
d.max(),
|
||||
]);
|
||||
for t in [0., 0.5, 1.] {
|
||||
let pt = curve
|
||||
.evaluate_point(k, e.interval.evaluate(t))
|
||||
.map_err(|e| h(e.to_string()))?;
|
||||
out.extend([pt.x, pt.y, pt.z]);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
fn classification(
|
||||
k: &Kernel,
|
||||
pair: &Pair,
|
||||
side: Side,
|
||||
edge: EdgeId,
|
||||
vertices: [VertexId; 2],
|
||||
) -> Result<CurveClassification<Object>, HarnessError> {
|
||||
let mut cc = CurveClassification::new(k);
|
||||
cc.set_curve(Some(Object::Curve(side)), false);
|
||||
cc.set_src_zone_tol_3d(1e-5);
|
||||
cc.set_size(k, 1).map_err(|e| h(e.to_string()))?;
|
||||
let d = side
|
||||
.get(&pair.bodies)
|
||||
.edge(edge)
|
||||
.map_err(|e| h(e.to_string()))?
|
||||
.interval;
|
||||
let pc = |kind, entity, t| {
|
||||
let mut p = PointClassification::new(1e-5);
|
||||
p.set_class_object(kind, Some(Object::Topology(side, entity)));
|
||||
p.set_t_param(t, false);
|
||||
p
|
||||
};
|
||||
let [start, end] = vertices;
|
||||
*cc.at_mut(0).map_err(|e| h(e.to_string()))? = CurveInterval {
|
||||
interval: d,
|
||||
start: pc(PointClassType::Vertex, EntityRef::Vertex(start), d.min()),
|
||||
mid: pc(PointClassType::Edge, EntityRef::Edge(edge), d.evaluate(0.5)),
|
||||
end: pc(PointClassType::Vertex, EntityRef::Vertex(end), d.max()),
|
||||
};
|
||||
Ok(cc)
|
||||
}
|
||||
pub(super) fn cpp(mode: Mode) -> Result<Output, HarnessError> {
|
||||
let result = if mode == Mode::Metadata {
|
||||
ffi::paired_fillet()
|
||||
} else {
|
||||
ffi::paired_fillet_scoped(mode.code())
|
||||
};
|
||||
result
|
||||
.map(|v| Output::new().values("out", v))
|
||||
.map_err(|e| h(e.to_string()))
|
||||
}
|
||||
pub(super) fn rust(mode: Mode) -> Result<Output, HarnessError> {
|
||||
let mut k = Kernel::default();
|
||||
let mut bodies = [box_at([0.; 3])?, box_at([1e-7, 0., 0.])?];
|
||||
let [target, other] = &bodies;
|
||||
let (a, av) = selected(target)?;
|
||||
let (b, bv) = selected(other)?;
|
||||
let curves = [edge_curve(target, a)?, edge_curve(other, b)?];
|
||||
for b in &mut bodies {
|
||||
b.set_editing_enabled(true);
|
||||
}
|
||||
let mut pair = Pair { bodies, curves };
|
||||
let mut left = classification(&k, &pair, Side::Target, a, av)?;
|
||||
let mut right = classification(&k, &pair, Side::Other, b, bv)?;
|
||||
let doing = mode != Mode::LinkedDisabled;
|
||||
left.set_fillet_data(
|
||||
(mode == Mode::Metadata).then_some(Object::Classification(Side::Other)),
|
||||
doing,
|
||||
);
|
||||
right.set_fillet_data(
|
||||
(mode == Mode::Metadata).then_some(Object::Classification(Side::Target)),
|
||||
doing,
|
||||
);
|
||||
let before = snapshot(&k, &pair, [a, b], [av, bv])?;
|
||||
let result = if matches!(mode, Mode::Linked | Mode::LinkedDisabled) {
|
||||
left.merge_classifications_linked_mut(&mut k, &mut pair, &mut right, false)
|
||||
} else {
|
||||
left.merge_classifications_mut(&mut k, &mut pair, &mut right, false)
|
||||
};
|
||||
let mut out = vec![];
|
||||
match result {
|
||||
Ok((a, b)) => out.extend([
|
||||
0.,
|
||||
a.faces.len() as f64,
|
||||
b.faces.len() as f64,
|
||||
a.edges.len() as f64,
|
||||
b.edges.len() as f64,
|
||||
a.vertices.len() as f64,
|
||||
b.vertices.len() as f64,
|
||||
]),
|
||||
Err(e) => {
|
||||
return Ok(Output::unported(&e)
|
||||
.unwrap_or_else(|| Output::new().values("out", vec![status(&e)])))
|
||||
}
|
||||
}
|
||||
out.extend(before);
|
||||
out.extend(snapshot(&k, &pair, [a, b], [av, bv])?);
|
||||
if mode != Mode::Metadata {
|
||||
out.extend([
|
||||
f64::from(left.other_classification().is_some()),
|
||||
f64::from(left.doing_fillets()),
|
||||
f64::from(right.other_classification().is_some()),
|
||||
f64::from(right.doing_fillets()),
|
||||
]);
|
||||
}
|
||||
Ok(Output::new().values("out", out))
|
||||
}
|
||||
@@ -581,7 +581,7 @@ fn run_rust(k: &Kernel, i: &Inputs, st: &mut Stats) -> solidcore::Result<Vec<f64
|
||||
solver.have_t_min_t_max = *have_t;
|
||||
solver.t_min_of_bbox = *t_min;
|
||||
solver.t_max_of_bbox = *t_max;
|
||||
let r = solver.local_solve(k, surf, leaf);
|
||||
let r = solver.local_solve(k, surf, leaf, None);
|
||||
match &r.status {
|
||||
Ok(()) => rec.push(0.0),
|
||||
Err(e @ SolidError::Unported { .. }) => return Err(e.clone()),
|
||||
|
||||
@@ -693,7 +693,7 @@ fn run_op(
|
||||
return Err(Fail::Harness(harness("surface cache build failed")));
|
||||
};
|
||||
let (tol, ang) = (t.base.approx_tol_3d, t.base.ang_tol_rad);
|
||||
match st(t.drop_trace(k, &mut cache, Some(tol), Some(ang)))? {
|
||||
match st(t.drop_trace(k, &mut cache, Some(tol), Some(ang), None))? {
|
||||
Ok(out) => {
|
||||
o.push(0.0);
|
||||
put_acc(o, &out);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user