Compare commits

...

16 Commits

Author SHA1 Message Date
Alex Lytvynenko a529c20a90 offset: preserve typed shell refusals after retiring split flags
Use the shared lost_manifold reason at the existing early shell refusal,
before the difference can consume the caller's input. Keep the manifold
predicate and comparison status 1001, and keep the empty-cavity opt-in's
1007 refusal first. Remove its now-redundant generic manifold check.

The cavity and sliver retention regressions fail on the rebased parent
because its early guard returns a generic error instead of main's typed
Unsound(NotManifoldSolid). Align the vase and drill-tip matrix assertions
with that typed contract without weakening their retained-input, validity,
or independent-volume checks.

Migrate the newly merged curved-seam tests off the retired loop-gap field;
their geometry, checks and unrelated analytic options stay unchanged.

No input, tolerance, golden, known-failure disposition, final manifold
gate, or excluded construction changes. Exact-head verification follows
this commit; this message does not claim a passing full gate.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <noreply@opencode.ai>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko 713fd2f822 docs: retain the disk check-order and boundary-preservation hold
Continue #3056 without blessing any disk or modifying a gate. At ec0f65581,
52 fresh existing results / 156 checks show nine disks pass once then fail
Loop Orientation on the second/third call. soundness, direct check, standing
and an arena-rebound control all agree. The first call makes four UV trims;
counts and tolerances stay unchanged. Both pyramids and two tilted disks
stay clean. This refutes a naming-only explanation, not a geometry hold.

Separately, gen0001's parallel-plane move changes the tangential boundary:
the original positive-weight curve control hull has y in [0,5], while a fresh
unchecked result has a vertex at y=7.5, exactly 2.5 outside. Exact input/raw
bits, four hull controls, native input/result views and the verified 27-member
archive are retained in the finding. No invented before-result or schematic.

Production, all cases, goldens, all failure entries, tolerances and assertions
remain untouched. The 54 ordinary differences and six local-operation test
failures remain open. No #3084 matching/gate or excluded pole construction.

Co-Authored-By: OpenAI GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko 52f92da4ff goldens: grade the two generated pyramid splits against their five planes
Continue #3056 / #2981 without production, case-input or tolerance changes.
gen0040 and gen0068 have the independently enumerated six plane-intersection
vertices and manifold 5/9/6 topology, with clean Level-0/1/2 checks.
Kernel volumes 992044.1995192543 / 1093044.507611554 versus independent
992044.19951951645359 / 1093044.50761159413764; relative errors 2.643e-13 /
3.662e-14 at the unchanged 1e-9 rule. All six oracle controls pass.

Only these two complete runner encodings change; full old/new encodings,
mathematical construction, kept-input plane residuals, controls and native
before/after wireframes are in the finding. The rounded nonplanar sides in
these cases are replaced by caller planes, not continued through their poles.
No excluded pole construction or #3073/#3084/#3124 scope is implemented.

The other eleven local-operation differences, six local-operation test
failures and all other units' unapproved expectations remain visible. No
failure registry, gate, floor or assertion changes. No canonical pass or
readiness is claimed. Actual new gate counts follow verification.

Co-Authored-By: OpenAI GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko 4582b413f9 test: grade the default pyramid split in both selection orders
Continue #3056: replace the obsolete flag-off refusal with the unconditional four-face split contract. Both replacement selection orders must give the independent five-plane solid: all six analytic plane-intersection vertices, unchanged 1e-9 volume comparison, manifoldness, topology 5/9/6 and clean Level-0/1/2 validity.

Only test code changes. Production local-operation prefix, failure matching, soundness gate, refusal helper and rollback assertions stay byte-unchanged; #3073/#3084/#3124 scopes are untouched.

Measured at parent 0dcc5df1a00ec86e0ffe54ec98e8ba99e00ec692 plus test SHA-256 305d5a9cd6c3e2214abad1b63702a0c9941e56336638ad7aadcb047e74155c50: all three pyramid controls and both crossing controls pass; local operations 29 pass / 6 fail / 2 ignore, the 17 integration binaries 56 pass / 5 fail / 9 ignore. Fmt, workspace/all-target clippy, wasm and panic inventory pass. All 2745 captured outputs including 121 known failures are byte-identical to the pre-refresh capture. The actual gate retains 56 ordinary differences; no further expectation or failure entry is changed.

Co-Authored-By: OpenAI GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko c8d2725317 goldens: certify the repaired square-to-circle shell result
Continue #3056: update only issue1818_square_to_circle_loft_api_shell_1_top after the second-order caller correction. Status 1001/no body becomes 1000, ownership transferred, result counts [2,2,11,12,24,96,16] and bbox [-20,-20,0,20,20,60]. Both input-purity flags, selected faces and all source geometry fields are retained; the boolean context is restored.

At e6ddf2c31, the actual fixture-runner result has a clean Level-2 check, manifoldness, 11 faces / 24 edges / 16 vertices and volume 8784.12356008852. All four direct issue regressions pass unchanged Level-0/1/2 validity, outward orientation and independently bounded cavity-slab volume checks. Same-camera native-edge before/after and shaded fixed-shell views are in the preceding finding. Fixture SHA-256: 0e7b3fa3d97e8f856fa6be9a838f496233b98990cce0cd5c8b844079664a1b5f.

The complete replacement row equals the actual profile-matched collector; all other rows are byte-unchanged, no failure entry or tolerance changes, and all 2745 affected cases remain selected. The other 56 unapproved golden differences and all unrelated construction failures remain visible.

Co-Authored-By: OpenAI GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko 1f7f7bdebd offset: request only the second-order jet for crossing classification
Continue #3056. The #1818 rim-mating failures request a full (2,2) rectangle from an implicit offset, exceeding its total-order-three limit. passes_through consumes only second-order partials. Request the triangular jet without changing its formula, derivative limits, pole handling or tolerances.

Tests first: the existing plane control passed and the new implicit-offset regression failed with status 1001. Both pass with the caller correction. The regression preserves the unsupported full-rectangle refusal and independently checks a touch, a lifted quadratic crossing and a shallow straight crossing.

Primary verification: seven creator tests and all four active #1818 tests pass, including strict validity and independent cavity-volume checks. Fmt, workspace/all-target clippy, wasm and panic inventory pass. Integration selection improves 53/8/9 to 56/5/9 (pass/fail/ignore). The seven local-operation failures remain; no assertion or gate is relaxed.

All 2745 affected golden cases remain selected. Exactly one ordinary output changes from the prior head: #1818 now returns the manifold 11-face shell instead of 1001. All 57 existing unapproved differences remain visible; no golden or known failure is blessed. Attach same-camera native-edge before/after views and the successful shaded shell, with provenance and limitations in the finding.

Co-Authored-By: OpenAI GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko fc097c0ae9 test: run the pyramid split regression without retired flag setup
Delete only the three stale assignments to removed split fields and describe the default path. The geometry, assertions and local-operation soundness gate are unchanged.

Co-Authored-By: OpenAI GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko d3280476ca test: verify the drilled-tip refusal preserves the input
Without the apex sphere cap, the cone offset remains open. The default shell guard refuses it with shell_brep status 1001, regardless of the legacy empty-cavity refusal flag, instead of returning a non-manifold body.

Probe all four side/top and refusal-flag combinations first: ownership is retained; the 8/16/11 input keeps its body key and tolerance, topology ids, vertex positions and tolerances; integrity and validity levels 0-2 pass and it remains manifold. Retained volume 31700.26772804645 agrees with the block-minus-drill closed form 31700.2677274178 within 2e-11 relative. Make these assertions permanent at 1e-9 relative, without refreshing retained caches.

Both optimized tests, focused clippy -D warnings, fmt and diff check pass. The sphere-cap success helper and four-way control are byte-identical. No kernel, fixture, golden or failure-registry change. Other PR failures and canonical CI remain unresolved.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko 2030c86a10 test: verify an open vase shell refusal retains its input
With WR-343 enabled and WR-342 disabled, the vase bottom gets no offset. The default shell guard refuses the resulting open offset with shell_brep status 1001 instead of returning it. Replace the obsolete open-body success expectation, not the construction.

Probe both rotational sweep directions first: ownership is retained; the original 3/4/3 vase keeps its key, body tolerance, topology ids, vertex positions and tolerances; integrity and validity levels 0-2 pass and it remains manifold. Retained volumes agree with the original cone/cylinder closed form 18849.55592153876 within 8e-10 relative. Make these assertions permanent without refreshing retained caches, at 1e-9 relative volume tolerance.

Both optimized WR-342 tests, focused clippy -D warnings, fmt and diff check pass. The successful-shell path and oracle are byte-identical, including the 1e-5 relative tolerance. No kernel, golden or failure-registry change; other PR failures and canonical CI remain unresolved.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko c177c5cc29 test: verify the sliver cavity refusal retains its frame
Without cavity-region selection, the crossing floor offsets keep a sliver and leave no manifold cavity. The default shell refuses that result with shell_brep status 1001, so the old successful-result unwrap is obsolete.

Before changing the expectation, probe the actual ShellOutcome: ownership is not transferred; the retained 21/48/32 frame keeps its key, body tolerance, topology ids, vertex positions and tolerances; integrity and validity levels 0-2 pass, it remains manifold, and volume 1249.9999999999995 agrees with the closed form 1250. Make these assertions permanent without refreshing the returned caches.

All four optimized tests, focused clippy -D warnings, fmt and diff check pass. The three successful controls are byte-identical, including the 638.75 volume at 1e-9 relative and the thicker-floor bitwise comparison. No kernel, golden or failure-registry change; other PR failures and canonical CI remain unresolved.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko 33eab5df60 test: verify the default cavity refusal retains its input
The ribbed lid without crossing-loop resolution leaves an open cavity. The default shell now refuses the non-manifold result with shell_brep status 1001 rather than returning it, so the old successful-result unwrap is obsolete.

Probe the actual ShellOutcome before updating the expectation: ownership is not transferred; the retained 11/24/16 input keeps its key, topology ids, vertex positions and tolerances; integrity and validity levels 0-2 are clean, it is manifold, and volume 2143.9999999999995 matches the closed form 2144. Make these checks permanent without refreshing the returned caches.

Share the unchanged shell options with the successful controls, whose 587.5 volume and 1e-9 tolerance remain byte-identical. All three optimized tests, focused clippy -D warnings, fmt and diff check pass. No kernel or golden change; other PR failures and canonical CI remain unresolved.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko b42a1d1774 test: require valid topology for the top-open drilled plate
Update only the top-open #1795 shell regression to the measured 16/36/24 topology and require clean validity levels 0, 1 and 2. The old extra straight-edge split shared a vertex one millimetre off the curve and hid it behind a 4.166 mm tolerance. Keep the existing volume reference and 1e-9 relative tolerance unchanged, and retain the unresolved side-open expectation byte for byte.

Focused optimized test passes: intact manifold solid, volume 9655.150691018822. Workspace fmt, focused clippy -D warnings and diff check pass. No kernel or golden change. Upstream advanced to 7a593124c with substantial overlapping changes; integration and fresh verification remain outstanding before publication/readiness.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko 7757416124 test: migrate remaining offset regressions to the default split path
Remove obsolete flag-off refusal assertions and duplicate constant-boolean helpers. Preserve the plate topology check, closed-form volume tolerances and loop-gap checks. The fully chamfered inset now names the vanishing edge (1043).

The square-to-circle loft on default fixes now runs the existing complete assert_sound oracle: all check levels, outward manifoldness and independent cavity/volume enclosure.

Focused validation: 42 tests pass across nine binaries with four pre-existing ignored. Workspace clippy -D warnings, fmt and wasm pass. The optimized whole-crate no-fail-fast run is pending, as are the moved-case soundness evidence and canonical exact-head CI.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko 0b4c50bebf review: bound the verification claims and remove retired flag scaffolding
Only one goldens unit was compared with a clean base; the 84-unit sweep is not an established platform baseline. Main run 42399108643 skipped goldens. Correct the unmerged finding instead of treating those failures as cleared.

Remove the constant-true four_face variable and its dead conditions without changing the enabled construction. Drop the retired flag documentation accidentally attached to the unrelated cone flag, and name the now-unused kernel argument explicitly.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko 235d35275e findings: goldens checksum blocks move across platforms (aarch64 darwin vs CI's x86_64)
The full goldens sweep on this machine fails 84/283 units whose changed rows
are checksum-block-only movements, with identical value rows and identical
op/crash/state hashes; reproduced on the clean base 7efbd5090 in a scratch
worktree while the same code is green on CI's x86_64 (run 42399108643 at
8d7a0bf07's tip family). Records what a later agent must check before
attributing such red units to their own lane (run the clean base first), and
what holds: value rows agree within the 1e-9 basis; the block tolerances
absorb aarch64-vs-x86_64 value noise.

Evidence thread for #2981's re-verification turns (the PR's own four units
PASS locally with within-gate values; the sweep red is the platform baseline).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-08 16:33:46 -05:00
Alex Lytvynenko f9fb8901dd offset: the four-face vertex split is the only path; the three split flags retire
Addresses solidcore#2981.

The three `UpstreamFixes` fields (`offset_closes_loop_gaps`,
`offset_splits_corner_vertices`, `offset_four_face_vertex_split`) delete with
their off paths: every extended offset closes its loop gaps and splits its
corner vertices (`step_gaps.rs`'s `split_corner_vertices` unconditional), the
geometry creator's four-face split always builds (its own helper is gone; the
tangency tolerance is the fork's half a degree unconditionally,
`default_tangency_tol_radians()`; `use_creator_tangency_tol` applies it), and
an offset or a shell of a manifold solid that is no closed manifold solid is
refused (`SM_ERR`, 1001) rather than returned, as WR-578/WR-657 ask.

Goldens blessed with the change's own justification: `offset.offset_executive_p2`
594 PASS (61 changed-cases: the eight shell variants of a closed box round whose
offset was no closed manifold solid is now refused 1000 -> 1001 — the default
path's own refusal — plus two cylinder offsets whose surface geometry moves by
ulp-scale rounding from the always-half-degree tangency tolerance);
`offset.offset_geometry_creation` 926 PASS (2: the executor's own state caches
shrink with the split's changed solve plan, the results are the same);
`topology.brep_local_operation` 165 PASS (83 changed: the built bodies now carry
the split's extra edge — graph 146 -> 249 nodes, edge owners +2, uv owners +2 —
whereas before the split was off and the merge either refused or left the
four-faces' corner as one grown vertex). `offset.offset_executive_p1` 1060 PASS,
no result change.

Tests move to the default path: `wr_578_offset_closes_loop_gaps.rs` and
`wr_657_offset_splits_corner_vertices.rs` lose their own flag bools;
`fork_2e1bb8c_four_face_vertex_split.rs`'s four off-path expectations delete
(the off state no longer exists) and the two closed-forms tests run on the
default path; `issue_2020`, `wr650`, `issue_1945`, `issue_1818`, `issue_2260`
drop their enable flags (`kernel_cases/fixes.json` 300 -> 297). The whole-crate
cargo test keeps one failure, `fillet_round_failure_notes_status::

the_rollover_failure_notes_its_status`, which reproduces on this base with the
change stashed (pre-existing on main, unrelated to the flags' paths).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-08 16:33:46 -05:00
35 changed files with 995 additions and 449 deletions
@@ -371,7 +371,7 @@
{"case":"issue1795_plate_static_shell_2_top_offset_solid","deferred":null,"error":null,"ints":{"doing_boolean":[0,0],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[0,1],"ownership":[1,2,2],"result.counts":[2,2,16,18,37,148,24],"result.edge_kinds":{"blocks":[[57,1253]],"n":37},"result.face_kinds":[1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2],"result.present":[1],"selected.after":[0,5],"selected.before":[0,5],"source.after.counts":[16,37,24],"source.after.evaluation_status":[],"source.after.kinds":[1,1,1,2,1,1,2,1,1,1,1,2,2,1,1,2],"source.after.present":[1],"source.before.counts":[8,15,10],"source.before.evaluation_status":[],"source.before.kinds":[1,1,1,2,1,1,2,1],"source.before.present":[1],"status":[1000]},"topology":null,"values":{"result.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.500000000000007],"source.after.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.500000000000007],"source.before.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.5]}}
{"case":"issue1795_plate_static_shell_2_y20","deferred":null,"error":null,"ints":{"doing_boolean":[0,1],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,0],"result.present":[0],"selected.after":[0,2],"selected.before":[0,2],"source.after.counts":[8,15,10],"source.after.evaluation_status":[],"source.after.kinds":[1,1,1,2,1,1,2,1],"source.after.present":[1],"source.before.counts":[8,15,10],"source.before.evaluation_status":[],"source.before.kinds":[1,1,1,2,1,1,2,1],"source.before.present":[1],"status":[1001]},"topology":null,"values":{"source.after.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.5],"source.before.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.5]}}
{"case":"issue1795_plate_static_shell_2_y20_offset_solid","deferred":null,"error":null,"ints":{"doing_boolean":[0,1],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,0],"result.present":[0],"selected.after":[0,2],"selected.before":[0,2],"source.after.counts":[8,15,10],"source.after.evaluation_status":[],"source.after.kinds":[1,1,1,2,1,1,2,1],"source.after.present":[1],"source.before.counts":[8,15,10],"source.before.evaluation_status":[],"source.before.kinds":[1,1,1,2,1,1,2,1],"source.before.present":[1],"status":[1001]},"topology":null,"values":{"source.after.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.5],"source.before.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.5]}}
{"case":"issue1818_square_to_circle_loft_api_shell_1_top","deferred":null,"error":null,"ints":{"doing_boolean":[0,1],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,0],"result.present":[0],"selected.after":[0,5],"selected.before":[0,5],"source.after.counts":[6,12,8],"source.after.evaluation_status":[],"source.after.kinds":[2,2,2,2,1,1],"source.after.present":[1],"source.before.counts":[6,12,8],"source.before.evaluation_status":[],"source.before.kinds":[2,2,2,2,1,1],"source.before.present":[1],"status":[1001]},"topology":null,"values":{"source.after.geometry":[0.00001,-20.0,-20.0,0.0,20.0,20.0,60.0],"source.before.geometry":[0.00001,-20.0,-20.0,0.0,20.0,20.0,60.0]}}
{"case":"issue1818_square_to_circle_loft_api_shell_1_top","deferred":null,"error":null,"ints":{"doing_boolean":[0,0],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,1],"result.counts":[2,2,11,12,24,96,16],"result.edge_kinds":{"blocks":[[44,590]],"n":24},"result.face_kinds":[1,1,1,2,2,2,2,2,2,2,2],"result.present":[1],"selected.after":[0,5],"selected.before":[0,5],"source.after.counts":[6,12,8],"source.after.evaluation_status":[],"source.after.kinds":[2,2,2,2,1,1],"source.after.present":[1],"source.before.counts":[6,12,8],"source.before.evaluation_status":[],"source.before.kinds":[2,2,2,2,1,1],"source.before.present":[1],"status":[1000]},"topology":null,"values":{"result.geometry":[1e-05,-20.0,-20.0,0.0,20.0,20.0,60.0],"source.after.geometry":[1e-05,-20.0,-20.0,0.0,20.0,20.0,60.0],"source.before.geometry":[1e-05,-20.0,-20.0,0.0,20.0,20.0,60.0]}}
{"case":"issue2174_cap_two_rim_edges_api_shell_in","deferred":null,"error":null,"ints":{"doing_boolean":[0,0],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,1],"result.counts":[2,2,4,4,8,32,6],"result.edge_kinds":[2,2,2,2,2,2,2,2],"result.face_kinds":[2,2,2,2],"result.present":[1],"selected.after":[],"selected.before":[],"source.after.counts":[1,3,3],"source.after.evaluation_status":[],"source.after.kinds":[2],"source.after.present":[1],"source.before.counts":[1,3,3],"source.before.evaluation_status":[],"source.before.kinds":[2],"source.before.present":[1],"status":[1000]},"topology":null,"values":{"result.geometry":[0.00001,-8.660254037844387,0.0,4.5,8.660254037844387,1.1e-15,10.0],"source.after.geometry":[0.00001,-8.660254037844389,0.0,5.000000000000001,8.660254037844387,5e-16,10.0],"source.before.geometry":[0.00001,-8.660254037844389,0.0,5.000000000000001,8.660254037844387,5e-16,10.0]}}
{"case":"issue2174_cap_two_rim_edges_api_shell_out","deferred":null,"error":null,"ints":{"doing_boolean":[0,0],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,1],"result.counts":[2,2,4,4,8,32,6],"result.edge_kinds":[2,2,2,2,2,2,2,2],"result.face_kinds":[2,2,2,2],"result.present":[1],"selected.after":[],"selected.before":[],"source.after.counts":[1,3,3],"source.after.evaluation_status":[],"source.after.kinds":[2],"source.after.present":[1],"source.before.counts":[1,3,3],"source.before.evaluation_status":[],"source.before.kinds":[2],"source.before.present":[1],"status":[1000]},"topology":null,"values":{"result.geometry":[0.00001,-9.526279441628823,0.0,5.0,9.526279441628823,1.1724617093029907e-15,11.0],"source.after.geometry":[0.00001,-8.660254037844389,0.0,5.000000000000001,8.660254037844387,5e-16,10.0],"source.before.geometry":[0.00001,-8.660254037844389,0.0,5.000000000000001,8.660254037844387,5e-16,10.0]}}
{"case":"issue2611_half_pipe_arc_r12_api_shell_in","deferred":null,"error":null,"ints":{"doing_boolean":[0,0],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,1],"result.counts":[2,2,8,8,18,72,12],"result.edge_kinds":{"blocks":[[34,339]],"n":18},"result.face_kinds":[1,1,2,2,2,2,2,2],"result.present":[1],"selected.after":[],"selected.before":[],"source.after.counts":[1,6,6],"source.after.evaluation_status":[],"source.after.kinds":[2],"source.after.present":[1],"source.before.counts":[1,6,6],"source.before.evaluation_status":[],"source.before.kinds":[2],"source.before.present":[1],"status":[1000]},"topology":null,"values":{"result.geometry":[0.00001,-24.979991993593593,0.9199999999999958,-40.0,24.979991993593593,4.5147186257614305,8.485281374238577],"source.after.geometry":[0.00001,-24.979991993593593,1.0,-40.0,24.979991993593593,4.5147186257614305,8.485281374238575],"source.before.geometry":[0.00001,-24.979991993593593,1.0,-40.0,24.979991993593593,4.5147186257614305,8.485281374238575]}}
@@ -100,7 +100,7 @@
{"case":"gen0037","deferred":null,"error":null,"ints":{"graph":{"blocks":[[50,1663]],"n":71}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":[1e+20,1e+20,1e+20,1e+20,0.0,0.0,-1.3221998894643683,0.00001,-1234567.0,-1234567.0,1234567.0,1234567.0,0.00001,-30.61467458920718,0.0,0.00001],"record":{"blocks":[[28974.333030821406,22289221.39035379,122141779.08161356]],"n":70},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0]}}
{"case":"gen0038","deferred":null,"error":null,"ints":{"graph":{"blocks":[[104,5428]],"n":99}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":{"blocks":[[8e+20,8e+20,3.6e+21]],"n":24},"record":{"blocks":[[58410.52515099852,58471.59621881038,2310587.9923582436]],"n":116},"surface_owners":[1.0],"uv_curve_owners":[0.0,1.0,-1.0,-1.0]}}
{"case":"gen0039","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000017e+21,4.656000000000001e+23]],"n":194},"record":{"blocks":[[141125.56968884318,141205.72410907954,18211054.808841206],[856315.8199191247,28873797.151187282,281138581.99902],[-736999.2588899866,14771293.71724904,24109598.28267207]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
{"case":"gen0040","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1172,159991],[1948,311927],[978,28202]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.4e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[156360.52780532837,158006.30563557148,19068776.25026989],[153903.1388885975,156472.02783429623,20181493.582973957],[104892.00006401539,108480.00015670061,12407745.286694974],[258.6666741371155,509.33334589004517,2804.4445185661316]],"n":784},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[360.0,392.0,9416.0]],"n":32}}}
{"case":"gen0040","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1360,199480],[1748,274861],[1932,107271]],"n":623}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0],"geometry":{"blocks":[[7.2e+21,7.200000000000017e+21,2.628e+23]],"n":148},"record":{"blocks":[[143904.70981994172,148850.83660596592,18749454.884977996],[60967.80078066501,91192355.46067868,416542727.02352035]],"n":460},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-36.0,36.0,-666.0]],"n":36}}}
{"case":"gen0041","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
{"case":"gen0042","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000008e+21,4.656000000000001e+23]],"n":194},"record":{"blocks":[[141028.51753995748,141100.51753995748,18209140.71170544],[104020.45910069923,20901260.41429212,82962163.56631431],[15281.0,15292.597876826972,503374.6497665959]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
{"case":"gen0043","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1881,231784],[3468,553413],[2259,207996],[5091,517645],[9023,1312991],[4471,251788]],"n":1396}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[210.0,210.0,3080.0]],"n":21},"geometry":{"blocks":[[1.6799999999999925e+22,1.6799999999999933e+22,1.4196e+24],[97.07644014672621,273.0764401467262,4532.062022572823]],"n":337},"record":{"blocks":[[140827.5568614641,140999.5568614641,18163612.41307346],[127029.93663265301,127151.93663265301,17644141.082648907],[103806.82188705914,19857052.821887065,77472491.1227448],[75988.12293491783,76084.12293491783,8443981.054912265]],"n":1001},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-84.0,84.0,-3570.0]],"n":84}}}
@@ -128,7 +128,7 @@
{"case":"gen0065","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1881,231784],[3468,553413],[2259,207996],[5091,517645],[9023,1312991],[4471,251788]],"n":1396}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[210.0,210.0,3080.0]],"n":21},"geometry":{"blocks":[[1.6799999999999925e+22,1.6799999999999941e+22,1.4196e+24],[216.74562947760214,251.5357930786577,9919.280402705568]],"n":337},"record":{"blocks":[[140916.00939200036,141010.75141430172,18185606.76314736],[127182.60496144516,127265.78343962408,17663526.26494957],[103781.86835273873,41335399.430042975,165038028.19196254],[75988.12293491783,76084.12293491783,8443981.054912265]],"n":1001},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-84.0,84.0,-3570.0]],"n":84}}}
{"case":"gen0066","deferred":null,"error":null,"ints":{"graph":{"blocks":[[50,1663]],"n":71}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":[1e+20,1e+20,1e+20,1e+20,0.0,0.0,-0.46440414733728574,0.00001,-1234567.0,-1234567.0,1234567.0,1234567.0,0.00001,-30.61467458920718,0.0,0.00001],"record":{"blocks":[[28988.883667349597,19782189.11633265,123948409.36653352]],"n":70},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0]}}
{"case":"gen0067","deferred":null,"error":null,"ints":{"graph":{"blocks":[[692,106331],[25,79]],"n":261}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0],"geometry":{"blocks":[[2.4e+21,2.400000000000004e+21,3.0000000000000004e+22]],"n":56},"record":{"blocks":[[87510.89747446822,19840766.897475913,71347892.72503424]],"n":210},"surface_owners":[1.0,1.0,1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0]}}
{"case":"gen0068","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1172,159991],[1948,311927],[978,28202]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.4e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[156360.52780532837,158006.30563557148,19068776.25026989],[153903.1388885975,156472.02783429623,20181493.582973957],[104892.00006401539,108480.00015670061,12407745.286694974],[258.6666741371155,509.33334589004517,2804.4445185661316]],"n":784},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[360.0,392.0,9416.0]],"n":32}}}
{"case":"gen0068","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1360,199542],[1745,274520],[1935,107845]],"n":623}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0],"geometry":{"blocks":[[7.2e+21,7.200000000000021e+21,2.628e+23]],"n":148},"record":{"blocks":[[142267.17126916934,149545.78124175625,18538871.935180865],[60965.31864733237,112625909.83332224,505787772.57405066]],"n":460},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-36.0,36.0,-666.0]],"n":36}}}
{"case":"gen0069","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000017e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141089.7399922187,141161.7399922187,18205050.581791796],[-2365178.595266464,27264383.404733535,-487506475.8921594],[2484414.4434644342,12360910.443464434,116552946.53889537]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
{"case":"gen0070","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
{"case":"gen0071","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000008e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141015.63183242833,141140.705756406,18197056.686032668],[104017.577173002,19857179.943658657,137194437.6835873],[15280.999999999996,15346.710572543841,503200.5403258097]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
-6
View File
@@ -819,10 +819,6 @@ pub struct UpstreamFixes {
pub local_operation_refuses_loose_tangent_edges: bool,
/// A local operation that moves one of two tangent faces along its normal builds the step: `EdgeuseOffset` finds the two faces parallel but apart across the edge (`IsStepAcrossEdge`) and builds the ruled wall between their edges (`CreateStepWall`, rails of uniform weight made non-rational), kept out of the face extensions; `CloseLoopGaps` closes the gaps the wall leaves at its ends and the edges running past its corners are split; a vertex moved past an edge's other end is `SM_ERR_OFFSET_EDGE_VANISHES` (1043) (WR-530 addendum; tangency-labs/smlib 5d428ae, 90aabff, 34318f4).
pub local_operation_step_walls: bool,
/// Every extended offset closes the loop gaps it leaves where two consecutive edges' offsets run parallel and never meet (`CloseLoopGaps`), before the merge; a vertex moved past an edge's other end is `SM_ERR_OFFSET_EDGE_VANISHES` (1043); a shell or merged offset of a solid that is no closed solid is refused (`SM_ERR`) whatever this flag says (WR-578; tangency-labs/smlib 7ff0a94, 34318f4, rig case shell/1494). Limit: tangency-labs/solidcore#1528's plate shells on a clean operand but is refused after a prior mass-properties query on it, which writes UV trims and gap caches the offset reads (upstream behaviour, WR-583).
pub offset_closes_loop_gaps: bool,
/// With `offset_closes_loop_gaps` (it has no effect without it): a convex corner of four plane faces whose offset planes no longer meet in one point (two chamfers meeting at a box's corner) becomes the edge between the two faces across it that the offset makes meet, where upstream ends the corner's edges at the same pair of the planes' meeting points whichever way the corner splits, and so leaves an inset, or a shell open at one of the four faces, with loop gaps and no volume (or the merge refuses it, 1001). A corner one of whose edges would vanish is left as it is. (An offset (`offset_brep`) or a shell of a manifold solid that is no closed manifold solid is refused (`SM_ERR`) by default; WR-657's refusal no longer needs this flag.)
pub offset_splits_corner_vertices: bool,
/// With `local_operation_step_walls`: an edge whose vertex a local operation moves onto its other end, where its other vertex is, has closed to a point and is squeezed out of its offset face (`CloseLoopGap`); once every gap is closed the offset faces that closed to nothing (`SmFace::IsDegenerate`, a step's wall whose low top moved onto the high top's plane) are deleted and forgotten, and the merge joins the faces either side. A closed edge on no such face or across from one is still `SM_ERR_OFFSET_EDGE_VANISHES` (WR-566; tangency-labs/smlib ccbf77a). In the face move (`offset::offset_faces`), the edges then left between a moved face and a face that stayed, on one surface, which `IsTopological` finds removable, are removed, so the tops either side of the closed wall are one face (WR-566 addendum; tangency-labs/smlib 9504880).
pub local_operation_closes_walls_to_nothing: bool,
/// An inward shell mates the walls across rounds thinner than it (collapsed faces) (#397).
@@ -1256,8 +1252,6 @@ pub struct UpstreamFixes {
pub offset_closed_curve_trim_arc: bool,
/// `TrimExtendedEdge` never takes an open edge's first trim point again as its second, by point as well as by parameter: on the closed curve a rim half of a sphere cap lifts onto (the whole offset circle), the first point's twin across the curve's seam (another parameter, the same point, often the far end of the coincident range the other rim half's offset gives) is refused, so the half no longer gets the whole circle (or nothing) and its offset loop no gap. Thickening the cap above z = 5 of a sphere whose rim is two half circles placed against its seam (the sphere cut by a turned box) failed with `SM_ERR_OFFSET_EDGE_VANISHES` (1043) under `offset_closes_loop_gaps`, and gave a non-manifold body without it (WR-673, #2174; the fork's tangency-labs/smlib@7ee0876, for #1502).
pub offset_open_edge_second_trim_point: bool,
/// The offset builds a vertex where more than three faces meet (a floor of four facets falling 1 degree to a drain, and its corners where two walls meet two facets) as the two vertices and the new edge the four offset planes give, where upstream fails the shell (1001): `SmOffsetGeometryCreation`'s tangency tolerance is half a degree, not two (the facets, 1.2 degrees apart, are intersected, not left to meet by extending their trims); `TrimExtendedEdge` keeps a trim point within 5 degrees of tangent where the curve crosses the surface (only a touch is dropped); and, where two faces' offsets cut the edge at a vertex all of whose edges are convex (or all concave), the edge ends at the cut it reaches first from its other end, not at the one nearest the vertex. It has no effect without `offset_closes_loop_gaps`, which closes the loop gaps the split leaves on the faces round the vertex (alone, the split returns a solid with those loops cut short, of the wrong volume) (WR-666, #2052; the fork's tangency-labs/smlib@2e1bb8c).
pub offset_four_face_vertex_split: bool,
/// `SmCone::CreateCanonical` (`Cone::create_canonical_with_semi_angle` and `Cone::create_canonical`) passes `inside_out` as the bounded cone constructor's `bInsideOut` and `false` as `bMakeNurbGenCurve` when `height` is `Some` (where upstream passes `FALSE` for `bInsideOut` and the caller's `bInsideOut` for `bMakeNurbGenCurve`, dropping an inside-out request), and rejects `height <= 0.0` in the two-radius overload with `SolidError::InvalidInput` (`SM_ERR_INVALID_INPUT`) instead of forwarding `top_radius` as the semi-angle in degrees of an unbounded cone (WR-63, #2207; see `plan/upstream-issues/WR-63--addendum-2026-11-01-cone-create-canonical-args.md`).
pub cone_create_canonical_preserves_inside_out_and_checks_height: bool,
/// `MateEdges` also intersects analytic copies of the two faces' surfaces (plane, cylinder, cone, sphere, torus; extended only along a plane or a cylinder's or cone's rulings, where the tangent (G1R) extensions still are the surface) and replaces each marched piece that lies on an exact line or conic between the same ends, and drops to one UV curve on each extended surface within the body's tolerance, by that exact curve, so a shelled cylinder's floor circle, seam line and rim stay exact (WR-495, #1453/#1130; the fork's tangency-labs/smlib@fddc366 and @98ab1b0).
@@ -328,17 +328,16 @@ impl OffsetExecutive {
self.tangency_tol_radians * 180.0 / PI
}
/// Takes the geometry creator's constructor tolerance under `kernel`'s fixes
/// ([`OffsetGeometryCreation::default_tangency_tol_radians`]): half a degree with
/// `Kernel::fixes.offset_four_face_vertex_split` and `offset_closes_loop_gaps` (WR-666),
/// else the 2 degrees of
/// [`Self::new`] unchanged. Called right after construction by the executives that
/// Takes the geometry creator's constructor tolerance
/// ([`OffsetGeometryCreation::default_tangency_tol_radians`]): half a degree (WR-666,
/// the fork's tangency-labs/smlib@2e1bb8c), not [`Self::new`]'s two degrees. Called
/// right after construction by the executives that
/// `SmApiOffsetBrepFull`, `SmApiShellBrepFull` and `SmBrep::LocalOperation` build.
pub fn use_creator_tangency_tol(&mut self, kernel: &Kernel) {
if crate::offset::offset_geometry_creation::four_face_vertex_split(&kernel.fixes) {
self.tangency_tol_radians =
OffsetGeometryCreation::default_tangency_tol_radians(&kernel.fixes);
}
///
/// On the default path since #2981: WR-666's split is the only path, so the creator
/// tolerance is always the fork's.
pub fn use_creator_tangency_tol(&mut self, _kernel: &Kernel) {
self.tangency_tol_radians = OffsetGeometryCreation::default_tangency_tol_radians();
}
/// Offset distance supplied by the geometry creator.
+36 -35
View File
@@ -403,27 +403,23 @@ impl OffsetExecutive {
let mut shell = run_shell_offset(kernel, original, options, faces, notifications)?;
// Opt-in (WR-454, #380; tangency-labs/smlib@fbe43bb): an inset of a solid whose
// offset leaves nothing has no cavity to take away (the difference would hand
// the input back unhollowed); an offset that is no closed solid bounds none.
// the input back unhollowed). Its non-manifold case is the shared typed guard
// below, which both flag states use.
if kernel.fixes.shell_empty_cavity_fails
&& options.offset.distance < 0.0
&& input_is_manifold
&& bp1::num_faces(&shell) == 0
{
if bp1::num_faces(&shell) == 0 {
return Err(invalid_input(OP_SHELL));
}
if !brep_is_manifold_solid(&shell)? {
return Err(SolidError::sm_err(OP_SHELL));
}
return Err(invalid_input(OP_SHELL));
}
// Opt-in (WR-578): with the loop gaps closed, an offset of a solid that is still no
// closed solid is refused rather than handed back as it is (as WR-454's opt-in
// does for an inset): the gap closing must never turn upstream's refusal into a
// success on a broken body
if kernel.fixes.offset_closes_loop_gaps
&& input_is_manifold
&& !brep_is_manifold_solid(&shell)?
// WR-578 (the loop gaps close on the default path since #2981): a shell of a
// solid that is still no closed manifold solid is refused rather than returned.
// A non-empty offset of a manifold solid that is no closed solid reaches this
// one typed refusal (1001) whichever state WR-454's flag is in.
if let Some(what) =
lost_manifold(brep_is_manifold_solid(&shell)?, || Ok(input_is_manifold))?
{
return Err(SolidError::sm_err(OP_SHELL));
return Err(Failure::new(OP_SHELL, OffsetFailure::Unsound { what }).into_error());
}
if !brep_is_manifold_solid(&shell)? && !options.create_offset_solid {
return Ok(shell);
@@ -560,13 +556,13 @@ impl OffsetExecutive {
notifications,
)?;
// (not upstream) a shell of a manifold solid is a manifold solid or a refusal (the
// ops contract, `finish`): an offset the merge would have refused can reach the
// difference, which then hands back no solid (a ruled square-to-circle loft
// shelled inward: ten faces, not manifold, volume 0; a box's chamfer left open
// by an inset of its two sides' width, which the cavity meets along a line only).
// The returns above are ungated by design: a solid's offset that is no solid with
// `create_offset_solid` false is the sheet the caller asked for, and a sheet
// input's thickening is not a solid's shell.
// ops contract, `finish`): the early check above refuses an intermediate offset of
// a manifold solid that is no manifold solid, and this final gate refuses a result
// the merge or difference leaves that way (a ruled square-to-circle loft shelled
// inward: ten faces, not manifold, volume 0; a box's chamfer left open by an inset
// of its two sides' width, which the cavity meets along a line only). The ungated
// return above is a sheet input's offset: with `create_offset_solid` false it is
// the sheet the caller asked for, and its thickening has its own path.
if let Some(what) =
lost_manifold(brep_is_closed_manifold(&shell)?, || Ok(input_is_manifold))?
{
@@ -995,16 +991,13 @@ impl OffsetExecutive {
}
}
// Not upstream (opt-in, fork tangency-labs/smlib 5d428ae and 7ff0a94): close the
// (not upstream, the executor's own split flags are the only path since
// #2981: fork tangency-labs/smlib 5d428ae, 7ff0a94, 2e1bb8c): close the
// loop gaps the offset leaves (a local operation's step walls' ends, a solid
// offset's vertex where two consecutive edges' offsets run parallel), then split
// the edges that run past a step wall's corner (after WR-531's caps below, in the
// fork's order)
let local = self.operation() == OffsetOperation::LocalOperation;
let step_walls = kernel.fixes.local_operation_step_walls && local;
if kernel.fixes.offset_closes_loop_gaps || step_walls {
self.close_loop_gaps(kernel, original, notifications)?;
}
self.close_loop_gaps(kernel, original, notifications)?;
// Not upstream (WR-531), opt-in `Kernel::fixes.offset_point_vertex_caps` (the
// fork's tangency-labs/smlib@d5dadaa): a vertex where one face closes on itself to
@@ -1041,7 +1034,9 @@ impl OffsetExecutive {
}
}
if step_walls {
if kernel.fixes.local_operation_step_walls
&& self.operation() == OffsetOperation::LocalOperation
{
self.split_edges_at_step_corners(kernel, notifications)?;
}
@@ -5450,7 +5445,11 @@ mod tests {
)?;
Ok(Some(volume))
};
assert_eq!(shell(false, false).unwrap(), None);
// The default path (the splits are the only path since #2981): the shell's
// stitch still welds the rims wrongly, the offset of the plate is no closed
// solid, and the WR-578 guard refuses it (1001)
assert_status(shell(false, false), 1001);
// the case's own opt-in (stitch at the body's tolerance) builds the closed form
// 40 x 40 x 10 less the hole, less the cavity: 8.5 high over the inner box (37 x 37)
// less the hole's offset disk (r 3.5, centre 3.49 from x = 1.5) cut by x = 1.5
let expected = 4564.924657;
@@ -5464,8 +5463,10 @@ mod tests {
}
/// tangency-labs/solidcore#380: a closed inward shell of a box whose wall is as thick
/// as (or thicker than) the inradius leaves no cavity. Upstream returns the empty
/// offset as a success (the fork: the body unhollowed); with the opt-in it fails, and a thinner wall still shells.
/// as (or thicker than) the inradius leaves no cavity. The default path (the split
/// flags are the only path since #2981) refuses it as no closed solid (1001); with
/// WR-454's own opt-in the typed empty-cavity refusal (1007) takes precedence; a
/// thinner wall still shells.
#[test]
fn issue_380_shell_with_no_cavity_fails_with_the_fix() {
let shell = |fix: bool, wall: f64| -> Result<Brep> {
@@ -5494,9 +5495,9 @@ mod tests {
.result
};
for wall in [5.0, 6.0] {
// upstream: an empty offset is "not a manifold solid", returned as a success
let upstream = shell(false, wall).unwrap();
assert_eq!(bp1::num_faces(&upstream), 0);
// the default path: an offset that is no closed solid is refused (1001)
assert_status(shell(false, wall), 1001);
// WR-454's opt-in keeps its own typed refusal (1007)
assert_status(shell(true, wall), 1007);
}
let thin = shell(true, 1.0).unwrap();
@@ -310,10 +310,8 @@ impl OffsetExecutive {
notifications: &mut OffsetNotifications<'_>,
) -> Result<()> {
let approx_tol = self.approximation_tol_3d();
// (WR-657, opt-in) the corners whose offset planes no longer meet in one point
if kernel.fixes.offset_splits_corner_vertices {
self.split_corner_vertices(kernel, original, b, approx_tol, notifications)?;
}
// (WR-657) the corners whose offset planes no longer meet in one point
self.split_corner_vertices(kernel, original, b, approx_tol, notifications)?;
// (fork ccbf77a) each edge that closed to a point: its offset face and the original
// face across it
let mut closed_edges: Vec<(FaceId, Option<FaceId>)> = Vec::new();
@@ -40,8 +40,8 @@
//! | `offset_vertex_image` (private) | not upstream: a vertex's image on its face's offset (WR-736; opt-in) |
//!
//! Deviations:
//! - **Four faces at a vertex (WR-666, opt-in).** With `Kernel::fixes.offset_four_face_vertex_split`
//! and `offset_closes_loop_gaps`, the fork's tangency-labs/smlib@2e1bb8c: the tangency
//! - **Four faces at a vertex (WR-666, the only path).** The fork's
//! tangency-labs/smlib@2e1bb8c: the tangency
//! tolerance is half a degree, `trim_extended_edge` keeps a near-tangent trim point where
//! the curve passes through the surface by more than the tolerance, and at an all-convex or all-concave vertex takes the cut
//! reached first from the edge's other end. That choice also skips, on an open edge's
@@ -1558,14 +1558,6 @@ fn is_arc(k: &Kernel, c: &BSplineCurve, num_samples: u64, tol: f64) -> Result<Op
// ---- the geometry creation --------------------------------------------------------------------
/// Whether WR-666's opt-in is in effect: `Kernel::fixes.offset_four_face_vertex_split`
/// together with `offset_closes_loop_gaps` (the fork's `CloseLoopGaps`, which closes the
/// loop gaps the split vertex leaves on the faces round it; without it the flag has no
/// effect, as the split alone leaves those faces' loops open).
pub(crate) fn four_face_vertex_split(fixes: &crate::kernel::UpstreamFixes) -> bool {
fixes.offset_four_face_vertex_split && fixes.offset_closes_loop_gaps
}
/// Whether `curve` at `t` passes through `surface` at `uv` (where they meet) by more than
/// `tol`. To second order in arc length s the curve's height over the surface is
/// `g1 s + g2 s^2 / 2`, `g1` the sine of the angle between them and `g2` the curve's
@@ -1589,7 +1581,18 @@ fn passes_through(
let err = || sm_err("offset_geometry_creation::passes_through");
let pv = curve.evaluate(k, t, 2, true, true)?;
let (d1, d2) = (*pv.get(1).ok_or_else(err)?, *pv.get(2).ok_or_else(err)?);
let s = surface.evaluate(k, uv, 2, 2, EvaluateFlags::default())?;
// Only the total-order-two jet is used below. The full rectangle would also
// request D(2, 2), beyond an implicit offset's derivative limit.
let s = surface.evaluate(
k,
uv,
2,
2,
EvaluateFlags {
only_upper_half: true,
..EvaluateFlags::default()
},
)?;
let at = |i: usize, j: usize| s.get(i).and_then(|r| r.get(j)).copied().ok_or_else(err);
let (du, dv, duu, duv, dvv) = (at(1, 0)?, at(0, 1)?, at(2, 0)?, at(1, 1)?, at(0, 2)?);
let n = du.cross(dv).unitize().map_err(|_| err())?;
@@ -1635,17 +1638,12 @@ impl OffsetGeometryCreation {
}
}
/// The constructor's angular tolerance (`m_dTangencyTolRadians`): `2.0 * PI / 180.0`;
/// with `Kernel::fixes.offset_four_face_vertex_split` and `offset_closes_loop_gaps`
/// (WR-666), half a degree,
/// `0.5 * PI / 180.0`, as the fork's tangency-labs/smlib@2e1bb8c.
/// The constructor's angular tolerance (`m_dTangencyTolRadians`): half a degree,
/// `0.5 * PI / 180.0` (WR-666, the fork's tangency-labs/smlib@2e1bb8c), not upstream's
/// two degrees, on the default path since #2981.
#[must_use]
pub fn default_tangency_tol_radians(fixes: &crate::kernel::UpstreamFixes) -> f64 {
if four_face_vertex_split(fixes) {
0.5 * PI / 180.0
} else {
2.0 * PI / 180.0
}
pub fn default_tangency_tol_radians() -> f64 {
0.5 * PI / 180.0
}
/// The same creator with the angular tolerance `tangency_tol_radians`, which an
@@ -2818,8 +2816,7 @@ impl OffsetGeometryCreation {
// for every sOffAdjEU - try letting trimPoint = OffAdjEu->ExtendedSurface/3DCurve intersection
let mut last_face: Option<O> = None;
// (WR-666, opt-in) each adjacent face's candidates in `solutions`
let four_face = four_face_vertex_split(&k.fixes);
// (WR-666) each adjacent face's candidates in `solutions`
let mut face_solutions: Vec<(usize, usize)> = Vec::new();
for &off_adj_eu in &off_adj_eus {
let f = brep
@@ -2857,9 +2854,7 @@ impl OffsetGeometryCreation {
// Note that the adjacent surface intersection overrides tangent intersections
// Test the solutions to see if they are not nearly tangent.
if four_face {
face_solutions.push((solutions.len(), solutions.len()));
}
face_solutions.push((solutions.len(), solutions.len()));
for sol in sols2.as_slice() {
// (neither status is checked upstream: a failed evaluation leaves its
// output at SmPoint3d's SM_UNDEF_DOUBLE default)
@@ -2885,7 +2880,7 @@ impl OffsetGeometryCreation {
.unitize_or_z();
let angle_rad = tangent.angle_between(normal)?;
let angle_deg = angle_rad * 180.0 / PI;
// `Kernel::fixes.offset_four_face_vertex_split` (WR-666, the fork's
// Four-face vertex splitting (WR-666, the fork's
// tangency-labs/smlib@2e1bb8c): a curve that crosses the surface at a
// shallow angle crosses it all the same (a trough's floor facets falling
// 1 degree, whose offsets cut the offset edges along the walls 0.6
@@ -2895,8 +2890,7 @@ impl OffsetGeometryCreation {
// round meets the top face tangentially) crosses it twice within the
// tolerance, so it crosses only where it passes through by more than
// the tolerance ([`passes_through`]).
let crosses = four_face
&& !(angle_deg < 85.0 || angle_deg > 95.0)
let crosses = !(angle_deg < 85.0 || angle_deg > 95.0)
&& passes_through(
k,
curve_of(brep, curve_3d)?,
@@ -2981,7 +2975,7 @@ impl OffsetGeometryCreation {
}
}
// `Kernel::fixes.offset_four_face_vertex_split` (WR-666, the fork's
// Four-face vertex splitting (WR-666, the fork's
// tangency-labs/smlib@2e1bb8c): where more than three faces meet at the vertex,
// the faces on either side of the edge's ends there are different faces, and
// their offsets cut the edge's offset at two points: four offset planes do not
@@ -2989,7 +2983,7 @@ impl OffsetGeometryCreation {
// and where every edge at the vertex is convex (or every one concave) the edge
// ends at the one of the two cuts its offset reaches first coming from its
// other end. With a vertex of mixed edges the nearest point stays.
if four_face && found && face_solutions.len() == 2 {
if found && face_solutions.len() == 2 {
if let Some(param) = self.first_cut_from_far_end(
k,
brep,
@@ -3067,9 +3061,9 @@ impl OffsetGeometryCreation {
}
}
/// `TrimExtendedEdge`'s choice at a vertex of more than three faces under
/// `Kernel::fixes.offset_four_face_vertex_split` (WR-666, the fork's
/// tangency-labs/smlib@2e1bb8c): when every edge at `base_vertex` is convex (or every
/// `TrimExtendedEdge`'s choice at a vertex of more than three faces under WR-666
/// (the fork's tangency-labs/smlib@2e1bb8c; the split is the only path since
/// #2981): when every edge at `base_vertex` is convex (or every
/// one concave; a lamina, tangent or shell-less edge counts as neither) and each of
/// the two adjacent faces (`face_solutions`, ranges of `solutions`) has a cut nearest
/// the vertex (not the start already chosen, nor, on an open edge's second end, the
@@ -6088,6 +6082,36 @@ mod tests {
assert!(passes_through(&k, &shallow, 0.5, &plane, uv, tol).unwrap());
}
/// The crossing test uses only the second-order jet. An implicit offset keeps
/// its three-total-derivative limit, yet must classify the same plane's touch
/// and shallow crossing (the unsupported request prevented #1818's rim mating).
#[test]
fn a_curve_passes_through_an_implicit_offset_using_only_second_order() {
use crate::surface::offset_surface::OffsetSurface;
use crate::surface::plane::Plane;
let k = Kernel::default();
let base = Surface::from(Plane::from_normal(Vec3::new(0.0, 0.0, -0.5), Vec3::Z));
let surface = Surface::from(OffsetSurface::new(&k, 0.5, base, true, 0).unwrap());
let uv = Point2::new(0.0, 0.0);
assert!(surface.evaluate_point(&k, uv).unwrap().distance(Vec3::ZERO) < 1e-15);
assert_eq!(
surface
.evaluate(&k, uv, 2, 2, EvaluateFlags::default())
.unwrap_err(),
SolidError::sm_err("surface::offset_surface::OffsetSurface::evaluate")
);
let tol = 1e-5;
let at = |e: f64| (1.0 + (6.0 * e).sqrt()) / 2.0;
let touching = over_the_plane(1e-6, 1.0 / 3.0, true);
assert!(!passes_through(&k, &touching, at(1e-6), &surface, uv, tol).unwrap());
let lifted = over_the_plane(1e-3, 1.0 / 3.0, true);
assert!(passes_through(&k, &lifted, at(1e-3), &surface, uv, tol).unwrap());
let shallow = over_the_plane(0.6_f64.to_radians().tan(), 0.0, false);
assert!(passes_through(&k, &shallow, 0.5, &surface, uv, tol).unwrap());
}
#[test]
fn creation_defaults() {
let g = OffsetGeometryCreation::new(0.25, 1e-5);
@@ -1858,18 +1858,21 @@ mod tests {
/// gen0091 of the difftest unit: a pyramid's side given the plane through its edges and
/// the side across its apex from it moved out. The four sides no longer meet in one point:
/// the apex splits into two vertices and a new edge between the two sides the moved one
/// parts (with the four-face split on). The new vertex is next to the apex, a pole of the
/// parts on the default split path. The new vertex is next to the apex, a pole of the
/// fourth side's B-spline patch, and on no extension of it (a side's extension narrows to
/// nothing at the pole): the side, which lies on a plane, continues as that plane. The
/// body is the convex solid of the five planes, a sound manifold of nine edges and six
/// vertices.
#[test]
fn a_pyramid_side_moved_past_its_apex_splits_it_on_the_planes_beside() {
assert_pyramid_split_on_its_five_planes([2, 1]);
}
/// Both selections describe the same five-plane convex solid. Check its
/// independent plane-intersection vertices as well as its volume and validity.
fn assert_pyramid_split_on_its_five_planes(selection: [usize; 2]) {
use crate::topology::brep::validation::{check_brep, CheckLevel};
let mut k = Kernel::default();
k.fixes.offset_closes_loop_gaps = true;
k.fixes.offset_splits_corner_vertices = true;
k.fixes.offset_four_face_vertex_split = true;
let mut b = bicubic_pyramid(&mut k);
let all = bp1::faces(&b, None).unwrap();
// the planes of the faces, each as (point, outward unit normal)
@@ -1885,14 +1888,15 @@ mod tests {
})
.collect();
// faces: the base, then the sides along y = -y0, x = x0, y = y0, x = -x0
let (kept, moved, distance) = (2_usize, 1_usize, 0.407_738_046_172_282_6);
let (moved, distance) = (1_usize, 0.407_738_046_172_282_6);
let mut given = planes.clone();
given[moved].0 = given[moved].0 + given[moved].1 * distance;
let surfaces = [kept, moved]
let surfaces = selection
.map(|i| Surface::from(Plane::from_normal(given[i].0, given[i].1)))
.to_vec();
let selected_faces = selection.map(|i| all[i]);
with_notifications(&mut b, |b, notes| {
local_operation(&mut k, b, &[all[kept], all[moved]], surfaces, notes)
local_operation(&mut k, b, &selected_faces, surfaces, notes)
})
.unwrap();
let (v, manifold) = volume_and_manifold(&k, &mut b);
@@ -1902,10 +1906,14 @@ mod tests {
(5, 9, 6)
);
b.use_arena_naming().unwrap();
let lines = check_brep(&mut k, &mut b, CheckLevel::Level2)
.unwrap()
.lines();
assert!(lines.is_empty(), "{lines:?}");
for level in [CheckLevel::Level0, CheckLevel::Level1, CheckLevel::Level2] {
let check = check_brep(&mut k, &mut b, level).unwrap();
assert!(
check.is_valid() && check.failure_count() == 0,
"{selection:?} {level:?}: {:?}",
check.lines()
);
}
// the convex solid of the five planes: its corners, and its volume from its faces'
// fans about the base's centre
let meet = |[i, j, l]: [usize; 3]| {
@@ -1916,6 +1924,25 @@ mod tests {
let corner = |i: usize, j: usize| meet([0, i, j]);
let (b0, b1, b2, b3) = (corner(4, 1), corner(1, 2), corner(2, 3), corner(3, 4));
let (apex, top) = (meet([2, 3, 4]), meet([1, 2, 4]));
let expected_vertices = [b0, b1, b2, b3, apex, top];
let actual_vertices: Vec<_> = b.vertices().iter().map(|(_, v)| v.point).collect();
let scale = expected_vertices
.iter()
.map(|p| p.max_dimension())
.fold(0.0, f64::max);
let same_point = |p: Vec3, q: Vec3| p.distance(q) <= 1e-9 * scale;
for &p in &actual_vertices {
assert!(
expected_vertices.iter().any(|&q| same_point(p, q)),
"{selection:?}: unexpected vertex {p:?}"
);
}
for &p in &expected_vertices {
assert!(
actual_vertices.iter().any(|&q| same_point(p, q)),
"{selection:?}: missing plane intersection {p:?}"
);
}
let faces: [&[Vec3]; 5] = [
&[b0, b3, b2, b1],
&[b0, b1, top],
@@ -2355,33 +2382,12 @@ mod tests {
}
}
/// gen0091 of the difftest unit with the default fixes: the pyramid's side moved past
/// its apex needs the four-face vertex split (opt-in), without which every end merges
/// into one vertex grown to cover the split: not sound, refused.
/// The former flag-off refusal is no longer a default behaviour. With the same
/// two replacement planes selected in reverse order, the unconditional split
/// must still build the analytically checked five-plane solid, not just succeed.
#[test]
fn a_pyramid_side_moved_past_its_apex_is_refused_without_the_four_face_split() {
let mut k = Kernel::default();
let mut b = bicubic_pyramid(&mut k);
let all = bp1::faces(&b, None).unwrap();
let plane_of = |b: &Brep, k: &Kernel, f: FaceId, d: f64| {
let s = face_ops::surface(b, f).unwrap().unwrap();
let uv = b.face(f).unwrap().uv_domain.evaluate(0.5, 0.5);
let p = s.evaluate_point(k, uv).unwrap();
let n = s.evaluate_normal(k, uv, Side::Left, Side::Left).unwrap();
let n = if n.dot(p - Vec3::new(0.0, 0.0, 25.0)) > 0.0 {
n
} else {
n * -1.0
};
Surface::from(Plane::from_normal(p + n * d, n))
};
// faces: the base, then the sides along y = -y0, x = x0, y = y0, x = -x0
let surfaces = vec![
plane_of(&b, &k, all[2], 0.0),
plane_of(&b, &k, all[1], 0.407_738_046_172_282_6),
];
let why = refused_unchanged_or_returned(&mut k, &mut b, &[all[2], all[1]], surfaces);
assert_eq!(why, Some(unsound_check(2, 61)));
fn a_pyramid_side_split_is_independent_of_replacement_selection_order() {
assert_pyramid_split_on_its_five_planes([1, 2]);
}
/// A 20 mm cube of bilinear B-spline faces whose bottom (z = 0) carries a defect of its
@@ -36,6 +36,7 @@ use solidcore::offset::offset_executive::p1::OffsetExecutive;
use solidcore::offset::offset_executive::p2::{OffsetOptions, ShellOptions};
use solidcore::primitive::api_primitives::create_box;
use solidcore::query::api_queries::brep_is_manifold_solid;
use solidcore::topology::brep::validation::{check_brep, CheckLevel};
use solidcore::{Brep, Kernel, SolidError};
/// The block with the rib `inset` in from its outer wall x = 0.
@@ -45,6 +46,18 @@ fn ribbed(k: &mut Kernel, inset: f64) -> Brep {
boolean_union(k, block, rib).unwrap()
}
fn shell_options() -> ShellOptions {
ShellOptions {
offset: OffsetOptions {
distance: -0.5,
extend_convex_edges: true,
merge_results: true,
},
create_offset_solid: true,
merge_sheet_side_walls: true,
}
}
/// The part shelled 0.5 inward open at the rib's outer side, under `fixes`.
fn shell(fixes: &UpstreamFixes, inset: f64) -> solidcore::Result<(bool, f64)> {
let mut k = Kernel::default();
@@ -57,15 +70,7 @@ fn shell(fixes: &UpstreamFixes, inset: f64) -> solidcore::Result<(bool, f64)> {
let mut wall = OffsetExecutive::shell_brep(
&mut k,
part,
ShellOptions {
offset: OffsetOptions {
distance: -0.5,
extend_convex_edges: true,
merge_results: true,
},
create_offset_solid: true,
merge_sheet_side_walls: true,
},
shell_options(),
&open,
&mut notifications.context(),
)
@@ -83,13 +88,37 @@ fn crossing(walls_touch: bool) -> UpstreamFixes {
f
}
/// Upstream (crossing loops not resolved): the merge returns no manifold solid (ten
/// edges that are not manifold, two regions, 23 faces); the shell refuses it
/// (`OffsetFailure::Unsound`, `NotManifoldSolid`, #3094) instead of returning it.
/// Without resolving the crossing loops, the cavity is open. The default shell
/// refuses that non-manifold result with `OffsetFailure::Unsound` rather than
/// returning it, retaining the input.
#[test]
fn default_fixes_refuse_the_shell_that_is_no_solid() {
let error = shell(&UpstreamFixes::default(), 0.15).unwrap_err();
let SolidError::Failed(failure) = &error else {
fn default_fixes_refuse_the_open_cavity_and_retain_the_input() {
let mut k = Kernel::default();
let mut part = ribbed(&mut k, 0.15);
part.refresh_cached_topology();
let open = planar_faces_at(&k, &part, 0, 0.15);
assert_eq!(open.len(), 1);
let mut notifications = Notifications::new(&mut part);
let key = part.key();
let ids = (
part.face_list().to_vec(),
part.edge_list().to_vec(),
part.vertex_list().to_vec(),
);
let points: Vec<_> = part
.vertices()
.iter()
.map(|(id, v)| (id, v.point, v.tolerance))
.collect();
let outcome = OffsetExecutive::shell_brep(
&mut k,
part,
shell_options(),
&open,
&mut notifications.context(),
);
let error = outcome.result.as_ref().unwrap_err();
let SolidError::Failed(failure) = error else {
panic!("not a refusal: {error}");
};
assert_eq!(
@@ -98,6 +127,35 @@ fn default_fixes_refuse_the_shell_that_is_no_solid() {
what: Unsoundness::NotManifoldSolid
})
);
assert!(!outcome.input_ownership_transferred());
let mut retained = outcome.retained_input.unwrap();
assert_eq!(retained.key(), key);
assert_eq!(
(
retained.face_list().to_vec(),
retained.edge_list().to_vec(),
retained.vertex_list().to_vec()
),
ids
);
assert_eq!(
retained
.vertices()
.iter()
.map(|(id, v)| (id, v.point, v.tolerance))
.collect::<Vec<_>>(),
points
);
// Do not refresh the retained caches: the operation must leave them coherent.
assert!(solidcore::topology::integrity::check(&retained).is_ok());
assert!(brep_is_manifold_solid(&retained).unwrap());
for level in [CheckLevel::Level0, CheckLevel::Level1, CheckLevel::Level2] {
let checked = check_brep(&mut k, &mut retained, level).unwrap();
assert!(checked.is_valid(), "{level:?}: {:?}", checked.lines());
}
let v = volume(&k, &mut retained).unwrap();
// The unchanged block (2000) and rib (3 * 16 * 3), not a partially shelled body.
assert!((v - 2144.0).abs() <= 1e-9 * 2144.0, "{v}");
}
/// The floor under the opening is kept: the shell is the closed form (with and without
@@ -1,6 +1,8 @@
//! The SolidCraft fork's tangency-labs/smlib@2e1bb8c under
//! `Kernel::fixes.offset_four_face_vertex_split` with `offset_closes_loop_gaps` (WR-666,
//! #2052), on the commit message's two cases, rebuilt:
//! The SolidCraft fork's tangency-labs/smlib@2e1bb8c (WR-666, its #2052, the default
//! path since #2981): a vertex where four plane faces meet splits into the edge their
//! two pairs' planes give, once; the loop gaps the split leaves close, and the
//! shell/inset then reaches its closed form. On the commit message's two cases,
//! rebuilt:
//!
//! - a trough 60 x 40 x 20 whose floor is four facets falling to a drain at its middle (the
//! long sides' facets fall 1 degree, the short sides' 2/3 degree, adjacent facets meet at
@@ -307,17 +309,9 @@ fn shell_pocket(fixes: &UpstreamFixes) -> Result<Shelled, String> {
shell(fixes, pocket, SLAB[2], POCKET_T)
}
/// The opt-in with the loop-gap closing it builds on (`offset_closes_loop_gaps`, WR-578).
/// The split pieces shell to their closed forms on the default path.
fn split() -> UpstreamFixes {
let mut f = gaps_only();
f.offset_four_face_vertex_split = true;
f
}
fn gaps_only() -> UpstreamFixes {
let mut f = UpstreamFixes::default();
f.offset_closes_loop_gaps = true;
f
UpstreamFixes::default()
}
fn assert_closed_form(got: &Shelled, expected: f64, faces: usize, label: &str) {
@@ -343,58 +337,19 @@ fn the_parts_have_their_closed_form_volumes() {
assert!((v - pocket_solid_volume()).abs() <= 1e-9 * v, "{v}");
}
/// Upstream (the default fixes) refuses the trough's shell: the merge fails (1001).
#[test]
fn default_fixes_refuse_the_trough_shell() {
let got = shell_trough(&UpstreamFixes::default());
assert_eq!(
got,
Err("boolean::merge::p2::make_faceuse: upstream status 1001".to_owned())
);
}
/// The loop-gap closing alone does not build it either: the corners and the drain are
/// still one vertex each.
#[test]
fn loop_gap_closing_alone_refuses_the_trough_shell() {
let got = shell_trough(&gaps_only());
assert!(got.is_err(), "{got:?}");
}
/// The flag without `offset_closes_loop_gaps` has no effect.
#[test]
fn the_flag_alone_is_the_default() {
let mut f = UpstreamFixes::default();
f.offset_four_face_vertex_split = true;
assert_eq!(shell_trough(&f), shell_trough(&UpstreamFixes::default()));
assert_eq!(shell_pocket(&f), shell_pocket(&UpstreamFixes::default()));
}
/// With the opt-in the trough (every edge at its corners and drain convex) shells to the
/// closed form: the 8 outer faces, their 8 offsets and the rim.
/// The trough (every edge at its corners and drain convex) shells to the
/// closed form on the default path: the 8 outer faces, their 8 offsets and the rim.
#[test]
fn trough_shells_to_its_closed_form() {
let got = shell_trough(&split()).unwrap();
assert_closed_form(&got, trough_shell_volume(), 17, "trough");
}
/// With the opt-in the pocket (every edge at its corners and drain concave) shells to the
/// closed form: the slab's 13 faces below the top, their 13 offsets and the two rims (round the outside and round the pocket).
/// The pocket (every edge at its corners and drain concave) shells to the
/// closed form on the default path: the slab's 13 faces below the top, their 13 offsets
/// and the two rims (round the outside and round the pocket).
#[test]
fn pocket_shells_to_its_closed_form() {
let got = shell_pocket(&split()).unwrap();
assert_closed_form(&got, pocket_shell_volume(), 28, "pocket");
}
/// Upstream (the default fixes) returns the pocket's shell, a manifold solid, at the wrong
/// volume (43302.668, the closed form 43522.321); the loop-gap closing alone refuses it.
#[test]
fn without_the_flag_the_pocket_shell_is_wrong_or_refused() {
let got = shell_pocket(&UpstreamFixes::default()).unwrap();
assert!(
(got.volume - pocket_shell_volume()).abs() > 100.0,
"{got:?} vs {}",
pocket_shell_volume()
);
assert!(shell_pocket(&gaps_only()).is_err());
}
@@ -35,6 +35,7 @@ use solidcore::offset::offset_executive::p1::OffsetExecutive;
use solidcore::offset::offset_executive::p2::{OffsetOptions, ShellOptions};
use solidcore::primitive::api_primitives::create_box;
use solidcore::query::api_queries::brep_is_manifold_solid;
use solidcore::topology::brep::validation::{check_brep, CheckLevel};
use solidcore::{Brep, Kernel, SolidError};
/// The frame, the ring cut's top at `ring_top` (2.5: a 0.5 floor under the recess).
@@ -48,6 +49,18 @@ fn frame(k: &mut Kernel, ring_top: f64) -> Brep {
boolean_difference(k, recessed, ring).unwrap()
}
fn shell_options() -> ShellOptions {
ShellOptions {
offset: OffsetOptions {
distance: -0.5,
extend_convex_edges: true,
merge_results: true,
},
create_offset_solid: true,
merge_sheet_side_walls: true,
}
}
/// The frame shelled 0.5 inward open at the ring cut's outer wall (x = 18), under
/// `fixes`: the shell's result.
fn shell_result(fixes: &UpstreamFixes, ring_top: f64) -> (Kernel, solidcore::Result<Brep>) {
@@ -61,15 +74,7 @@ fn shell_result(fixes: &UpstreamFixes, ring_top: f64) -> (Kernel, solidcore::Res
let mut wall = OffsetExecutive::shell_brep(
&mut k,
part,
ShellOptions {
offset: OffsetOptions {
distance: -0.5,
extend_convex_edges: true,
merge_results: true,
},
create_offset_solid: true,
merge_sheet_side_walls: true,
},
shell_options(),
&open,
&mut notifications.context(),
)
@@ -95,15 +100,38 @@ fn keep_cavity() -> UpstreamFixes {
f
}
/// Upstream (flag off): the sliver between the floor's crossing offsets is kept too and
/// the result is no manifold solid (five regions, eight edges that are not manifold); the
/// shell refuses it (`OffsetFailure::Unsound`, `NotManifoldSolid`, #3094) instead of
/// returning it.
/// With cavity selection off, the sliver between the crossing offsets leaves no
/// manifold cavity. The shell refuses it with `OffsetFailure::Unsound` and retains
/// the original frame.
#[test]
fn flag_off_refuses_the_shell_with_the_sliver_region() {
let (_, result) = shell_result(&UpstreamFixes::default(), 2.5);
let error = result.unwrap_err();
let SolidError::Failed(failure) = &error else {
fn flag_off_refuses_the_sliver_and_retains_the_input() {
let mut k = Kernel::default();
let mut part = frame(&mut k, 2.5);
part.refresh_cached_topology();
let open = planar_faces_at(&k, &part, 0, 18.0);
assert_eq!(open.len(), 1);
let mut notifications = Notifications::new(&mut part);
let key = part.key();
let tolerance = part.tolerance();
let ids = (
part.face_list().to_vec(),
part.edge_list().to_vec(),
part.vertex_list().to_vec(),
);
let points: Vec<_> = part
.vertices()
.iter()
.map(|(id, v)| (id, v.point, v.tolerance))
.collect();
let outcome = OffsetExecutive::shell_brep(
&mut k,
part,
shell_options(),
&open,
&mut notifications.context(),
);
let error = outcome.result.as_ref().unwrap_err();
let SolidError::Failed(failure) = error else {
panic!("not a refusal: {error}");
};
assert_eq!(
@@ -112,6 +140,36 @@ fn flag_off_refuses_the_shell_with_the_sliver_region() {
what: Unsoundness::NotManifoldSolid
})
);
assert!(!outcome.input_ownership_transferred());
let mut retained = outcome.retained_input.unwrap();
assert_eq!(retained.key(), key);
assert_eq!(retained.tolerance(), tolerance);
assert_eq!(
(
retained.face_list().to_vec(),
retained.edge_list().to_vec(),
retained.vertex_list().to_vec()
),
ids
);
assert_eq!(
retained
.vertices()
.iter()
.map(|(id, v)| (id, v.point, v.tolerance))
.collect::<Vec<_>>(),
points
);
// Check the returned caches as-is, without refreshing away an inconsistency.
assert!(solidcore::topology::integrity::check(&retained).is_ok());
assert!(brep_is_manifold_solid(&retained).unwrap());
for level in [CheckLevel::Level0, CheckLevel::Level1, CheckLevel::Level2] {
let checked = check_brep(&mut k, &mut retained, level).unwrap();
assert!(checked.is_valid(), "{level:?}: {:?}", checked.lines());
}
let v = volume(&k, &mut retained).unwrap();
// Block 2000, less the recess 200 and square ring (256 - 36) * 2.5 = 550.
assert!((v - 1250.0).abs() <= 1e-9 * 1250.0, "{v}");
}
/// The flag: only the cavity is made manifold; the shell is the closed form.
@@ -4,12 +4,14 @@
//! and the hole r 7, axes 2.5 apart, so the hole's tube passes out through the arc's offset.
//! SolidCraft's `smlib_shell` (the fork's C++) reports 1001 on its plate. Pinned SMLib
//! 1.0.1 shells this reconstruction open at the top and fails it open at y = 20 with
//! `SM_ERR` (1001), through `SmApiShellBrepFull` and through a bare `ShellBrep`; the port
//! does the same, with the same values (the `offset.offset_executive_p2` cases
//! `SM_ERR` (1001), through `SmApiShellBrepFull` and through a bare `ShellBrep` (the
//! historical `offset.offset_executive_p2` cases
//! `issue1795_plate_{api,static}_shell_2_*`; WR-617).
//!
//! `shell_open_at_the_top_shells_as_upstream` and `shell_open_at_y20_fails_as_upstream`
//! pin those default results (the API path, as `smlib_shell` calls it).
//! `shell_open_at_the_top_is_valid_on_the_default_split_path` checks the corrected
//! top-open result, including validity rather than only matching historical counts.
//! `shell_open_at_y20_fails_as_upstream` retains the side-open refusal expectation;
//! that separate regression is still unresolved.
//! `issue_1795_extended_edges_mated` (ignored) shells the plate under WR-344's opt-in
//! `offset_extended_edges_mated`, the setting that made the top-open shell fail before its
//! refinement; `issue_1795_y20_read_back` (ignored) shells the y = 20 case on every
@@ -90,27 +92,32 @@ fn options(create_offset_solid: bool) -> ShellOptions {
}
}
/// The default fixes shell the plate through the API as pinned SMLib 1.0.1 does (the
/// difftest case agrees to 0): a closed manifold solid of 16 faces, 37 edges and 24
/// vertices. Volume 9655.150697869625 since #2890 (precise properties integrate from the
/// trims' base; it was 9655.150582612916): against the closed form 9655.150444016748
/// (the plate less the blind hole less the cavity inset 2, open at z = 27.5, with the hole's
/// offset r 7 to x = 9.5 in it) +2.5e-4 (2.6e-8 relative; it was +1.4e-4), within the
/// volume's accuracy (1e-6).
/// Default vertex splitting builds a valid closed solid of 16 faces, 36 edges and
/// 24 vertices. The old 37-edge result split one straight edge at a vertex one mm
/// off its curve, hidden by a 4.166 mm vertex tolerance; validity must reject that.
/// Keep the existing volume reference 9655.150697869625 and relative tolerance
/// 1e-9 unchanged. The independent closed form is 9655.150444016748 (the plate less
/// the blind hole less the cavity inset 2, open at z = 27.5, excluding the hole's
/// offset r 7 to x = 9.5); the measured result's relative error is 2.6e-8, within
/// the volume integrator's accuracy (1e-6).
#[test]
fn shell_open_at_the_top_shells_as_upstream() {
fn shell_open_at_the_top_is_valid_on_the_default_split_path() {
let mut k = Kernel::default();
let mut b = drilled(&mut k);
let open = planar_faces_at(&k, &b, 2, 27.5);
assert_eq!(open.len(), 1, "the top face");
let r = api_shell(&mut k, &mut b, options(false), &open).unwrap();
let mut r = api_shell(&mut k, &mut b, options(false), &open).unwrap();
for level in [CheckLevel::Level0, CheckLevel::Level1, CheckLevel::Level2] {
let checked = check_brep(&mut k, &mut r, level).unwrap();
assert!(checked.is_valid(), "{level:?}: {:?}", checked.lines());
}
assert_eq!(
(
r.face_list().len(),
r.edge_list().len(),
r.vertex_list().len()
),
(16, 37, 24)
(16, 36, 24)
);
let label = "1795 shell 2 open at z 27.5";
assert!(report(&k, label, Ok(r), Some(9655.150697869625), 1e-9));
@@ -4,7 +4,7 @@
//! edges, merged results and the offset solid made (as SolidCraft's `smlib_shell` calls
//! `SmApiShellBrepFull`).
//!
//! Upstream (default fixes) refuses the shell: the merge fails with `SM_ERR` (1001) in
//! Pinned upstream refuses the shell: the merge fails with `SM_ERR` (1001) in
//! `make_faceuse` (pinned C++ agrees, `offset.offset_executive_p2` case
//! `issue1818_square_to_circle_loft_api_shell_1_top`). With the flag alone, on the fitted
//! side extensions, the difference returned ten faces, no manifold solid, volume 0 (the
@@ -14,6 +14,8 @@
//! its volume the loft's less the cavity's, measured independently ([`assert_sound`]).
//! solidcore is the reference here, not the fork's refusal. The circle-to-circle loft
//! (a cone frustum r 15) is the control: it shells to the closed form under the flag.
//! With loop-gap closure and the vertex splits always enabled, the default fixes
//! now build the same sound eleven-face shell; the old flag-off refusal is gone.
#![allow(
clippy::unwrap_used,
@@ -28,7 +30,6 @@ mod offset_fixture;
use offset_fixture::{offset_full, planar_faces_at, volume, Notifications};
use solidcore::curve::curve::Curve;
use solidcore::curve::ellipse::Ellipse;
use solidcore::error::SM_ERR;
use solidcore::geom::axis2_placement::Axis2Placement;
use solidcore::geom::extent1d::Extent1d;
use solidcore::kernel::UpstreamFixes;
@@ -125,36 +126,12 @@ fn mated() -> UpstreamFixes {
f
}
fn assert_sm_err(result: solidcore::Result<Brep>, operation: &str, label: &str) {
match result {
Err(e) if e.sm_status().is_some() => {
assert_eq!(e.sm_status(), Some(SM_ERR), "{label}");
assert_eq!(e.operation(), operation, "{label}");
}
Err(other) => panic!("{label}: SM_ERR expected, got {other}"),
Ok(mut wall) => {
wall.refresh_cached_topology();
panic!(
"{label}: SM_ERR expected, got Ok: {} faces, manifold {:?}",
wall.face_list().len(),
brep_is_manifold_solid(&wall)
);
}
}
}
/// Upstream's refusal (the default path, at parity with the pinned C++).
/// Default loop-gap closure now reaches the shell difference rather than the old early refusal.
#[test]
fn default_fixes_refuse_the_square_to_circle_shell() {
let (_, result) = shell(&UpstreamFixes::default(), true);
// The early merge refusal happens before shell_difference takes ownership.
assert!(result.retained_input.is_some());
assert!(!result.input_ownership_transferred());
assert_sm_err(
result.result,
"boolean::merge::p2::make_faceuse",
"default fixes",
);
fn default_fixes_shell_the_square_to_circle_loft() {
let (mut k, result) = shell(&UpstreamFixes::default(), true);
assert!(result.input_ownership_transferred());
assert_sound(&mut k, result.result.unwrap(), "default fixes");
}
/// The shell of the square-to-circle loft is sound: eleven faces, no failure at any check
@@ -222,26 +199,8 @@ fn mated_offsets_alone_shell_the_square_to_circle_loft() {
);
}
/// With WR-578's loop-gap closing too, the offset is still refused, now by WR-578's guard
/// (an offset of a solid that is no closed solid) and no longer by the gap closing itself:
/// at each top corner the side faces close their rim arcs' gaps to the lateral edge's
/// vertex, while the open top's face closes the same corner by a chord between the two
/// arcs' ends, so the closing edges are not shared. That inconsistency is the gap
/// closing's own (an opt-in path); the flag alone builds the sound shell above.
#[test]
fn mated_offsets_with_loop_gaps_still_refuse() {
let mut fixes = mated();
fixes.offset_closes_loop_gaps = true;
let (_, result) = shell(&fixes, true);
assert_sm_err(
result.result,
"offset::offset_executive::p2::shell_brep",
"offset_extended_edges_mated + offset_closes_loop_gaps",
);
}
/// SolidCraft's fixes (`issue_1476_swept_bar_shell.rs`, which include the flag and WR-454's
/// `shell_empty_cavity_fails`, not WR-578's loop gaps): the same sound body.
/// `shell_empty_cavity_fails`): the same sound body, with loop-gap closure unconditional.
#[test]
fn solidcraft_fixes_shell_the_square_to_circle_loft() {
let (mut k, result) = shell(&solidcraft_fixes(), true);
@@ -99,7 +99,6 @@ fn sc_fixes(k: &mut Kernel) {
k.fixes.offset_walls_touch_refused = true;
k.fixes.local_operation_refuses_loose_tangent_edges = true;
k.fixes.local_operation_step_walls = true;
k.fixes.offset_closes_loop_gaps = true;
k.fixes.local_operation_closes_walls_to_nothing = true;
k.fixes.offset_collapsed_rounds_mated = true;
k.fixes.offset_missing_surface_fails = true;
@@ -319,7 +319,6 @@ fn solidcraft_flags(k: &mut Kernel) {
k.fixes.offset_analytic_mate_edges = true;
k.fixes.offset_clip_right_side_out = true;
k.fixes.offset_closed_curve_trim_arc = true;
k.fixes.offset_closes_loop_gaps = true;
k.fixes.offset_collapsed_rounds_mated = true;
k.fixes.offset_excess_faces_after_merge = true;
k.fixes.offset_extended_edges_mated = true;
@@ -220,20 +220,17 @@ const SOLIDCRAFT_C494A18: &[&str] = &[
"offset_analytic_mate_edges",
"offset_clip_right_side_out",
"offset_closed_curve_trim_arc",
"offset_closes_loop_gaps",
"offset_collapsed_rounds_mated",
"offset_edges_keep_source_tolerance",
"offset_excess_faces_after_merge",
"offset_extended_edges_mated",
"offset_extension_as_itself",
"offset_extension_from_base",
"offset_four_face_vertex_split",
"offset_keep_cavity_regions",
"offset_missing_surface_fails",
"offset_open_edge_second_trim_point",
"offset_point_vertex_caps",
"offset_resolve_crossing_loops",
"offset_splits_corner_vertices",
"offset_stitch_at_body_tolerance",
"offset_swap_uv_extended_distance",
"offset_tangent_gap_statuses",
@@ -242,19 +242,18 @@ fn check_quarter_torus_seam_case(mut k: Kernel) -> solidcore::Result<()> {
#[test]
fn curved_shell_tangent_quarter_torus_seam_tracks_boundary() -> solidcore::Result<()> {
// `offset_closes_loop_gaps` (WR-578) runs `CloseLoopGaps` — including
// `reconcile_shell_tangent_rails` — before `do_extended_offset` merges the shell.
// `CloseLoopGaps` — including `reconcile_shell_tangent_rails` — runs on the default
// path before `do_extended_offset` merges the shell (WR-578, the split flags are the
// only path since #2981).
// 1. Default surface representations (`SmBSplineSurface` revolved wall and
// `SmOffsetSurface` over the B-spline top-rim round).
let mut k_bspline = Kernel::default();
k_bspline.fixes.offset_closes_loop_gaps = true;
let k_bspline = Kernel::default();
check_quarter_torus_seam_case(k_bspline)?;
// 2. Analytic wall and round (`revolve_analytic_profile_faces` produces `SmCone`
// for the cylindrical wall, and `fillet_exact_round_classes` produces `SmTorus`
// for the circular top-rim round).
let mut k_analytic = Kernel::default();
k_analytic.fixes.offset_closes_loop_gaps = true;
k_analytic.fixes.revolve_analytic_profile_faces = true;
k_analytic.fixes.fillet_exact_round_classes = true;
check_quarter_torus_seam_case(k_analytic)
@@ -262,11 +261,10 @@ fn curved_shell_tangent_quarter_torus_seam_tracks_boundary() -> solidcore::Resul
#[test]
fn shelled_box_open_inner_wall_shared_vertex_consistency() -> solidcore::Result<()> {
// Only `offset_closes_loop_gaps` (WR-578) is enabled so `do_extended_offset` runs
// `CloseLoopGaps` / `reconcile_shell_tangent_rails` across the open inner cavity
// wall's tangent corners before merging.
// `do_extended_offset` runs `CloseLoopGaps` / `reconcile_shell_tangent_rails`
// across the open inner cavity wall's tangent corners before merging (WR-578, the
// default path since #2981).
let mut k = Kernel::default();
k.fixes.offset_closes_loop_gaps = true;
// Issue #2954 geometry: `70 x 50 x 30` box (`[-35, 35] x [-25, 25] x [0, 30]`) with
// vertical `R = 6` corner rounds, shelled `2 mm` inward open on top (`z = 30`), then
@@ -125,8 +125,6 @@
"offset_keep_cavity_regions",
"local_operation_refuses_loose_tangent_edges",
"local_operation_step_walls",
"offset_closes_loop_gaps",
"offset_splits_corner_vertices",
"local_operation_closes_walls_to_nothing",
"offset_collapsed_rounds_mated",
"offset_missing_surface_fails",
@@ -232,7 +230,6 @@
"fillet_corner_per_vertex_fan",
"offset_closed_curve_trim_arc",
"offset_open_edge_second_trim_point",
"offset_four_face_vertex_split",
"cone_create_canonical_preserves_inside_out_and_checks_height",
"offset_analytic_mate_edges",
"shell_edges_on_analytics_exact",
@@ -264,7 +264,6 @@ fn round() -> (f64, Result<(bool, f64), i32>) {
k.fixes.offset_analytic_mate_edges = true;
k.fixes.offset_clip_right_side_out = true;
k.fixes.offset_closed_curve_trim_arc = true;
k.fixes.offset_closes_loop_gaps = true;
k.fixes.offset_collapsed_rounds_mated = true;
k.fixes.offset_excess_faces_after_merge = true;
k.fixes.offset_extended_edges_mated = true;
@@ -11,7 +11,8 @@
//! clips the face to the offset domain with a 2D boolean of planar faces made from the
//! face's UV loops. Taken from the inset's opposite face use, upstream's loops run
//! clockwise, the planar face is inside out, the intersection is empty and the bottom
//! gets no offset: the shell comes back open, of no volume. With
//! gets no offset: the offset is open, so the default shell refuses it and retains
//! the input. With
//! `offset_clip_right_side_out` the bottom is offset and the shell is closed, its
//! volume the vase's less the cavity bounded by the exact offsets (a cylinder r 13 and
//! the bottom cone moved 2 along its normal):
@@ -29,13 +30,18 @@
#[path = "support/offset_fixture.rs"]
mod offset_fixture;
use offset_fixture::{planar_faces_at, shell, volume};
use offset_fixture::{planar_faces_at, shell, volume, Notifications};
use solidcore::curve::curve::Curve;
use solidcore::error::offset::{OffsetFailure, Unsoundness};
use solidcore::error::Reason;
use solidcore::math::{Point3, Vec3, PI};
use solidcore::offset::offset_executive::p1::OffsetExecutive;
use solidcore::offset::offset_executive::p2::{OffsetOptions, ShellOptions};
use solidcore::primitive::api_curves::create_line_segment;
use solidcore::query::api_queries::brep_is_manifold_solid;
use solidcore::sweep::api_brep::create_rotational_sweep;
use solidcore::{Brep, Kernel};
use solidcore::topology::brep::validation::{check_brep, CheckLevel};
use solidcore::{Brep, Kernel, SolidError};
/// Wall radius, height of the conical bottom, height of the vase.
const R: f64 = 15.0;
@@ -99,12 +105,96 @@ fn wr_342_vase_shells_closed_with_the_fix() {
}
}
/// Upstream: the bottom gets no offset, and the shell is open, of no volume.
/// Without WR-342 the bottom gets no offset. The default shell refuses the open
/// result and retains the vase, for either rotational sweep direction.
#[test]
fn wr_342_default_leaves_the_bottom_without_an_offset() {
fn wr_342_default_refuses_the_open_shell_and_retains_the_vase() {
for up in [true, false] {
let (k, mut b) = shelled(up, false);
assert!(!brep_is_manifold_solid(&b).unwrap(), "up {up}");
assert!(volume(&k, &mut b).unwrap().abs() < 1e-9, "up {up}");
let mut k = Kernel::default();
k.fixes.revolution_offset_axis_both_sides = true;
let mut part = vase(&mut k, up);
let open = planar_faces_at(&k, &part, 1, H);
assert_eq!(open.len(), 1);
let mut notifications = Notifications::new(&mut part);
let key = part.key();
let tolerance = part.tolerance();
let ids = (
part.face_list().to_vec(),
part.edge_list().to_vec(),
part.vertex_list().to_vec(),
);
let points: Vec<_> = part
.vertices()
.iter()
.map(|(id, v)| (id, v.point, v.tolerance))
.collect();
let outcome = OffsetExecutive::shell_brep(
&mut k,
part,
ShellOptions {
offset: OffsetOptions {
distance: -2.0,
extend_convex_edges: true,
merge_results: true,
},
create_offset_solid: false,
merge_sheet_side_walls: false,
},
&open,
&mut notifications.context(),
);
let error = outcome.result.as_ref().unwrap_err();
let SolidError::Failed(failure) = error else {
panic!("up {up}: not a typed refusal: {error}");
};
assert_eq!(
failure.operation, "offset::offset_executive::p2::shell_brep",
"up {up}: {error:?}"
);
assert_eq!(
failure.reason,
Reason::Offset(OffsetFailure::Unsound {
what: Unsoundness::NotManifoldSolid
}),
"up {up}: {error:?}"
);
assert!(!outcome.input_ownership_transferred());
let mut retained = outcome.retained_input.unwrap();
assert_eq!(retained.key(), key);
assert_eq!(retained.tolerance(), tolerance);
assert_eq!(
(
retained.face_list().to_vec(),
retained.edge_list().to_vec(),
retained.vertex_list().to_vec()
),
ids
);
assert_eq!(
retained
.vertices()
.iter()
.map(|(id, v)| (id, v.point, v.tolerance))
.collect::<Vec<_>>(),
points
);
// Check the retained caches as returned, without refreshing them first.
assert!(
solidcore::topology::integrity::check(&retained).is_ok(),
"up {up}"
);
assert!(brep_is_manifold_solid(&retained).unwrap(), "up {up}");
for level in [CheckLevel::Level0, CheckLevel::Level1, CheckLevel::Level2] {
let checked = check_brep(&mut k, &mut retained, level).unwrap();
assert!(
checked.is_valid(),
"up {up}, {level:?}: {:?}",
checked.lines()
);
}
let v = volume(&k, &mut retained).unwrap();
let exact = PI * R * R * (HB / 3.0 + H - HB);
// The original cone and cylinder, not an open or partially hollowed body.
assert!((v - exact).abs() <= 1e-9 * exact, "up={up}: {v}");
}
}
@@ -2,14 +2,13 @@
//! `shelled_box_with_a_drilled_hole`): a box with a blind drilled hole (r 3, a 118
//! degree tip) shelled 2 mm inward, open at a side and, separately, at the top. The
//! hole's tip is a cone's apex whose seam is its only edge: the extended offset builds
//! no offset there, so the cone's offset ends open on a circle. `ShellBrep` then hands
//! back the offset as it is, not a closed solid (upstream returns `SM_SUCCESS` with that
//! non-manifold result, `SmOffsetExecutive.cpp:255-262`); the fork's shell fails (1001)
//! because its WR-454 check (`shell_empty_cavity_fails`, tangency-labs/smlib@fbe43bb)
//! refuses that open offset. With `offset_point_vertex_caps` the apex's offset is a
//! sphere zone of radius 2 and the shell is a closed solid.
//! no offset there, so the cone's offset ends open on a circle. Pinned SMLib returns
//! that non-manifold offset as a success (`SmOffsetExecutive.cpp:255-262`). Here the
//! default shell refuses it with or without WR-454's `shell_empty_cavity_fails` check,
//! retaining the input. With `offset_point_vertex_caps` the apex's offset is a sphere
//! zone of radius 2 and the shell is a closed solid.
//!
//! The result must pass SMLib's level-2 check (`check_brep`) clean.
//! The successful result must pass the level-2 check (`check_brep`) clean.
#![allow(
clippy::unwrap_used,
clippy::expect_used,
@@ -20,17 +19,21 @@
#[path = "support/offset_fixture.rs"]
mod offset_fixture;
use offset_fixture::{planar_faces_at, shell};
use offset_fixture::{planar_faces_at, shell, volume, Notifications};
use solidcore::boolean::api_brep::boolean_difference;
use solidcore::curve::curve::Curve;
use solidcore::math::{Point3, Vec3};
use solidcore::error::offset::{OffsetFailure, Unsoundness};
use solidcore::error::Reason;
use solidcore::math::{Point3, Vec3, PI};
use solidcore::offset::offset_executive::p1::OffsetExecutive;
use solidcore::offset::offset_executive::p2::{OffsetOptions, ShellOptions};
use solidcore::primitive::api_curves::create_line_segment;
use solidcore::primitive::api_primitives::create_box;
use solidcore::query::api_queries::brep_is_manifold_solid;
use solidcore::surface::surface::SurfaceKind;
use solidcore::sweep::api_brep::create_rotational_sweep;
use solidcore::topology::brep::validation::{check_brep, CheckLevel};
use solidcore::{Brep, Kernel};
use solidcore::{Brep, Kernel, SolidError};
/// A 40 x 40 x 20 box with a hole r 3 drilled 10 deep from the top at (20, 20), its
/// tip a 118 degree cone.
@@ -116,18 +119,109 @@ fn wr_531_drilled_hole_shells_with_a_sphere_cap() {
}
}
/// Without the opt-in the cone's offset stays open at the tip: the pinned `ShellBrep`
/// hands it back as a success, no closed solid, and WR-454's check refuses it (the
/// fork's failing shell).
/// Without a sphere cap the cone's offset stays open at the tip. The shell
/// refuses it and retains the input, regardless of the legacy WR-454 flag.
#[test]
fn wr_531_without_the_opt_in_the_shell_is_open() {
fn wr_531_without_a_tip_cap_the_shell_is_refused_and_keeps_the_input() {
for open_top in [false, true] {
let (_, b) = shelled(false, false, open_top).unwrap();
assert!(!brep_is_manifold_solid(&b).unwrap(), "open_top {open_top}");
assert_eq!(spheres(&b), 0, "open_top {open_top}");
assert!(
shelled(false, true, open_top).is_err(),
"open_top {open_top}"
);
for refuse_open in [false, true] {
let case = format!("open_top={open_top} refuse_open={refuse_open}");
let mut k = Kernel::default();
k.fixes.shell_empty_cavity_fails = refuse_open;
let mut part = drilled_box(&mut k);
let open = if open_top {
planar_faces_at(&k, &part, 2, 20.0)
} else {
planar_faces_at(&k, &part, 0, 0.0)
};
assert_eq!(open.len(), 1);
let mut notifications = Notifications::new(&mut part);
let key = part.key();
let tolerance = part.tolerance();
let ids = (
part.face_list().to_vec(),
part.edge_list().to_vec(),
part.vertex_list().to_vec(),
);
let points: Vec<_> = part
.vertices()
.iter()
.map(|(id, v)| (id, v.point, v.tolerance))
.collect();
let outcome = OffsetExecutive::shell_brep(
&mut k,
part,
ShellOptions {
offset: OffsetOptions {
distance: -2.0,
extend_convex_edges: true,
merge_results: true,
},
create_offset_solid: false,
merge_sheet_side_walls: false,
},
&open,
&mut notifications.context(),
);
let error = outcome.result.as_ref().unwrap_err();
let SolidError::Failed(failure) = error else {
panic!("{case}: not a typed refusal: {error}");
};
assert_eq!(
failure.operation, "offset::offset_executive::p2::shell_brep",
"{case}: {error:?}"
);
assert_eq!(
failure.reason,
Reason::Offset(OffsetFailure::Unsound {
what: Unsoundness::NotManifoldSolid
}),
"{case}: {error:?}"
);
assert!(!outcome.input_ownership_transferred());
let mut retained = outcome.retained_input.unwrap();
assert_eq!(retained.key(), key);
assert_eq!(retained.tolerance(), tolerance);
assert_eq!(
(
retained.face_list().to_vec(),
retained.edge_list().to_vec(),
retained.vertex_list().to_vec()
),
ids
);
assert_eq!(
retained
.vertices()
.iter()
.map(|(id, v)| (id, v.point, v.tolerance))
.collect::<Vec<_>>(),
points
);
// Inspect the returned caches without refreshing away an inconsistency.
assert!(
solidcore::topology::integrity::check(&retained).is_ok(),
"{case}"
);
assert!(brep_is_manifold_solid(&retained).unwrap(), "{case}");
assert_eq!(
spheres(&retained),
0,
"{case}: the original has no sphere cap"
);
for level in [CheckLevel::Level0, CheckLevel::Level1, CheckLevel::Level2] {
let checked = check_brep(&mut k, &mut retained, level).unwrap();
assert!(
checked.is_valid(),
"{case}, {level:?}: {:?}",
checked.lines()
);
}
let v = volume(&k, &mut retained).unwrap();
let tip = 3.0 / 59.0_f64.to_radians().tan();
let exact = 40.0 * 40.0 * 20.0 - PI * 3.0 * 3.0 * (10.0 + tip / 3.0);
// Original block less the cylindrical bore and its conical drill tip.
assert!((v - exact).abs() <= 1e-9 * exact, "{case}: {v}");
}
}
}
@@ -1,7 +1,7 @@
//! Regression test for WR-578 (the fork's tangency-labs/smlib 7ff0a94 and 34318f4): a
//! solid offset leaves a face loop's vertex behind where two consecutive edges' offsets
//! run parallel and never meet, and the merge refuses the shell (1001). With
//! `offset_closes_loop_gaps` the gap is closed by the straight edge between the two ends.
//! run parallel and never meet, and the merge refuses the shell (1001). The gap is
//! closed by the straight edge between the two ends (the default path since #2981).
//!
//! SolidCraft rig case shell/1494, rebuilt: a 47.5 x 45 x 30 box joined with an r7.5
//! cylinder along x from its side x = 47.5 to 77.5, the axis in the front face's plane
@@ -39,12 +39,6 @@ use solidcore::{Brep, Kernel};
const SHELL_1494: f64 = 8864.598272772;
fn kernel(fixed: bool) -> Kernel {
let mut k = Kernel::default();
k.fixes.offset_closes_loop_gaps = fixed;
k
}
/// The box joined with the cylinder, its seam `seam_deg` from +y towards +z.
fn box_and_cylinder(k: &mut Kernel, seam_deg: f64) -> Brep {
let body = create_box(k, Point3::new(0.0, 0.0, 0.0), 47.5, 45.0, 30.0).unwrap();
@@ -62,8 +56,8 @@ fn box_and_cylinder(k: &mut Kernel, seam_deg: f64) -> Brep {
b
}
fn shell_1494(fixed: bool, seam_deg: f64) -> (Kernel, solidcore::Result<Brep>) {
shell_1494_with(kernel(fixed), seam_deg)
fn shell_1494(seam_deg: f64) -> (Kernel, solidcore::Result<Brep>) {
shell_1494_with(Kernel::default(), seam_deg)
}
fn shell_1494_with(mut k: Kernel, seam_deg: f64) -> (Kernel, solidcore::Result<Brep>) {
@@ -81,7 +75,7 @@ fn shell_1494_with(mut k: Kernel, seam_deg: f64) -> (Kernel, solidcore::Result<B
#[ignore = "open: volume 2.7e-7 / 7.7e-7 off the fork's (WR-578)"]
fn wr_578_box_and_cylinder_shell_closes_the_loop_gaps() {
for seam in [30.0, 55.0] {
let (k, result) = shell_1494(true, seam);
let (k, result) = shell_1494(seam);
assert!(report(
&k,
&format!("wr_578 shell/1494 seam {seam}"),
@@ -92,12 +86,12 @@ fn wr_578_box_and_cylinder_shell_closes_the_loop_gaps() {
}
}
/// Seam 200 with the opt-in alone: each face's surface extended by its offset's reach (not
/// Seam 200 on the default path: each face's surface extended by its offset's reach (not
/// five offsets), the shell is sound as at seams 30 and 55 (manifold, integrity clean), its
/// volume 1.5e-7 off the fork's closed form as theirs are (WR-578's divergence).
#[test]
fn wr_578_seam_200_shells_sound() {
let (k, result) = shell_1494(true, 200.0);
let (k, result) = shell_1494(200.0);
assert!(report(
&k,
"wr_578 shell/1494 seam 200",
@@ -113,7 +107,7 @@ fn wr_578_seam_200_shells_sound() {
#[ignore = "open: the port returns a non-manifold body for these seams (WR-578)"]
fn wr_578_box_and_cylinder_shell_other_seams() {
for seam in [125.0, 200.0] {
let (k, result) = shell_1494(true, seam);
let (k, result) = shell_1494(seam);
assert!(report(
&k,
&format!("wr_578 shell/1494 seam {seam}"),
@@ -129,7 +123,7 @@ fn wr_578_box_and_cylinder_shell_other_seams() {
/// its base edge runs along, and the shell is sound as at seam 200.
#[test]
fn wr_578_seam_125_shells_sound() {
let (k, result) = shell_1494(true, 125.0);
let (k, result) = shell_1494(125.0);
assert!(report(
&k,
"wr_578 shell/1494 seam 125",
@@ -139,11 +133,11 @@ fn wr_578_seam_125_shells_sound() {
));
}
/// Without the opt-in the shell is not that sound body (upstream refuses it, 1001).
/// Loop-gap closure is now the default, so the formerly refused shell is sound.
#[test]
fn wr_578_box_and_cylinder_shell_without_the_opt_in() {
let (k, result) = shell_1494(false, 30.0);
assert!(!report(
fn wr_578_box_and_cylinder_shell_on_the_default_path() {
let (k, result) = shell_1494(30.0);
assert!(report(
&k,
"wr_578 shell/1494 default",
result,
@@ -162,7 +156,7 @@ fn notched(k: &mut Kernel) -> Brep {
#[test]
fn wr_578_shell_thicker_than_a_step_is_edge_vanishes() {
let mut k = kernel(true);
let mut k = Kernel::default();
let b = notched(&mut k);
let strip = planar_faces_at(&k, &b, 1, 35.0);
assert_eq!(strip.len(), 1, "the far side under the notch");
@@ -174,7 +168,7 @@ fn wr_578_shell_thicker_than_a_step_is_edge_vanishes() {
#[test]
fn wr_578_notched_box_open_at_the_floor_shells() {
let mut k = kernel(true);
let mut k = Kernel::default();
let b = notched(&mut k);
let floor = planar_faces_at(&k, &b, 2, 2.5);
assert_eq!(floor.len(), 1, "the notch's floor");
@@ -192,7 +186,7 @@ fn wr_578_notched_box_open_at_the_floor_shells() {
/// a half circle r 10 (x < 0), a blind hole r 5 along x through the round side at
/// (y, z) = (12.5, 12.5) from x = -10 to 7.5, shelled 2 inward, open at x = 42.5, on a
/// clean operand. A prior mass-properties query on the operand changes what the shell
/// reads (UV trims, a snapped trim, gap caches: WR-583), and the opt-in then refuses it;
/// reads (UV trims, a snapped trim, gap caches: WR-583), and closure then refuses it;
/// nothing here measures the operand.
fn plate_1528(k: &mut Kernel) -> Brep {
// the profile swept 27.5 along z (one curved side face), as the #1528 fixture builds it
@@ -220,8 +214,8 @@ fn plate_1528(k: &mut Kernel) -> Brep {
b
}
fn shell_1528(fixed: bool) -> (Kernel, solidcore::Result<Brep>) {
let mut k = kernel(fixed);
fn shell_1528() -> (Kernel, solidcore::Result<Brep>) {
let mut k = Kernel::default();
let b = plate_1528(&mut k);
let open = planar_faces_at(&k, &b, 0, 42.5);
assert_eq!(open.len(), 1, "the far end");
@@ -231,24 +225,13 @@ fn shell_1528(fixed: bool) -> (Kernel, solidcore::Result<Brep>) {
#[test]
fn wr_578_issue_1528_plate_shells_on_a_clean_operand() {
let (k0, default) = shell_1528(false);
let default = default.unwrap();
assert_eq!(default.face_list().len(), 16, "the default shell's faces");
assert_eq!(default.edge_list().len(), 37, "the default shell's edges");
assert!(report(
&k0,
"wr_578 #1528 plate default",
Ok(default),
None,
0.0
));
let (k, fixed) = shell_1528(true);
let (k, fixed) = shell_1528();
let fixed = fixed.unwrap();
assert_eq!(fixed.face_list().len(), 16, "the opt-in shell's faces");
assert_eq!(fixed.edge_list().len(), 36, "the opt-in shell's edges");
assert_eq!(fixed.face_list().len(), 16, "the default shell's faces");
assert_eq!(fixed.edge_list().len(), 36, "the default shell's edges");
assert!(report(
&k,
"wr_578 #1528 plate opt-in",
"wr_578 #1528 plate default",
Ok(fixed),
// since #2890 (precise properties integrate from the trims' base): +4.8e-4 from the
// closed form 10400.212373846494 (issue_1950's), within the volume's accuracy
@@ -2,8 +2,8 @@
//! four plane faces whose offset planes no longer meet in one point. The offset of each
//! edge there ends at the same pair of the planes' meeting points whichever way the
//! corner splits, so an inset, or a shell open at one of the four faces, gets loop gaps
//! and no volume (or the merge refuses it). With `offset_closes_loop_gaps` and
//! `offset_splits_corner_vertices` the corner becomes the edge the offset needs.
//! and no volume (or the merge refuses it). The corner becomes the edge the offset
//! needs (the default path since #2981).
//!
//! A 30 x 20 x 10 box with edges 0 (the top's edge at x = 30) and 9 (the top's edge at
//! y = 0) chamfered 2 (`SmApiFilletEdges`, a linear section), with edge 8 (the upright
@@ -22,7 +22,6 @@ mod offset_fixture;
use offset_fixture::{offset_full, planar_faces_at, report, shell, Notifications};
use solidcore::blend::fillet_executive::api::fillet_edges;
use solidcore::error::SM_ERR;
use solidcore::offset::offset_executive::p2::{api_offset_brep_full, OffsetOptions};
use solidcore::primitive::api_primitives::{create_box, create_sphere};
use solidcore::topology::brep::p1 as bp1;
@@ -32,13 +31,6 @@ use solidcore::{Brep, FaceId, Kernel, Point3};
/// Relative tolerance of the volumes (the port lands within 1e-12 of each).
const REL: f64 = 1e-9;
fn kernel(fixed: bool) -> Kernel {
let mut k = Kernel::default();
k.fixes.offset_closes_loop_gaps = fixed;
k.fixes.offset_splits_corner_vertices = fixed;
k
}
/// The box with `edges` (indices into its edge list; `None`: every edge) chamfered 2.
fn chamfered(k: &mut Kernel, edges: Option<&[usize]>) -> Brep {
let mut b = create_box(k, Point3::new(0.0, 0.0, 0.0), 30.0, 20.0, 10.0).unwrap();
@@ -110,14 +102,10 @@ fn no_loop_gaps(k: &mut Kernel, b: &Brep) -> bool {
fn wr_657_two_chamfers_shell_inward_open_at_a_wall() {
// the reported case: shelled 1 inward open at the wall at x = 30
let want = 1806.1797017478002;
let shell_x30 = |fixed: bool| {
let mut k = kernel(fixed);
let b = chamfered(&mut k, Some(&[0, 9]));
let open = x30(&k, &b);
let result = shell(&mut k, b, -1.0, &[open]);
(k, result)
};
let (k, result) = shell_x30(true);
let mut k = Kernel::default();
let b = chamfered(&mut k, Some(&[0, 9]));
let open = x30(&k, &b);
let result = shell(&mut k, b, -1.0, &[open]);
assert!(report(
&k,
"wr_657 two chamfers shell -1 x30",
@@ -125,15 +113,6 @@ fn wr_657_two_chamfers_shell_inward_open_at_a_wall() {
Some(want),
REL
));
// upstream (the pinned C++ too): Ok, no closed solid, volume 0
let (k, result) = shell_x30(false);
assert!(!report(
&k,
"wr_657 two chamfers shell -1 x30 default",
result,
Some(want),
REL
));
}
#[test]
@@ -147,7 +126,7 @@ fn wr_657_two_chamfers_shells_at_each_corner_face() {
(chamfer_9, 1.0, 2226.2751561773057),
];
for (i, (open, d, want)) in cases.into_iter().enumerate() {
let mut k = kernel(true);
let mut k = Kernel::default();
let b = chamfered(&mut k, Some(&[0, 9]));
let open = open(&k, &b);
let result = shell(&mut k, b, d, &[open]);
@@ -166,11 +145,14 @@ fn wr_657_two_chamfers_shell_outward_has_no_loop_gaps() {
// upstream's volume is right (2152.620538, the C++ too) but two loops have a gap
// of sqrt(2)
let want = 2152.6205383941633;
let mut k = kernel(true);
let mut k = Kernel::default();
let b = chamfered(&mut k, Some(&[0, 9]));
let open = x30(&k, &b);
let result = shell(&mut k, b, 1.0, &[open]).unwrap();
assert!(no_loop_gaps(&mut k, &result), "loop gaps with the opt-in");
assert!(
no_loop_gaps(&mut k, &result),
"loop gaps on the default path"
);
assert!(report(
&k,
"wr_657 two chamfers shell +1 x30",
@@ -178,17 +160,12 @@ fn wr_657_two_chamfers_shell_outward_has_no_loop_gaps() {
Some(want),
REL
));
let mut k = kernel(false);
let b = chamfered(&mut k, Some(&[0, 9]));
let open = x30(&k, &b);
let result = shell(&mut k, b, 1.0, &[open]).unwrap();
assert!(!no_loop_gaps(&mut k, &result), "upstream leaves loop gaps");
}
#[test]
fn wr_657_three_chamfers_offsets() {
for (d, want) in [(1.0, 8238.532227977406), (-1.0, 3979.9694554143525)] {
let mut k = kernel(true);
let mut k = Kernel::default();
let mut b = chamfered(&mut k, Some(&[0, 9, 8]));
let result = offset_full(&mut k, &mut b, d);
assert!(report(
@@ -199,24 +176,12 @@ fn wr_657_three_chamfers_offsets() {
REL
));
}
// upstream: Ok with loop gaps and volume 0 at +1, refused (1001) at -1. With each plane's
// extension on the plane itself the default reaches the closed form at +1 too.
let mut k = kernel(false);
let mut b = chamfered(&mut k, Some(&[0, 9, 8]));
let result = offset_full(&mut k, &mut b, 1.0);
assert!(report(
&k,
"wr_657 three chamfers offset +1 default",
result,
Some(8238.532227977406),
REL
));
}
#[test]
fn wr_657_three_chamfers_shells() {
// open at the wall at x = 30 (1 each way) and at the corner's triangle (1 outward)
let mut k = kernel(true);
let mut k = Kernel::default();
let b = chamfered(&mut k, Some(&[0, 9, 8]));
let open = x30(&k, &b);
let result = shell(&mut k, b, -1.0, &[open]);
@@ -227,7 +192,7 @@ fn wr_657_three_chamfers_shells() {
Some(1807.385883644974),
REL
));
let mut k = kernel(true);
let mut k = Kernel::default();
let b = chamfered(&mut k, Some(&[0, 9, 8]));
let open = x30(&k, &b);
let result = shell(&mut k, b, 1.0, &[open]);
@@ -238,7 +203,7 @@ fn wr_657_three_chamfers_shells() {
Some(2155.9012941491765),
REL
));
let mut k = kernel(true);
let mut k = Kernel::default();
let b = chamfered(&mut k, Some(&[0, 9, 8]));
let open = face_at(&k, &b, [28.0 + 2.0 / 3.0, 4.0 / 3.0, 8.0 + 2.0 / 3.0]);
let result = shell(&mut k, b, 1.0, &[open]);
@@ -254,7 +219,7 @@ fn wr_657_three_chamfers_shells() {
#[test]
fn wr_657_fully_chamfered_box() {
for (d, want) in [(1.0, 7654.848301953257), (-1.0, 3831.755643314821)] {
let mut k = kernel(true);
let mut k = Kernel::default();
let mut b = chamfered(&mut k, None);
let result = offset_full(&mut k, &mut b, d);
assert!(report(
@@ -283,7 +248,7 @@ fn wr_657_fully_chamfered_box() {
([29.0, 10.0, 9.0], 1.0, 2009.1803204583593),
];
for (at, d, want) in cases {
let mut k = kernel(true);
let mut k = Kernel::default();
let b = chamfered(&mut k, None);
let open = face_at(&k, &b, at);
let result = shell(&mut k, b, d, &[open]);
@@ -298,25 +263,26 @@ fn wr_657_fully_chamfered_box() {
}
/// An inset of a chamfer's width leaves the cavity meeting the open chamfer along a line
/// only (the body is no manifold solid): refused, not returned.
/// only (the body is no manifold solid): the default four-face split detects a
/// vanishing edge before the final manifold-result guard, and refuses it by name.
#[test]
fn wr_657_fully_chamfered_box_inset_open_at_a_chamfer_is_refused() {
let mut k = kernel(true);
let mut k = Kernel::default();
let b = chamfered(&mut k, None);
let open = face_at(&k, &b, [29.0, 10.0, 9.0]);
match shell(&mut k, b, -1.0, &[open]) {
Err(e) => assert_eq!(e.sm_status(), Some(SM_ERR)),
other => panic!("SM_ERR expected: {other:?}"),
Err(e) => assert_eq!(e.sm_status(), Some(1043)),
other => panic!("SM_ERR_OFFSET_EDGE_VANISHES expected: {other:?}"),
}
}
/// An offset asked not to merge leaves its overlapping faces (`OffsetBrep`, `bDoSelfInt`), so a
/// result that is no closed solid is what was asked for: a 10 mm cube inset 1, convex edges
/// not extended, comes back as with the flags off, as it does from the fork at d01d7b0.
/// not extended, still comes back with faces on the default split path.
#[test]
fn wr_657_unmerged_offset_is_not_refused() {
let faces = |fixed: bool| {
let mut k = kernel(fixed);
let faces = || {
let mut k = Kernel::default();
let mut b = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 10.0, 10.0, 10.0).unwrap();
let mut n = Notifications::new(&mut b);
let options = OffsetOptions {
@@ -325,33 +291,27 @@ fn wr_657_unmerged_offset_is_not_refused() {
merge_results: false,
};
api_offset_brep_full(&mut k, Some(&mut b), options, &mut n.context())
.unwrap_or_else(|e| panic!("fixed {fixed}: {e:?}"))
.unwrap_or_else(|e| panic!("{e:?}"))
.face_list()
.len()
};
let with_flags = faces(true);
assert!(with_flags > 0);
assert_eq!(with_flags, faces(false));
assert!(faces() > 0);
}
/// #2092: a sphere of radius 10 offset by -10 (convex edges not extended) leaves nothing.
/// With both flags on, the empty body comes back as with either off: a result with no
/// faces is not refused as a broken solid.
/// The splits are the only path since #2981: an empty body still comes back as itself, no
/// broken-solid refusal on a result with no faces.
#[test]
fn issue_2092_sphere_offset_to_its_centre_is_empty() {
for flags in [(false, false), (true, false), (false, true), (true, true)] {
let mut k = Kernel::default();
k.fixes.offset_closes_loop_gaps = flags.0;
k.fixes.offset_splits_corner_vertices = flags.1;
let mut ball = create_sphere(&mut k, Point3::new(0.0, 0.0, 0.0), 10.0).unwrap();
let mut n = Notifications::new(&mut ball);
let options = OffsetOptions {
distance: -10.0,
extend_convex_edges: false,
merge_results: true,
};
let out = api_offset_brep_full(&mut k, Some(&mut ball), options, &mut n.context())
.unwrap_or_else(|e| panic!("flags {flags:?}: {e:?}"));
assert!(out.face_list().is_empty(), "flags {flags:?}: no faces");
}
let mut k = Kernel::default();
let mut ball = create_sphere(&mut k, Point3::new(0.0, 0.0, 0.0), 10.0).unwrap();
let mut n = Notifications::new(&mut ball);
let options = OffsetOptions {
distance: -10.0,
extend_convex_edges: false,
merge_results: true,
};
let out = api_offset_brep_full(&mut k, Some(&mut ball), options, &mut n.context())
.unwrap_or_else(|e| panic!("{e:?}"));
assert!(out.face_list().is_empty(), "no faces");
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,39 @@
# Local goldens failures: one clean-base control, cause not established
Found: 2026-11-07, while re-verifying the #2981 flag-retirement lane locally.
## What was observed
A full goldens sweep (`cargo run --profile goldens -p goldens -- --all --jobs 4 --goldens`)
on the PR head a4e81596c, based on 7efbd5090, reported 84 of 283 units failing
on aarch64 macOS. This was not a full sweep of the clean base. One inspected
failure was in a compact integer-array summary
(`[sum, weighted_sum]` per chunk):
`topology.brep_constructor`'s `hexagon_prism_separate_stitched`, where only the
topology-int weighted sum moves (`383570` -> `383763`) while its other reported
fields are unchanged. Its array length stays the same. These summaries represent
actual topology entries; a difference is not harmless merely because it is compacted.
The clean base 7efbd5090 reproduced that unit's one changed case in a scratch
worktree. This establishes a pre-existing local failure for that case only. It
does not attribute the other 83 failing units to the baseline or establish a
cross-platform cause. Missing upstream fixtures, including `Platform.smp`, were
also reported by the PR-head sweep and need separate investigation.
## CI evidence and limits
Main's CI run 42399108643 at 8d7a0bf07 completed successfully, but it was a
`push` run whose `goldens`, `clippy`, and `wasm` jobs were **skipped**. It is not
evidence of a successful goldens comparison on another platform or on 7efbd5090.
No same-revision cross-platform comparison has been performed here.
The PR's four focused units passed locally against their committed expectations.
That establishes consistency with those expectations, not geometric correctness
or a successful full sweep. Remaining failures must be compared against a matching
clean base before being attributed or dismissed; canonical exact-head CI remains
unverified while its workflow requires approval.
## Related investigation
Issue #2929 concerns a QP attempt-budget difference on aarch64 Linux/GB10. It does
not establish the cause of these unrelated local goldens failures.
@@ -0,0 +1,88 @@
# The crossing classifier requested derivatives it never consumes
- **Units:** `offset.offset_geometry_creation`, `offset.offset_executive_p2`.
- **Reported case:** #1818, the square-to-circle loft shelled inward by 1, open at
its top. This is a correction within #3056, not a new construction or flag.
- **Measured predecessor:** `0c53c6e0ebac6cd3eedcfa7416750c14b706b72c`, based on
`b13f07fbdd7fe298f923911af88e266bb9fe4ea2`.
## Cause and bounded correction
An isolated diagnostic snapshot records four `MateEdges` failures at
`offset_geometry_creation::passes_through`. Every backtrace reaches
`OffsetSurface::evaluate` with `hu=2`, `hv=2`, `only_upper_half=false`. That is a
fourth-total-order request, correctly refused by the implicit offset's existing
maximum total order of three. The crossing formula reads only `Su`, `Sv`, `Suu`,
`Suv` and `Svv`, so it needs only the second-order triangular jet.
The correction requests that jet with `only_upper_half=true`. It changes no
formula, derivative limit, normal treatment, tolerance or requested-grid contract.
The equal `(2,2)` triangular grid is not an unequal-grid alias workaround. No pole
patch, retry or fallback is added.
The earlier stale-mating-bookkeeping hypothesis was refuted: original rim edges
2, 6, 10 and 14 were already unmated before loop closure, and SSI remained enabled
for both incident faces. No speculative invalidation was added. After the caller
correction, the diagnostic #1818 selection has no derivative refusal and no
gap-closing split; all four active tests pass (one generator stays ignored).
## Regression and verification
The new regression offsets the plane `z=-0.5` by `0.5`, independently confirming
that its point is on `z=0`. It preserves the full `(2,2)` refusal and checks a
within-tolerance touch, a lifted quadratic crossing and a shallow straight
crossing against the existing geometric rule. With the production caller
unchanged, the plane control passes and this regression fails on the unsupported
request. With only the triangular caller correction, both pass.
The uncommitted source measured in the primary worktree has SHA-256
`2713a22ea271eb08487333a7693794da0d57abbb5a073bd55ea41668772170e9`.
Rust 1.98.0, `CARGO_BUILD_JOBS=2`, commands under `nice -n 10`:
- Both crossing regressions and all seven creator-module tests pass.
- All four active #1818 tests pass, including the unchanged Level-0/1/2 validity,
outward-solid, eleven-face and independent cavity-slab-volume assertions.
- The 17-binary integration selection records 56 passed, 5 failed, 9 ignored;
the predecessor records 53 passed, 8 failed, 9 ignored. The three repaired
failures are #1818. The other failures are not waived.
- Local-operation tests remain 28 passed, 7 failed, 2 ignored. No helper,
reason assertion, rollback assertion or soundness-gate implementation changes.
- Workspace fmt, workspace/all-target clippy (`-D warnings`), wasm32 and panic
inventory pass.
- All 2,745 cases of the four affected golden units remain selected, including
121 existing known failures; none is added, removed or broken. The real gate
still has 43 p2, 1 geometry-creation and 13 local-operation differences.
A complete ordinary-record comparison against the predecessor finds exactly one
caller-induced change: `issue1818_square_to_circle_loft_api_shell_1_top`, from
status 1001/no body to status 1000/ownership transferred and result counts
`[2,2,11,12,24,96,16]` (regions, shells, faces, loops, edges, edgeuses, vertices),
with bbox `[-20,-20,0,20,20,60]`. It was already an unapproved changed case; no
previously matching ordinary case moves. All other ordinary encodings agree.
The temporary raw-capture directory has no failure registry and preserves all
known-case outputs too; it is not a second successful gate or an approval to
bless them. Repository goldens and failure entries remain untouched.
## Native views and limitations
The same public-stage builder, view `-35,60` and image size `1200x900` are used.
The before offset has 6 faces / 20 edges / 16 vertices and is non-manifold; the
corrected merged offset has 6 / 12 / 8 and is manifold. The two offset images are
explicitly native-edge wireframes: shaded intermediate rendering is unsupported
because a face has no exact record. No intermediate meshing success is claimed.
| Before caller correction, native edges only | After caller correction, native edges only |
|---|---|
| ![Before](figures/pr3056-caller-offset-before.png) | ![After](figures/pr3056-caller-offset-after.png) |
The corrected shell is 11 / 24 / 16 and manifold on both default and mated paths.
Its native shaded render succeeds with 2,368 triangles and no refused face:
![Corrected shell, native shaded render](figures/pr3056-caller-shell-after.png)
These images come from the diagnostic snapshot plus the single caller correction;
the direct, uninstrumented primary-worktree tests supply the validity and volume
evidence. The independently measured old-main control remains attributed to
`6432dafe7d716a973a653fca72d5d4c7cd7002c8`, not to the newer integrated base.
This record claims neither a full-suite/all-unit-goldens pass nor canonical CI
success. The remaining #3056 construction and expectation blockers remain open.
@@ -0,0 +1,124 @@
# Disk results: repeated validation and a boundary-preservation hold (2026-11-08)
Evidence-only continuation of #3056. This reconciles the earlier soundness-only
probe with the direct checks recorded beside the two justified pyramid results.
It does not bless a disk, change a failure entry, implement a gate, or take
#3084's per-entity matching work. The eleven disk rows remain unapproved.
## Four fresh sequences per exact retained case
Measured at published source head
`ec0f6558107e36c48e7f9063b9ebfc05a2c3fe5f`. For each case, the public
`topology_brep_local_operation::result_breps` reconstructs a new body and kernel
independently for each sequence:
1. `validation::soundness`, three times on the same body;
2. direct `check_brep(Level2)`, three times;
3. `validation::standing`, three times;
4. relabel this private diagnostic body's geometry with `use_arena_naming`, then
`soundness`, three times.
There are 52 fresh constructions and 156 checks. No rename occurs in sequences
1–3. All four sequences agree case by case:
| Case | First / second / third check | Stored UV trims before / after first check |
|---|---|---|
| `gen0001` | pass / fail / fail | 0 / 4 |
| `gen0014` | pass / pass / pass | 0 / 4 |
| `gen0019` | pass / pass / pass | 0 / 4 |
| `gen0020` | pass / fail / fail | 0 / 4 |
| `gen0023` | pass / fail / fail | 0 / 4 |
| `gen0029` | pass / fail / fail | 0 / 4 |
| `gen0049` | pass / fail / fail | 0 / 4 |
| `gen0056` | pass / fail / fail | 0 / 4 |
| `gen0073` | pass / fail / fail | 0 / 4 |
| `gen0078` | pass / fail / fail | 0 / 4 |
| `gen0086` | pass / fail / fail | 0 / 4 |
| `gen0040` (pyramid control) | pass / pass / pass | 0 / 18 |
| `gen0068` (pyramid control) | pass / pass / pass | 0 / 18 |
Every failing later pass reports:
```text
SmTrimmingTools::Test #3 Loop Orientation: Face Loop has bad orientation [owner SmFace]
AssertValid skipped: ValidatePointers failed
```
Topology counts and all body/face/edge/vertex tolerances stay unchanged across
the first check in every sequence. The disk results have eight edge uses; the
first check materializes four UV trims. The pyramid controls have 36 edge uses
and materialize 18. This refutes a naming-only explanation for the discrepancy:
the unchanged `soundness` function itself returns pass then fail, without any
relabeling. It does not prove that every internal cache or derived curve stays
unchanged; the UV curves expressly do not.
The source explains the relevant ordering: `trimming_tools::check_face` returns
without checking a loop when a stored UV curve is missing. Other checking paths
can create those curves (`face::p3::compute_loop_orientation`, for example).
The next pointer-validation pass then has curves to grade. These measurements
do not choose a repair to that shared checking pipeline. Repeating a check in a
new production gate merely to obtain a different verdict would not repair the
construction. No such retry or gate change is added here.
## A separate original-input boundary witness: gen0001
The unchanged case replaces the one face of `disk.smb` with a parallel plane:
offset `0.9196834024495337`, tilt `[0,0,0]`. Its original boundary is a rational
quadratic upper semicircle and its diameter. The original 3D homogeneous control
points are read directly from the retained file, not from kernel derivatives,
sampled edges, a result, or a golden. All weights are positive. Therefore each
curve, and the intended parallel translation of its bounded face, lies within
the Euclidean control-coordinate bounds:
```text
x in [-5, 5], y in [0, 5], z = 0.9196834024495337
```
The proof uses exact rational arithmetic on the binary64 file inputs. In
particular, `y <= 5` is a whole-curve positive-weight convex-hull bound, not a
sampled estimate. The freshly returned result, captured before any check or
owner rebinding, has a vertex at
`[1.7535453870102995,7.5,0.9196834024495337]`: its y coordinate is **exactly 2.5**
outside that bound. Another is at y `-2.4999999999999996`. This demonstrates a
tangential boundary change, not just a different edge split of the original
parallel-moved boundary. The result body tolerance is `1e-5`, not remotely that
gap. No tolerance comparison is loosened.
The witness vertex's exact binary64 bits are
`[4610576085524163749,4620130267728707584,4606458991202417516]`.
The original `.smb` SHA-256 is
`9b0c3f1aa62d191364c484f2a3ad23ee6e902e93c94759d5d8aab4392bc5202f`.
All four input-hull controls pass: exact weighted bounds and z-only translation;
nonpositive-weight rejection; the actual separation witness; and tampered-bit or
tilted-case rejection. This is a negative preservation witness, not a complete
boundary certificate for either result or the two tilted disk cases whose
repeated checks stay clean. Those two are not blessed either.
## Native views
These are native `solidcore-diag` edge renders at the same `--view 0,90`, size
`1200x900`; each image is independently framed by the renderer. The first is the
original input, the second the exact generated result, not a hand-authored
schematic or an invented before-result. Looking down z, the expected z-only
translation keeps the input outline.
![Original semicircle and diameter.](figures/pr3056-disk-input.png)
![gen0001 result: the boundary is no longer the original semicircle and diameter.](figures/pr3056-disk-gen0001.png)
## Evidence and scope
The 27-member executable/raw/native-picture archive was read back and
hash-verified, SHA-256
`170977910ee4732b3421637ccfbdcff33f8c8923506f2424774d4eedc3cdd7d8`.
It retains every sequence's before/after states and verdicts, the original
inputs, exact output bits, artifact identities, the input-hull model and controls,
the original input and native result `.smb`, and both pictures. Local path:
`~/.local/share/opencode/solidcore-cleanup-evidence/pr3056-disk-check-order-20261108-ec0f65581/`.
No production, case input, assertion, golden, known-failure registry, tolerance,
floor or gate changes accompany this record. All 54 ordinary differences and
the six failing local-operation tests remain open. This evidence does not
implement #3073's tangent-round construction, #3084's gate, #3124's kept-edge
restoration, #3174's manifold-result refusal, #3015's shell work, or the excluded
pole-patch construction. The draft hold remains in force.
@@ -0,0 +1,120 @@
# Two generated pyramid results meet their five-plane solids (2026-11-08)
Continues #3056 / #2981. Only `gen0040` and `gen0068` in
`topology.brep_local_operation` are justified here. No production code, tolerance,
failure registry, case input, gate or excluded pole-patch construction changes.
The other eleven local-operation golden differences remain unresolved.
## Exact cases and independent construction
Both cases read `upstream/TestFiles/pt_TestFiles/Solids/pyramid.smb`, without
rebuilding its trims. Their original input body has 5 faces / 8 edges / 5 vertices.
The cases replace three faces with explicit planes:
- `gen0040`: face 0 unchanged; face 1 offset by `-0.7722087962601212`, no tilt;
face 2 unchanged.
- `gen0068`: face 2 unchanged; face 1 offset by `-0.7342210955636247`, tilt
`[-0.18041196740136894,0.1762487457010129,-0.20331349053160247]`; face 0
offset 0, tilt `[-0.1304434045850086,-0.1498474917575508,-0.007725377686032431]`.
The runner's actual plane data is `p + offset*n`, normal `n + tilt`, from each
original face's middle point and normal, not from the result. The independent
Python calculation orients those five planes outward, enumerates all triples'
closed-form intersections by Cramer's rule, and retains points in all five
halfspaces. It builds each convex facet from those intersections and sums its
oriented tetrahedra. No result vertex or kernel volume enters that calculation.
It evaluates at 80 and 160 decimal digits, converting binary64 inputs exactly.
The two kept patches' complete homogeneous input nets lie on their planes to a
maximum coefficient-distance residual of `6.075e-15`; this is below their plane
dot-product rounding guard (`gamma_32` times the coordinate magnitude). The
rounded patches with residual `1.447e-6` are **replaced** by explicit planes,
not continued through uncertified poles. That distinction is essential: this
proof does not certify the excluded curved/nonplanar pole construction.
## Measured geometry, topology and validity
Measured at exact source head `134a7186f1a14abf84ee4d0115c8cc6c302b6001`, before
these expectation-only edits. The source body volume measured in this run is
`983422.1665753094`; that is not relabelled as a historical old-result volume.
| Case | New F/E/V | Kernel volume | Independent five-plane volume | Relative error | Maximum two-way vertex distance |
|---|---|---|---|---|---|
| `gen0040` | 5/9/6 | `992044.1995192543` | `992044.1995195164535906177425004735` | `2.643e-13` | `8.152e-11` |
| `gen0068` | 5/9/6 | `1093044.507611554` | `1093044.507611594137640470483235565` | `3.662e-14` | `9.620e-11` |
Both are manifold solids and all direct Level-0/1/2 checks are clean. Each has
exactly the six independent intersections, checked in both directions at the
existing `1e-9` part-scale vertex rule. Volumes satisfy the unchanged `1e-9`
relative rule, with no new floor. Relative 80/160-digit volume drift is below
`1e-79`. All six oracle controls pass: exact cube corners/volume; translated cube
and reordered planes; the actual pyramid; wrong volume and vertex rejection;
nonplanar kept-patch/invalid-body rejection; and the distinction between an
explicitly replaced rounded patch and a kept patch.
## Native pictures
The existing `case_smb` example rebuilt the exact generated cases; the native
`solidcore-diag` renderer drew their edges, not a hand-authored schematic. All
three views use `--wireframe --view -35,30 --size 1200x900 --edges-above 0.001`.
The small `gen0040` split is barely resolved in the whole-part view; its numeric
vertex and edge checks above, not visual similarity, establish the split.
![Original pyramid, whose four sides meet at one apex.](figures/pr3056-pyramid-input.png)
Original input: four sides meet at one apex.
![gen0040 after the face edit, with the short split at its apex.](figures/pr3056-pyramid-gen0040.png)
`gen0040`: the short split is measured as six vertices and nine edges.
![gen0068 after the tilted plane edits, with two separated top vertices.](figures/pr3056-pyramid-gen0068.png)
`gen0068`: the two top intersections and the edge between them are visible.
## Complete old and new golden encodings
The old encoding did not contain a volume field; the source-volume observation
above is kept separate from it. Every replacement below equals the retained
actual runner encoding. All other rows and every known-failure entry stay
byte-unchanged.
`gen0040`, old:
```json
{"case":"gen0040","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1172,159991],[1948,311927],[978,28202]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.4e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[156360.52780532837,158006.30563557148,19068776.25026989],[153903.1388885975,156472.02783429623,20181493.582973957],[104892.00006401539,108480.00015670061,12407745.286694974],[258.6666741371155,509.33334589004517,2804.4445185661316]],"n":784},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[360.0,392.0,9416.0]],"n":32}}}
```
`gen0040`, new:
```json
{"case":"gen0040","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1360,199480],[1748,274861],[1932,107271]],"n":623}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0],"geometry":{"blocks":[[7.2e+21,7.200000000000017e+21,2.628e+23]],"n":148},"record":{"blocks":[[143904.70981994172,148850.83660596592,18749454.884977996],[60967.80078066501,91192355.46067868,416542727.02352035]],"n":460},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-36.0,36.0,-666.0]],"n":36}}}
```
`gen0068`, old:
```json
{"case":"gen0068","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1172,159991],[1948,311927],[978,28202]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.4e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[156360.52780532837,158006.30563557148,19068776.25026989],[153903.1388885975,156472.02783429623,20181493.582973957],[104892.00006401539,108480.00015670061,12407745.286694974],[258.6666741371155,509.33334589004517,2804.4445185661316]],"n":784},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[360.0,392.0,9416.0]],"n":32}}}
```
`gen0068`, new:
```json
{"case":"gen0068","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1360,199542],[1745,274520],[1935,107845]],"n":623}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0],"geometry":{"blocks":[[7.2e+21,7.200000000000021e+21,2.628e+23]],"n":148},"record":{"blocks":[[142267.17126916934,149545.78124175625,18538871.935180865],[60965.31864733237,112625909.83332224,505787772.57405066]],"n":460},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-36.0,36.0,-666.0]],"n":36}}}
```
## Preserved evidence and remaining holds
The 41-member archive was read back and hash-verified, SHA-256
`a7a2c79290b672472bf6661707c68c3399bb1e91f75db426d44e28f12b47c049`.
It retains the exact input/result data, original nets, executable convex model,
controls, native `.smb` captures/images and all probe versions. Local path:
`~/.local/share/opencode/solidcore-cleanup-evidence/pr3056-pyramid-input-planes-20261108-134a7186f/`.
The first probe did not reconcile the runner's notification identities with
direct checks; its raw results remain retained, without attributing new failures.
The later diagnostic gives its private bodies arena identities before direct checks. The eleven disk
cases remain unapproved: their changed boundary geometry and discrepancies
between the earlier custom-key soundness probe and direct checks require
separate reconciliation. No result is blessed just because that earlier probe
called it sound.
The PR remains draft: six local-operation test failures, other invalid/ungraded
results, unresolved golden differences and canonical approval holds remain.
No #3073, #3084 or #3124 implementation is taken here.