Compare commits

...

65 Commits

Author SHA1 Message Date
jyviko f3b507c056 Known failing: the 14 cargo tests SMLib v1.1.0-alpha fails, with what each sees; two cases that pass now
Develop's CI after the a18bbe5 pin (run 41522001173) fails 14 tests that
passed on f916a8d, and passes two listed ones. Each new line says what the
test sees on v1.1.0-alpha; v1.1.0-alpha.1 fixes the setback hole's cause.
shell_tangent_chain_that_opens_every_face_explains_how_to_keep_walls and
oracle 37-pyramid-apex-blend pass and leave their lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 16:24:16 -05:00
jyviko 2bf58241fd Kernel ladder: a golden case marked unstable is gated on its status, and 06/018 is marked
On SMLib v1.1.0-alpha, 06/018 makes 597 or 594 edges from the same input:
the union's post-process keeps pointers to edges it has deleted. A golden
entry's `unstable` reason makes the gate compare that case's status only
and name it in its output; recording keeps the reason, and only an edit of
the golden removes it. v1.1.0-alpha.1 fixes the cause and removes the mark.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 15:44:41 -05:00
jyviko 62d372cc16 Kernel ladder: 07/021 and 06/018 recorded as they are on SMLib v1.1.0-alpha, each with its reason
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 15:44:41 -05:00
jyviko c29190ec35 Kernel rig: the fillet and ladder results on SMLib v1.1.0-alpha, the new failures recorded with their reasons
Fixed (off the known-failing list): the Mixed corner's peg cases and the concave arc with the front top edge (smlib #123), and the 40° boss junction (smlib #124). Eight cases in all.

New failures, recorded as they are, each with its reason. They passed on f916a8d.
- **Six cases grade a blend seam G0 where G1 (or G2) is wanted,** at the end of the round's contact seam, after smlib #124's End at a smooth vertex. Two of them also shift in volume.
- **Four cases come back unsupported** after smlib #125, which leaves setbacks to the Setback's corners only.
- **Ladder 07/012** makes 9 faces, 20 edges and 13 vertices, where its golden had 8, 18 and 12. Its golden entry is the new result, with the reason added.

Whether each change is by design or a defect is being established. A defect is fixed in solidcore, where the End and the Setback are being brought over (#2364, #2365), not in the frozen SMLib.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 15:44:41 -05:00
jyviko 4e01031bc7 smlib export tests: rustfmt's line for the rounded cube's edge midpoints
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 15:44:41 -05:00
jyviko d3902c954d SMLib: pin v1.1.0-alpha (a18bbe5): the generic blend's step-4 corners, the Ball's sphere patch through the flat arrays, all-target CI
Since f916a8d:
- #120: every target again, plus the warnings fix;
- #121: the Ball's patch as flat kind 6 with its sphere, read by the previous commit;
- #122: tests gated on counts;
- #123: the Mixed corner;
- #124: the End at a smooth vertex;
- #125: four or more rounds, and open setbacks;
- #126: the equal-area thumbweight guard;
- #127: the trunk merge.

The SMLIB_PREBUILT checksums are from the release prebuilt-a18bbe5…, written by `scripts/fetch-smlib.sh --pin`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 15:44:41 -05:00
jyviko 653c16e6f5 Native B-rep: a ball patch is its own surface kind, read from and written to SMLib's flat kind 6
SMLib's flat arrays now carry the corner of three equal rounds as `SURFACE_BALL_PATCH` (kind 6): its net as a NURBS face's block, with its sphere's centre and radius as its definition (ids 36 and 166). The record reads it as `ball_patch {center, radius}`, a label like the analytic kinds, with the net as its `nurbs`. A plain sphere with a form won't do: the net is not a piece of SMLib's sphere NURBS, so SMLib's `IsNurbSurfaceSphere` would refuse it.

Written back, a ball patch is kind 6 with the same definition, which SMLib takes once its net is certified on that sphere. Elsewhere:
- Offsets classify it as its sphere, so its offsets stay exact and collapse at the centre.
- Face kinds count it as a sphere: fillet_three_edges_at_a_corner's corner is now `sphere`, no longer `other`.
- STEP, which has no ball-patch entity, gets the net as the face's B-spline surface, pcurves included.
- Iso curves and SSI of the label are refused; the face's form is what evaluates.

Test: a_rounded_cubes_ball_patches_go_round_with_their_spheres. The cube's eight corners are ball patches about the corners pulled in by r. Points of each patch lie on its sphere to 1e-12. The patches are kind 6 again after import, with the same definitions bit for bit, and the closed-form volume holds.

Format: pending/ball-patch-faces.rs. Docs: docs/native-brep.md and the rig README's `surface_types` (`ball_patch`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 15:44:41 -05:00
jyviko 92734d7eb5 Kernel rig: the boss root's and the hole rim's curvature rounds take 600 s again: under CI's parallel load they pass the 180 s default (develop 820b7b6)
They take 37 s and 34 s alone on the box, but CI runs 8 cases at once on slower runners. Their
G2 certificate cost is the open item (tangency-labs/smlib#106, moved to solidcore#2344).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 03:37:56 -05:00
jyviko 820b7b6677 Agents: AGENTS.md loses a stray merge-conflict marker line at its end
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 02:58:29 -05:00
jyviko 597b23ba8e Sheet metal: cuts and holes pass over sheets and bends by their exact boxes, never a mesh's
A primitive cut skips the sheets kernel::apart (exact bounds grown by the record tolerance)
finds clear of its tool; the bend check leaves the clear case to kernel::boolean, which answers
it from the same exact boxes with no intersection work. The mesh-box helpers are gone, and their
four lines leave xtask/mesh-decisions.txt (172 sites left).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 02:58:29 -05:00
jyviko 608d07ecac xtask: mesh-decisions, model decisions use exact geometry; today's 176 mesh decision sites are listed and the list only shrinks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 02:58:29 -05:00
jyviko ad3ee05857 Agents: model decisions use exact geometry; a mesh is for display, export, previews and mesh bodies
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 02:58:29 -05:00
jyviko 99953687c7 Kernel rig: the boss root's and the hole rim's curvature rounds take the default timeout again (37 s and 34 s on the box under the rig's parallel load, tangency-labs/smlib#96)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 22:57:24 -05:00
jyviko 5d168a5e26 Kernel tests: SMLib's own corner between curvature rounds is curvature continuous, as the G2 patch is
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 22:57:24 -05:00
jyviko e6db7be0af Kernel tests: three rounds of different radii at a corner meet in SMLib's setback corner
Each round runs whole to its section square to its edge at its setback (the owner's rule, at
least 1.5 r, scaled to the least at which the G1 fill is certified convex), and the hole is the
setback split: one patch per round and the triangle in three quadrilaterals, 15 faces, G1.
Outside the box the sections span the body is the three strips', so its volume is theirs within
that box's. A later r3 round at the mitred corner makes the body all three at once make
(7928.142563938, 15 faces), as the engine and the model-cache tests have it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 22:57:24 -05:00
jyviko 5136898b9d Fillet: a setback too short for its corner is refused naming the least that builds (smlib_blend_fit_size)
SMLib's setback corner takes a G1 fill only once certified convex, and refuses a setback too
short for that as SMLIB_ERR_NO_ROOM with SMLIB_FIT_SETBACK_AT_LEAST, the smallest setback that
builds. The kernel reads it (solidcraft-smlib binds smlib_blend_fit_size) and names it; the
Fillet dialog says it in plain words. Other room refusals keep their words. The r5 box corner
set back 5.5 is refused naming 5.962, which builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 22:57:24 -05:00
jyviko 17de3569fd SMLib: pin f916a8d (a round on a definition face is its definition; the G1 setback fill certified convex, setbacks too short refused naming the least; the G2 certificate halves a span no level can certify at once)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 22:57:24 -05:00
jyviko aed0477033 Kernel rig: the pipe on a plate's curvature round meets the plate G2 and the pipe G3, 40307.8856312 (option B's order-2 jets, tangency-labs/solidcraft#10845)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 20:03:30 -05:00
jyviko 970446dec0 Kernel rig: a case's continuity can ask for an order per seam (seams: [{order, edges, exact}])
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 20:03:30 -05:00
jyviko c5332faf4b Kernel rig: the boss root's curvature round gets the rig's 1800 s cap: on the CI runners, 8 cases at a time, its G2 certification passed 600 s (tangency-labs/smlib#96)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:33:08 -05:00
jyviko 303111e4ed Known-failing: the action matrix's oblique cylinder cut fillet passes on SMLib 5e8c9c1; two shells still fail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:33:08 -05:00
jyviko 4a4d9c31ce Known-failing: the shell of a fully rounded box opened at one face fails on SMLib 5e8c9c1 (tangency-labs/smlib#102)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:33:08 -05:00
jyviko 68c3ccb754 Oracle: 34-chamfer-then-fillet and 35-fillet-curved-curved pass on SMLib 5e8c9c1 (their STEP edges are on their surfaces)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:33:08 -05:00
jyviko 75efb8f0cf Kernel rig: two curvature rounds get 600 s, past the 180 s default, while G2 certification is slow (tangency-labs/smlib#96)
On the CI runners the boss root and the hole rim, both size-2/size-1 curvature rounds, build
correctly but take more than 180 s: the order-2 section's span certificate and the round's
definition are evaluated station by station. The per-case limit is the one the thread cases use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko f873056481 Known failing: two kernel tests' reasons as they fail on SMLib 5e8c9c1 (a partial selection running into an unblended tangent edge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko 72109054c1 Kernel tests: the box-and-fillet wire fixture records the round's cylinder frame as SMLib now keeps it
The round's cylinder keeps the frame it was made on when it is turned (SMLib's SwapUV no longer
re-derives it): its origin sits at x = −20 on the same axis line, (x, 2, 8) along x, radius 2.
Same cylinder; one number in the record moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko 459937e524 Kernel rig: the curvature round on a pipe on a plate is listed: a seam grades G3 where exactly G2 is wanted
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko 38385ab5d3 Kernel tests: three rounds at a box corner end on one rational patch on the ball's sphere, not an analytic sphere
SMLib's Ball corner is always the rational patch exactly on the sphere (owner ruling, blend
design §23), so the corner counts as an 'other' face beside the three cylinders and six planes.
The removed volume still checks the corner against r³ − πr³/6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko ea49dff48f Ladder: 016 and 018 refused, their r3 chain takes the vertical edge; 020 and 021 recorded
With the tangent chain on, 016's and 018's r3 runs on along the r5 round's end arc and down the
vertical edge, so the third pick (60, 40, 8) is 3 from both of that round's contact lines and
names no edge: refused, asking for a point along one edge. The corners they were made for are
020 and 021 (the r3 alone), whose golden entries are 016's and 018's (021 measured on f12a101).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko d5cd5f303b Known failing: two 851 chains whose corner piece is a ball of tube-centre radius 0
851-fillet-arc-then-top-front and 851-fillet-top-front-then-upright: with the tangent chain on,
each R2 chain runs along the end curve of an R2 round on a curved face (the arc round's end on the
front face; the first round's end on the wall), where the rolling ball sits on that round's own
centre line, so the piece is the ball about (39.2538, -19, 8): the three-edge rolling-ball corner.
SMLib fails with status 1001; the same piece on a plane end arc builds (front-left upright).
Expectations unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko f2ff6f0d4f Kernel rig: a curvature chain round a round's end refused for its size; the concave half circle builds
- every-fillet-after-fillet-curvature-front-then-side-r3: the side edge's chain runs round the
  first curvature round's end curve, whose radius of curvature has a weighted mean of 3 (its
  setback) and is unbounded at the ends, so somewhere below 3: the size-3 blend's contact line
  folds back on itself. Refused, "larger than the faces beside its edges allow", as ladder
  07-fillets/016's r7 on the r5 arc. (It was solid or refused for running on tangentially.)
- every-block-cylinder-tangent-concave-arc-r2-no-tangent-chain: the chain takes every piece of
  the half circle and ends at the convex edges it runs on into; it builds the analytic 19091.9620,
  so the outcome is solid (the refusal alternative was the chain-off one's).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko ee3ed66a4f Kernel rig: two chains that now build graded by their exact volumes
With the tangent chain on, two cases round more than the edge they pick, and their old
expectations were the single edge's:
- 1207-fillet-box-end-edge-into-unblended-round: the r1 chain is the end face's top edge, the r3
  round's end arc and the back upright; 34 A + (pi/2) A (3 - c) by Pappus on the arc:
  V = 7953.139216 (it built 7953.139216, the old 7957.505 passed only inside 1e-3), 10 faces.
- 851-fillet-top-front-then-front-left-upright: the upright's chain runs up the first round's
  end arc on x = 0 and along the left top edge, so the corner becomes the R2 ball's eighth:
  V0 less the first round's spandrel to the wall (35.344727) less 27 (4 - pi) + 2 pi/3:
  V = 9530.777979 (built 9530.777987), 10 faces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko 78ed751627 Kernel rig: the concave arc between the block's tangent sides kept with tangent chain off
fillets/every-block-cylinder-tangent-concave-arc-r2-partial-chain: the pick takes one piece of the
concave half circle, which SMLib splits; with the chain off its other pieces are left out, and the
blend is refused at the piece's end (the base gate's outcome). With the chain on the case rounds
the whole half circle. The copy keeps its expectation, to be re-derived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko ce8b01fa30 Kernel rig: the partial-selection cases kept as -partial-chain copies, tangent chain off
Where a case's point is one edge of a tangent chain rounded alone, a copy keeps tangent_chain
false as its own case (SMLib rounds exactly the picked edges, ending at the smooth vertex); their
expectations stay as they were, to be re-derived:
- fillets/1207-fillet-box-end-edge-into-unblended-round-partial-chain;
- fillets/every-fillet-after-fillet-curvature-front-then-side-r3-partial-chain;
- fillets/1207-fillet-bar-top-edge-after-boss-arc-round-partial-chain, renamed from
  ...-no-tangent-chain: with the chain on it is exactly 1207-fillet-bar-top-edge-after-boss-arc-round,
  which the rig already has;
- ladder/07-fillets/020 and 021, the r5/r3/r7 and r5/r3/r2 corners (016 and 018) with the r3 on
  its edge alone: with the chain on, the r3 runs on along the r5 round's end arc and down the
  vertical edge the third round was to take.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko 21f00a9175 Kernel rig: every fillet and chamfer case rounds its tangent chain
The owner's ruling of 2026-11-01: the rig and the ladder round with tangent_chain true, how a
user's default pick behaves. The 38 fillets-area recipes and the ladder's 07-fillets/016 and 018
that passed false now pass true. The shell recipes keep theirs: a shell's tangent_chain is its
open-face chain, a different feature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko 155f1f4702 Known failing: the law corners' kernel and crate tests and rig 1207 pass on SMLib 48ea616
SMLib builds a variable round's law corners as its balls' union (the corner
ball's zone at a dip, the sides' crossing at a peak): the kernel's corner
through a middle point (39.5410) and the crate's 4 -> 2 -> 4 law (39.5421) pass.
Rig 1207 (the bar's top edge rounded after the boss's arc, r0.5) builds and
grades on the same SMLib; it failed on c820346.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 19:01:31 -05:00
jyviko cde8be4a19 Reference tests: the variable rounds 77, 78, 80 and 81 expect the union of their balls
A variable round is the envelope of the balls rolled along the edge, which takes less than a
quarter round in each section. The expectations are now that union, the law continued past the
edge's ends along its end slopes, integrated by Gauss-Legendre panels in x and t; 2, 4 and 8
panels agree to 1e-9 of the body's values, and 77's removed volume matches a brute-force union of
balls to 2.5e-10. The nearest-ball search now scans the balls the ray meets around ball x
(|x - s| < r(s) sqrt(sin 2t)) on their own, which near the faces and where 78's radius closes to 0
are narrower than the global scan's step. Regenerating keeps each case's known_failure. 79, a
constant radius, is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 19:01:31 -05:00
jyviko bf51e62f7b Kernel tests: a law's dip shows its corner ball's sphere, a peak's two rounds cross (the reasons had them swapped)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 19:01:31 -05:00
jyviko e24eb77894 SMLib: pin 5e8c9c1 (the new blend's corners: Ball and End/Join; exact fold and room checks with fit sizes; overflow in place; the round as its definition)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-01 19:01:31 -05:00
jyviko 6ba069d066 Reference tests: the variable rounds 77, 78 and 81 pass on SMLib d01d7b0; their known-failure marks come off
The new section builds their edges on the surfaces, so STEP export no longer finds them 4.6e-5
to 6.1e-5 off. develop's Reference tests (60393a718) report all three as passing while marked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:19:39 -05:00
jyviko 60393a7183 Ladder: the variable rounds graded to 1e-6 by the union of their balls; 012 by its law going on past the edge
At the default 1e-3 the great circles' volumes (4e-6 off) passed too. SMLib builds each within
1.6e-7. The golden takes the new volumes and areas; counts unchanged. The G2 corner test listed
as failing passes on SMLib's step 3 and leaves the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko e8caa1e910 Tests: three rounds of different radii at a corner listed: the corner's transition kinks with the balls' touch circles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko 1745e5c389 Kernel rig: the variable round on its cone passes (the round is the balls' envelope)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko 0b1cc8a8c2 Kernel tests: a spline radius whose balls fold over one another at its peak is refused
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko 7bba3092c6 Kernel tests: variable rounds graded by the union of their balls, the removed volume to 1e-4 of itself
removed_by_balls integrates, in each plane square to a right-angled edge, the corner the union
of the balls leaves (by rays from the edge, the least entry over the balls), cut to the body's
section. The law with a corner through a middle point is its own test, listed as failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko 01e2e939e9 Variable rounds: graded by the union of their balls (their envelope), not the balls' great circles
The smlib crate's laws and ladder 005, 006, 007 and 012 take the volumes the balls' envelope
removes; the law with a corner is its own test, listed as failing until the corner step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko cf5e0e3c10 Kernel rig: a junction family: oblique wedges 30 to 150 degrees rounded and chamfered (exact), bosses upright and tilted, pipe tees and a Y junction
Four are listed with their causes: the equal-pipe tee (its exact conic-pair rounds meet at the
ellipses' crossings in a corner the old corner code cannot join), the tilted bosses (1001, before the
blend rework too) and the Y junction (refused for room at its triple points).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko 7577e032fb Kernel rig: the turn at a corner of two rounds, Mitre (the default) and Swept, at a pocket's floor and a boss's base, by exact volumes
A round's spandrel A = (1 - pi/4) r^2 and its first moment about its wall M = r^3 (5/6 - pi/4):
Mitre adds L A -+ 2 M per edge (cut at, or run on to, the corners' bisector planes), Swept turns
the section about a convex corner's edge, (pi/2) M a corner, and refuses a concave corner rounded
less than the ball. The pocket's tangent chain round its R3 corners is a torus each. Graded to
1e-7, where Mitre and Swept differ by 6.6e-6. The Swept cases are listed until solid.fillet
takes a turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko 539d126489 Kernel rig: four G2 corners wait for the corner step: their patch meets the new section's rounds G1 at one face seam
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko 4df3adc5a7 Curvature fillets: the section's default thumbweight is equal area, so the round's volumes are the circle's
On the round's rails the order-2 section takes off what the ball's circle does: (1 - pi/4) s^2 at a
square corner, first moment 0.0474559669443 s^3 (w = -0.5252123889). The kernel and engine tests
and the curvature rig cases take their volumes from it by their own methods; the pipe on the plate
from its own wedge (70.52878 degrees, w = -0.8544103493). The G2 corner patch's seam test is listed
until the corner step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko 19455396be Kernel rig: the curvature fillets' expected bodies from the order-2 section at thumbweight 0
SMLIB_BLEND_CURVATURE's section is now the order-2 G^n section at thumbweight 0. Between two
planes square to each other it cuts A = 0.154192655384275 s^2 off the corner, its first moment
about either face 0.0291753295725360 s^3 (Green's theorem on the section): the curvature cases
built on that corner (862, 2708, every-box-edge, every-hole-top-rim, every-l-bracket,
every-block-cylinder-crease, the three-edges-at-bar-corner reports, 3604's fillet) take their
volumes from it by their own methods, the peg's front blend 23.3237146876 (its section along x
up to the wall). Between planes the section's t^3 term vanishes (their feet are linear in the
distance): the box edge case is exactly G3. The boss root is a certified fit, G3 to the plate and
G2 to the wall at Continuity Analysis's tolerances: at least G2. The boss root and the pipe on
the plate pass on SMLib's section and leave the known-failing list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko 922ee28f37 Kernel rig: the section orders, chord, circle and curvature section, on planes and against a cylinder
Eight fillet cases for the blend's cross-section by its order of contact with the faces, each
graded on its analytic volume at rel 1e-7 and its exact continuity (the order, and not the next):

- a box's top back edge chamfered 3 (G0), rounded r3 (G1) and curvature-rounded 3 (G2): the
  areas cut off the corner s^2/2, (1 - pi/4) s^2 and 113/756 s^2, the last by the default
  thumbweight's definition;
- a pipe R10 dipping into a plate, its concave edge chamfered 2 (the chord between the contacts of
  the ball SMLib rolls for that setback, radius 1), rounded r2 and curvature-rounded 2, each
  section's added area by Green's theorem along the plate, the section and the pipe's arc; the
  order-2 section is built from the faces' feet of the point at distance d from both, which on
  the pipe are not linear in d;
- a boss root curvature-rounded 2, the square corner's section revolved (Pappus);
- a variable round r1 to r5 along a box edge (rel 1e-5): the balls' envelope is a circular cone,
  and the volume is the cone's sections integrated along the edge.

Three fail today and are listed: the two curvature rounds whose faces are not a square corner of
planes (SMLib's quintic blend curve, not the order-2 section: rel 6.5e-7 and 1.2e-5) and the
variable round (great circles, not the envelope's: rel 4.8e-5). The other five pass on SMLib
7ac037e.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko 9d4f780010 SMLib: pin d01d7b0 (the new blend's one Gⁿ section generator: the chord, circle and curvature sections, and the variable rounds as the union of their balls)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:00:02 -05:00
jyviko 05f5e86011 Kernel rig: #316's bottom rounds as large as the wall, or larger, shell with a step wall at the opening, not refused
Where the round meets the open bottom the cavity's wall steps down along the bottom's normal, so the
round meets it at 90 degrees and the opening is exactly the picked face. The cases are renamed and
graded on the exact volumes, and come off the shell known-failing list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:19:29 -04:00
jyviko aa41c0b5de Kernel rig: the fillets known-failing list is the 22 cases that fail on SMLib 7ac037e
Twelve listed cases build on the new blend's end event and exact classes, or were already
built on develop: they come off the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:33:44 -04:00
jyviko 60923d8cc1 SMLib: pin 7ac037e (the new blend's tangent-end event, zero-speed G2 ends, and its exact-class rounds)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:33:44 -04:00
jyviko 73416a9bf0 Kernel rig: the exact-class rounds pass on SMLib's classified exact rounds
The rounds now come as the rational surfaces their classes give, closed ones as two faces split
at their seams, the touching pipes' as four: one splines entry a face, each of the class's degrees.
Their KNOWN_FAILING lines go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:33:44 -04:00
jyviko 19014f38a0 Kernel rig: constant rounds of the exact classes, and a tee's round
A plane cutting a cylinder or a cone at an angle, a sphere or a cone coaxial with the other face, a
cone and a cylinder touching one ball, two pipes touching at a point: their rounds' centre lines
are rational, so the rounds are rational surfaces of degrees fixed by the construction ((4, 2),
(2, 2) revolved, (6, 2), (12, 2)). The tee's centre line is not rational; its round is graded on
its volume. Volumes from the divergence theorem over the exact pieces, at rel 1e-6. The six that
SMLib 0cc48bd traces are listed in KNOWN_FAILING with what they come out as.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:33:44 -04:00
jyviko 4401ffb1f9 Rig: the test-peg's concave chamfer, alone and after the front chamfer, builds
With SMLib's tangent end as an event of the fillet's trace (blend-end-event 6d73e4f), the
concave chamfer's rail on the cylinder ends where it crosses the seam along which the front
face runs tangent into the cylinder, continued past the vertex, and its end rib on the front
face is built from there. report-peg-concave-chamfer-d2 and
report-peg-front-chamfer-d1-then-concave-chamfer-d2 pass and leave KNOWN_FAILING. The two
chamfers together still fail, now at the mixed-convexity corner where they meet: its line says
so. Rig against SMLib 0cc48bd: no area regresses (963/993 pass, 961 on a87c502), ladder 168/168,
kernel lib only its listed failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:33:44 -04:00
jyviko 6cd4003adb Rig: the curvature-continuous corner cases pick the curvature fillet
The six corner cases added in 3a854104a took the fillet's default type, on the premise that the
default would become curvature-continuous. The default stays the circular round, so each case
now asks for the curvature fillet itself (type curvature: curvature-continuous sections and
corner patches) and keeps its grading: every seam of the rounds and of the corner patch G2, one
valid body. They are renamed from cov-fillet-g2-default-* to cov-fillet-g2-*.

The oblique three-edge corner (a block drafted 15 deg) and the pocket corner pass. Four fail, as
KNOWN_FAILING now says:

- the corner beside a cylinder: the curvature fillet builds its sections only between planar
  faces;
- the four- and five-edge apex corners: refused because the default setback is shorter than
  where the rounds' contact lines meet (3.90 and 3.51 mm from the apex);
- the mixed corner: SMLib sets back a corner only where its edges are all convex or all
  concave.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:33:44 -04:00
jyviko be0b1b39bd Rig: the test-peg's front chamfer builds
With the SMLib fork's smlib-tangent-ends (a rail's end on its side edge is the rail/edge
crossing), report-peg-front-chamfer-d2 passes. Its line leaves KNOWN_FAILING, for the pin that
carries that fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:33:44 -04:00
jyviko a3165f62d2 Rig: fillet corners graded curvature-continuous, the default to be
Six fillet cases that pick their edges with no type, so they take the fillet's default, which the
owner has ruled is to be curvature-continuous (G2) at every corner, with circular rounds kept as
an option. Each grades every seam of the rounds and of the corner patch G2, on one valid body:

- an oblique convex corner (a block drafted 15 deg);
- a four-edge and a five-edge apex corner (pyramids lofted to a point);
- a pocket's concave corner;
- a mixed corner (a step: one concave edge, two convex);
- a corner beside a cylinder (the top corner of a half cylinder).

All six fail on SMLib 2fc375f, listed in KNOWN_FAILING: today's default is the circular round,
whose seams jump in curvature by 1/r. On the five-edge apex the corner patch also meets two
rounds at 8.45 degrees (G0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:33:44 -04:00
jyviko 2273379056 Rig: blends that end where their face runs tangent into another
Fifteen fillet cases on the test-peg's shape with clean numbers: a bar 40 x 16 x 10 whose far
end is a half disc R8, a disc R8 standing 10 on its top, joined, so the front face runs tangent
into the cylinder along x = 40 and the front top edge (convex) meets the concave arc of the top
tangentially there. Each blend ends on a face tangent to the one it rolls on.

- Rounds: both edges together at r2 and r5; one after the other in either order at r2, and at
  r1.5 and r3; the front edge alone as a curvature round, whose end on the cylinder is a
  semicubical cusp. The containment invariant (the unblended body's exact box, to 4e-5) and
  G1 seams, or the analytic volume where the blend has a closed section.
- Chamfers: the front edge alone and the concave arc alone (analytic volumes: the section along
  x up to the wall, the cone's spandrel ring clipped by the sides), both together, and one after
  the other (1 then 2).
- STEP: the peg written to STEP and read back, its front edge rounded (analytic) and both edges
  rounded together.
- A cone: the peg drafted 5 deg, whose front face runs tangent into a cone along a generator,
  its front edge alone and both edges together.

The quadratures are tanh-sinh, converged to 1e-13; SMLib's own volumes of the passing analytic
cases agree to 4e-9. A sphere is left out: a plane meets a sphere tangentially at a point, not
along a seam, so it is not this ending.

On SMLib 2fc375f eight fail, listed in KNOWN_FAILING: every chamfer that ends tangent (status
1001, SmFilletNx1Corner), the two rounds together (the corner patch 0.0374 at r2 and 0.128 at r5
out of the front face, and the rounds meeting with normals 180 degrees apart where their
contact lines on the top cross), the same on the STEP-read peg (status 1001) and on the drafted
peg (its corner refused as unsupported).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:33:44 -04:00
jyviko 2c75cf424f Rig: the test-peg corner at r5 and r7
Two more fillet cases, the reported part's concave arc and front top edge rounded together at
r5 and r7, with the same exact-box containment check (graded to 4e-5, the corner patch's
tolerance). On SMLib 2fc375f the corner patch stands 0.128 (r5) and 0.230 (r7) out of the
bar's front face (box y min -0.0699 and -0.1723): listed in KNOWN_FAILING.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:33:44 -04:00
jyviko 3c16884f7c Rig: the test-peg's concave arc and front top edge rounded together stay within the body's box
A new fillet case, report-fillet-concave-arc-and-front-top-edge-together-solver-residues:
the reported part's bar (with the sketch solver's residues) whose sides run tangent into the
cylinder, its concave arc and front top edge rounded r2 together. The plane of the front face
bounds the whole body, so every blend lies within the unblended body's exact box (SMLib's tight
box); graded to 4e-5, the corner patch's own tolerance. On SMLib 2fc375f the corner patch
stands 0.0374 out of the front face (box y min 0.0205): listed in KNOWN_FAILING.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:33:44 -04:00
278 changed files with 9950 additions and 487 deletions
+4
View File
@@ -318,6 +318,10 @@ jobs:
cargo test -p xtask ui_containers --locked
- name: Sorted lists
run: cargo xtask lists
- name: Model decisions use exact geometry, not meshes
run: |
cargo xtask mesh-decisions
cargo test -p xtask mesh_decisions --locked
- name: Agent router and skills cite real files
run: |
cargo xtask skills
+1 -1
View File
@@ -10,6 +10,7 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
- No GPL/LGPL/AGPL code, not even reading it (OpenCASCADE, FreeCAD, SolveSpace, CadQuery, LibreCAD, QCAD): `cargo xtask licences --check`.
- Every asset is original or openly licensed with a row in ATTRIBUTION.md; read `assets/AGENTS.md` before icon work: `cargo xtask assets`, `cargo xtask icons`.
- Never crash: no unwrap/expect/panic/unsafe in non-test code (unsafe allowlist in crash-safety), errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
- Model decisions (what an operation does, which bodies it touches, which face or edge a point or name means, depths, nearness) use exact geometry; a mesh is for display, export, previews and mesh bodies (`xtask/mesh-decisions.txt` only shrinks): `cargo xtask mesh-decisions`.
- Everything is a command and programmatic calls never open dialogs: `skills/solidcraft-commands/SKILL.md`.
- Layering (geom, sketch/kernel/render, doc, io, engine, ui-egui, apps); only `crates/kernel` uses truck: `cargo xtask layers`.
- One outer UI container, one camera path (`navigation::handle`), colours from theme tokens: `cargo xtask ui-containers`.
@@ -45,4 +46,3 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
| A bug report, a crash, a wrong result | `skills/solidcraft-reporting/SKILL.md` |
| Checking, porting, designs in git | `skills/solidcraft-checking/SKILL.md` |
| Assemblies, joints, motion | `skills/solidcraft-mechanisms/SKILL.md` |
>>>>>>> 4a3e0772a (Agents: AGENTS.md becomes a router to focused skills, with a staleness check (Refs #6439))
+59 -13
View File
@@ -198,6 +198,28 @@ fn continuity_seams(b: &solidcraft_engine::doc::ModelBody, edges: &Value) -> Res
}
}
/// The orders a case's `continuity` asks for: `{order, edges, exact}` for its `edges` seams, or
/// `seams: [{order, edges, exact}, ...]`, each read the same way, for seams that reach different
/// orders (`edges` points picking each). Nothing where it asks for no order (the curvature-jump
/// form, or none); an `order` that is not g0…g4 is an error.
fn continuity_wants(cont: &Value) -> Vec<Result<(&Value, solidcraft_engine::kernel::Continuity, bool), String>> {
fn one(w: &Value) -> Option<Result<(&Value, solidcraft_engine::kernel::Continuity, bool), String>> {
let order = w.get("order").filter(|o| !o.is_null())?;
let exact = w["exact"].as_bool().unwrap_or(false);
Some(
order
.as_str()
.and_then(solidcraft_engine::kernel::Continuity::parse)
.map(|o| (&w["edges"], o, exact))
.ok_or_else(|| format!("continuity `order` {order} is not g0…g4")),
)
}
match cont.get("seams").and_then(Value::as_array) {
Some(seams) => seams.iter().map(|w| one(w).unwrap_or_else(|| Err("a continuity seam with no `order`".to_owned()))).collect(),
None => one(cont).into_iter().collect(),
}
}
/// The `continuity: {order, edges}` check of `b`: its `edges` seams measured as Continuity
/// Analysis (`inspect.continuity`) measures them, at its default stations and tolerances, and the
/// lowest order among them with the seam that has it (the worst: of those, the one whose first
@@ -456,7 +478,8 @@ fn output_checks(c: &mut Checks, want: &Value, out: &str, results: &[Value], rel
}
/// The word a `surface_types` check names a face's surface by, as the record holds it: an
/// analytic kind, `bspline` (a B-spline or NURBS surface) or `revolution` (a revolved curve).
/// analytic kind, `ball_patch` (a corner's patch on a sphere), `bspline` (a B-spline or NURBS
/// surface) or `revolution` (a revolved curve).
fn surface_word(s: &solidcraft_engine::geom::brep::Surface) -> &'static str {
use solidcraft_engine::geom::brep::Surface as S;
match s {
@@ -464,6 +487,7 @@ fn surface_word(s: &solidcraft_engine::geom::brep::Surface) -> &'static str {
S::Cylinder { .. } => "cylinder",
S::Cone { .. } => "cone",
S::Sphere { .. } => "sphere",
S::BallPatch { .. } => "ball_patch",
S::Torus { .. } => "torus",
S::Spline { .. } | S::RationalSpline { .. } => "bspline",
S::PlacedRevolution { .. } => "revolution",
@@ -794,22 +818,22 @@ fn check_case(dir: &str, dumps: Option<&std::path::Path>, validation: solidcraft
);
}
// The order every blend seam reaches, as Continuity Analysis measures it.
if let Some(order) = want["continuity"].get("order").filter(|o| !o.is_null()) {
let edges = &want["continuity"]["edges"];
let exact = want["continuity"]["exact"].as_bool().unwrap_or(false);
let which = if edges.is_array() { "picked" } else { edges.as_str().unwrap_or("blend") };
let need = format!("{} {} at every {which} seam", if exact { "exactly" } else { ">=" }, order.as_str().unwrap_or("?"));
match order.as_str().and_then(solidcraft_engine::kernel::Continuity::parse) {
Some(o) => match continuity_order(b, o, edges, exact) {
Ok((got, ok)) => c.push("continuity_order", got, json!(need), ok),
Err(e) => c.push("continuity_order", json!(format!("{}: {e}", b.name)), json!(need), false),
},
None => c.push("continuity_order", json!(format!("continuity `order` {order} is not g0…g4")), json!("an order"), false),
for wanted in continuity_wants(&want["continuity"]) {
match wanted {
Ok((edges, o, exact)) => {
let which = if edges.is_array() { "picked" } else { edges.as_str().unwrap_or("blend") };
let need = format!("{} {} at every {which} seam", if exact { "exactly" } else { ">=" }, o.as_str());
match continuity_order(b, o, edges, exact) {
Ok((got, ok)) => c.push("continuity_order", got, json!(need), ok),
Err(e) => c.push("continuity_order", json!(format!("{}: {e}", b.name)), json!(need), false),
}
}
Err(e) => c.push("continuity_order", json!(e), json!("an order"), false),
}
}
// Curvature continuity across the seams round a point: the worst jump clear of the
// seams' turns must be within the case's bound.
if let Some(cont) = want.get("continuity").filter(|c| !c.is_null() && c.get("order").is_none()) {
if let Some(cont) = want.get("continuity").filter(|c| !c.is_null() && c.get("order").is_none() && c.get("seams").is_none()) {
let (near, within, clear, bound) =
(v3(&cont["near"]), cont["within_mm"].as_f64(), cont["clear_mm"].as_f64(), cont["curvature_jump_per_mm"].as_f64());
match (near, within, clear, bound) {
@@ -1196,6 +1220,28 @@ mod tests {
let _ = std::fs::remove_dir_all(&root);
}
/// `continuity` asks for one order at every seam it picks, or per seam (`seams`), each entry
/// read as the one-order form; the curvature-jump form asks for none.
#[test]
fn continuity_asks_for_one_order_or_one_per_seam() {
use solidcraft_engine::kernel::Continuity;
let read = |c: Value| {
continuity_wants(&c).into_iter().map(|w| w.map(|(e, o, x)| (e.clone(), o, x))).collect::<Vec<Result<(Value, Continuity, bool), String>>>()
};
let g2 = Continuity::parse("g2").unwrap();
let g3 = Continuity::parse("g3").unwrap();
assert_eq!(read(json!({"order": "g2", "exact": true})), vec![Ok((Value::Null, g2, true))]);
let (p, q) = (json!([[30.0, 31.3, 10.0]]), json!([[30.0, 29.4, 12.7]]));
assert_eq!(
read(json!({"seams": [{"order": "g2", "exact": true, "edges": p}, {"order": "g3", "edges": q}]})),
vec![Ok((p.clone(), g2, true)), Ok((q, g3, false))]
);
assert!(read(json!({"near": [0.0, 0.0, 0.0], "within_mm": 1.0})).is_empty());
assert!(read(Value::Null).is_empty());
assert!(read(json!({"order": "g9"}))[0].is_err());
assert!(read(json!({"seams": [{"edges": p}]}))[0].is_err(), "a seam must say its order");
}
/// The flat pattern is graded on the last `sheet.flat_pattern` result: its size, its outline's
/// area less the cut-outs', and its bend lines matched either way round, one for one.
#[test]
+4 -3
View File
@@ -317,7 +317,8 @@ fn a_model_saved_under_another_check_mode_is_evaluated_again() {
/// later round at the corner where they meet blends them again with it whether the design was
/// opened from its model or evaluated anew. Two edges of a 20 mm box rounded r2 (a mitre at the
/// corner (20, 0, 20)), saved; opened, the upright edge there rounded r3: the corner all three
/// rounded at once make, 10 faces and 7928.743041 mm³, the same body as the design evaluated anew.
/// rounded at once make (SMLib's setback corner, kernel `three_rounds_at_a_corner`), 15 faces and
/// 7928.142563938 mm³, the same body as the design evaluated anew.
#[test]
fn a_later_round_after_opening_blends_the_saved_rounds_again() {
let mut built = Session::default();
@@ -334,8 +335,8 @@ fn a_later_round_after_opening_blends_the_saved_rounds_again() {
let st = s.model.state();
let b = &st.body("Box").unwrap().body;
let v = solidcraft_engine::kernel::measure(b).unwrap().volume;
assert_eq!(b.face_count(), 10);
assert!((v - 7928.743041).abs() < 1e-6, "{v}");
assert_eq!(b.face_count(), 15);
assert!((v - 7928.142563938).abs() < 1e-6, "{v}");
}
assert_eq!(model("later round", &cached), model("later round", &rebuilt));
let _ = std::fs::remove_file(&path);
@@ -0,0 +1,23 @@
{
"en": {
"ui.fillet_error_setback_short": "The setback is too short for this corner's patch: it builds from {least} on. Try a longer Setback."
},
"ar": {
"ui.fillet_error_setback_short": "مسافة الإرجاع قصيرة جدًا على رقعة هذه الزاوية: تُبنى ابتداءً من {least}. جرّب مسافة إرجاع أطول."
},
"de": {
"ui.fillet_error_setback_short": "Der Rücksprung ist für das Eckstück zu kurz: Es lässt sich ab {least} bauen. Versuchen Sie einen längeren Rücksprung."
},
"he": {
"ui.fillet_error_setback_short": "הנסיגה קצרה מדי עבור הטלאי של פינה זו: הוא נבנה החל מ-{least}. נסו נסיגה ארוכה יותר."
},
"ja": {
"ui.fillet_error_setback_short": "このコーナーのパッチにはセットバックが短すぎます。{least} 以上で作れます。より長いセットバックを試してください。"
},
"ko": {
"ui.fillet_error_setback_short": "이 코너의 패치에 비해 셋백이 너무 짧습니다. {least}부터 만들 수 있습니다. 더 긴 셋백을 시도해 보세요."
},
"zh": {
"ui.fillet_error_setback_short": "对于此角点的面片,后退距离太短:从 {least} 起可以构建。请尝试更长的后退距离。"
}
}
+15 -41
View File
@@ -5,7 +5,7 @@ use super::*;
use crate::EmbossFace;
use crate::apply_point;
use crate::sheet;
use solidcraft_geom::{Aabb3, Loop2, Ring, Seg2};
use solidcraft_geom::{Loop2, Ring, Seg2};
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
match &f.kind {
@@ -1025,15 +1025,17 @@ pub(super) fn sheet_primitive_cut(
return Ok(Vec::new());
}
// The first sheet the primitive meets decides the machining direction. Sheets clear of it
// by their boxes are passed over; the one sheet left needs no boolean to show it is met (that
// boolean came out empty against a sheet carrying a label's cut-outs, and the hole was then
// cut into the solid without a place in the flat pattern).
let tool_box = body_box(tool);
let near: Vec<&sheet::SheetBody> = st
.sheets
.iter()
.filter(|sh| sheet_eligible(f, st, &[], sh) && st.body(&sh.body).is_some_and(|mb| !boxes_apart(&mesh_box(mb), &tool_box)))
.collect();
// by their exact boxes ([`kernel::apart`]) are passed over; the one sheet left needs no
// boolean to show it is met (that boolean came out empty against a sheet carrying a label's
// cut-outs, and the hole was then cut into the solid without a place in the flat pattern).
let mut near: Vec<&sheet::SheetBody> = Vec::new();
for sh in st.sheets.iter().filter(|sh| sheet_eligible(f, st, &[], sh)) {
if let Some(mb) = st.body(&sh.body)
&& !kernel::apart(&mb.body, tool)?
{
near.push(sh);
}
}
let mut normal = None;
for sh in &near {
if let Some(mb) = st.body(&sh.body)
@@ -1344,9 +1346,6 @@ fn sheet_prism_cut(
} else {
Vec::new()
};
// Boxes around the tools: a tool clear of a bend by its box needs no boolean to show it (a
// label's letters against every bend and flange of a tray).
let tool_boxes: Vec<Option<Aabb3>> = tools.iter().map(body_box).collect();
let mut plans = Vec::new();
let mut notes = Vec::new();
'sheets: for original in &st.sheets {
@@ -1366,12 +1365,10 @@ fn sheet_prism_cut(
// is exact, and a cut that machines nothing there is checked against the solid after it.
// (That boolean cost each label hundreds of milliseconds, and against a sheet already
// carrying a label's cut-outs it came out empty: the next cut was dropped.)
// A tool clear of a bend by its exact box (a label's letters against every bend and
// flange of a tray) costs no intersection work: the boolean answers it from the boxes.
for bends in sh.unmodeled_solids(plane)? {
let bends_box = body_box(&bends);
for (tool, tool_box) in tools.iter().zip(&tool_boxes) {
if boxes_apart(&bends_box, tool_box) {
continue;
}
for tool in tools {
if kernel::boolean(&bends, tool, kernel::BoolOp::Intersect)?.is_some() {
return Err(DocError::Invalid(
"this machining intersects a folded bend or a nonparallel panel; unfold the sheet before machining that material".into(),
@@ -1556,29 +1553,6 @@ fn sheet_prism_cut(
Ok(handled)
}
/// A box around `body`, grown past its mesh's deflection from the exact faces (and a hair
/// more): bodies whose boxes are apart do not meet. None when it can't be worked out.
fn body_box(body: &Body) -> Option<Aabb3> {
let tol = (body.size() * 1e-3).max(1e-4);
padded_box(&body.mesh(&solidcraft_kernel::MeshTolerance::chord(tol)).ok()?, tol)
}
/// [`body_box`] of a model body, from the mesh it keeps.
fn mesh_box(mb: &ModelBody) -> Option<Aabb3> {
padded_box(&mb.mesh(), mb.display_tol())
}
fn padded_box(mesh: &solidcraft_geom::Mesh, tol: f64) -> Option<Aabb3> {
let b = mesh.bounds();
(!b.is_empty() && b.min.is_finite() && b.max.is_finite()).then(|| b.inflated(2.0 * tol + 1e-6))
}
/// Whether two boxes are known to be apart (an unknown box is never apart).
fn boxes_apart(a: &Option<Aabb3>, b: &Option<Aabb3>) -> bool {
let (Some(a), Some(b)) = (a, b) else { return false };
a.max.x < b.min.x || b.max.x < a.min.x || a.max.y < b.min.y || b.max.y < a.min.y || a.max.z < b.min.z || b.max.z < a.min.z
}
/// A cut's note on the pieces it parted from the base of `sh` (see
/// [`sheet::SheetBody::parted_from_base`]; `material` is the sheet's flat material after the cut,
/// `before` the sheet before it): how many, their area and where they lie in the flat pattern.
@@ -0,0 +1,4 @@
//! A face may save `surface` as a `ball_patch` {center, radius}: the corner of three equal rounds
//! as SMLib holds it, a rational patch on that sphere, always with its `nurbs`, the patch itself.
//! An older design has these faces as plain NURBS faces and keeps them so. The last release
//! refuses a design with this change (the newer format number).
+3 -1
View File
@@ -89,7 +89,9 @@ fn redefining_a_fillet_as_g2_on_sloped_ends_survives_undo_and_reopen() {
assert_eq!(bodies.len(), 1);
bodies.first().unwrap().body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.0001)).unwrap().measure().volume
};
let expected = |r: f64| 12000.0 - 40.0 * 113.0 / 756.0 * r.powi(2) + 13711.0 / 486486.0 * r.powi(3);
// The curvature section at its equal-area thumbweight removes (1 - pi/4) r² at the square
// corner, first moment 0.0474559669443 r³ (Green's theorem on the section).
let expected = |r: f64| 12000.0 - 40.0 * (1.0 - std::f64::consts::FRAC_PI_4) * r.powi(2) + 0.0474559669443 * r.powi(3);
let actual = volume(&s);
assert!((actual - expected(5.0)).abs() < 0.05, "{actual} vs {}", expected(5.0));
let edited = s.doc.clone();
+48
View File
@@ -5770,3 +5770,51 @@ fn an_unfolded_sheets_flat_face_is_named_after_its_base_at_every_size() {
}
}
}
/// A primitive cut passes over a sheet its exact box is clear of: that sheet comes back the very
/// same body, and the sheet the cut meets gets the hole.
#[test]
fn a_primitive_cut_passes_over_a_sheet_clear_of_it_by_its_exact_box() {
let mut s = Session::default();
for (name, x) in [("Left", 0.0), ("Right", 100.2)] {
run(&mut s, "solid.box", json!({"corner": [x, 0, 0], "length": 100, "width": 60, "height": 2.5, "body_name": name}));
run(&mut s, "sheet.convert", json!({"body": name, "face": [x + 50.0, 30, 2.5]}));
}
let body = |s: &Session, name: &str| s.model.state().bodies.iter().find(|b| b.name == name).map(|b| b.body.clone()).unwrap();
let left = body(&s, "Left");
let right_volume = body_volume(&mut s, "Right");
// 0.2 mm from the Left sheet's end: within the display mesh's tolerance of it, clear of its exact box.
run(&mut s, "solid.cylinder", json!({"base": [102.9, 30, -1], "radius": 2.5, "height": 5, "operation": "cut"}));
assert!(body(&s, "Left").same(&left), "the sheet clear of the cut is passed over");
let holes = |s: &Session, name: &str| s.model.state().sheets.iter().find(|sh| sh.body == name).map(|sh| sh.holes.len());
assert_eq!(holes(&s, "Left"), Some(0));
assert_eq!(holes(&s, "Right"), Some(1));
assert!((right_volume - body_volume(&mut s, "Right") - PI * 6.25 * 2.5).abs() < 1e-3);
}
/// A cut that only grazes a bend's curved faces near the edge where the bend meets the base is
/// checked against the bend and refused (the sheet is unchanged); one clear of the bend's exact
/// box cuts the base.
#[test]
fn a_cut_grazing_a_bend_near_its_edge_is_refused_and_one_clear_of_its_exact_box_cuts() {
// `plate_with_flange`: the bend (R 2.5, T 2.5) runs from y = 0 to y = 5, where the base
// panel starts.
for (y0, grazes) in [(4.9, true), (5.1, false)] {
let mut s = Session::default();
plate_with_flange(&mut s);
let v0 = volume(&mut s);
rectangle_tool(&mut s, "Slot", [40.0, y0], [60.0, 15.0]);
let cut = json!({"sketch": "Slot", "through_all": true, "direction": "symmetric", "operation": "cut"});
if grazes {
let before = s.doc.to_json();
let e = s.execute("solid.extrude", &cut).unwrap_err().to_string();
assert!(e.contains("intersects a folded bend"), "{e}");
assert_eq!(s.doc.to_json(), before);
assert_eq!(volume(&mut s), v0);
} else {
run(&mut s, "solid.extrude", cut);
assert!((v0 - volume(&mut s) - 20.0 * (15.0 - y0) * 2.5).abs() < 1e-3, "{}", v0 - volume(&mut s));
assert_eq!(flat(&mut s)["cutouts"].as_array().map(Vec::len), Some(1));
}
}
}
+9 -8
View File
@@ -91,10 +91,10 @@ fn fillet_as_wide_as_a_face_rounds_it_off() {
/// The owner's ruling of 2026-10-30: two edges of a 20 mm box rounded r2 in one fillet (they
/// meet in a mitre at the corner (20, 0, 20), the upright edge there sharp), then the upright edge
/// r3 in a second fillet, whose tangent chain takes the mitre's seam with it. The corner is one
/// smooth patch, the body rounding all three at once makes (kernel `fillet_radii` of the box with
/// each edge's radius: 10 faces, 7928.743041 mm³); the r2 rounds keep their names, and the r3
/// round and the corner patch are the second fillet's.
/// r3 in a second fillet, whose tangent chain takes the mitre's seam with it. The corner is the
/// body rounding all three at once makes (kernel `fillet_radii` of the box with each edge's
/// radius, `three_rounds_at_a_corner`: SMLib's setback corner, 15 faces, 7928.142563938 mm³); the
/// r2 rounds keep their names, and the r3 round and the corner's patches are the second fillet's.
#[test]
fn a_later_round_at_a_mitred_corner_is_blended_with_the_rounds_there() {
let mut s = Session::default();
@@ -116,16 +116,17 @@ fn a_later_round_at_a_mitred_corner_is_blended_with_the_rounds_there() {
let second = run(&mut s, "solid.fillet", json!({"edges": [[20, 0, 10]], "radius": 3}))["feature"].as_u64().unwrap();
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{:?}", s.model.results);
let v = volume(&mut s);
assert!((v - 7928.743041).abs() < 1e-5, "{v}");
assert!((v - 7928.142563938).abs() < 1e-6, "{v}");
let faces = run(&mut s, "model.faces", json!({"body": "Box"}))["faces"].as_array().cloned().unwrap_or_default();
assert_eq!(faces.len(), 10);
assert_eq!(faces.len(), 15);
let after: Vec<String> = rounds.iter().map(|p| named(&mut s, *p)).collect();
assert_eq!(after, before);
// The r3 round (halfway up, at 45°) and the corner patch: the two faces the second fillet made.
// The r3 round (halfway up, at 45°) and the corner's patches: the faces the second fillet made.
let k3 = 3.0 - 4.5_f64.sqrt();
assert!(named(&mut s, solidcraft_geom::Vec3::new(20.0 - k3, k3, 10.0)).starts_with(&format!("F{second}:blend:")));
let made = faces.iter().filter(|f| f["name"].as_str().is_some_and(|n| n.starts_with(&format!("F{second}:")))).count();
assert_eq!(made, 2, "{faces:?}");
// the r3 round and the corner's six patches (a setback patch per round, three in the triangle)
assert_eq!(made, 7, "{faces:?}");
}
#[test]
+21 -7
View File
@@ -94,10 +94,10 @@ pub struct Face {
pub surface: Surface,
/// The NURBS form the face is parameterised on, when it differs from `surface`'s own
/// parameters: a cylinder, cone, sphere, torus or placed revolution as SMLib holds it (every
/// SMLib analytic surface is a B-spline surface, its trims in that NURBS's parameters). When
/// given, `domain` and every pcurve are in this surface's (u, v), and `surface` only says what
/// it is; it is a `Spline` or `RationalSpline` and `surface` is a cylinder, cone, sphere, torus
/// or placed revolution
/// SMLib analytic surface is a B-spline surface, its trims in that NURBS's parameters), and a
/// ball patch's own net. When given, `domain` and every pcurve are in this surface's (u, v),
/// and `surface` only says what it is; it is a `Spline` or `RationalSpline` and `surface` is a
/// cylinder, cone, sphere, ball patch, torus or placed revolution
/// (docs/native-brep.md, "Surfaces").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nurbs: Option<Surface>,
@@ -247,6 +247,10 @@ pub enum Surface {
#[serde(default, skip_serializing_if = "is_zero")]
tube_start: f64,
},
/// A rational patch on the sphere of `center` and `radius`: the corner of three equal rounds as
/// SMLib holds it (`SmBallPatch`). It has no (u, v) of its own: the face's `nurbs` is the patch,
/// and this says it lies on that sphere, whose offsets are exact and collapse at its centre.
BallPatch { center: [f64; 3], radius: f64 },
/// A tensor-product B-spline: `points[i][j]`, i along u (STEP `B_SPLINE_SURFACE_WITH_KNOTS`).
Spline { u_knots: Vec<f64>, v_knots: Vec<f64>, points: Vec<Vec<[f64; 3]>> },
/// A NURBS surface: homogeneous `[x*w, y*w, z*w, w]` coefficients.
@@ -378,6 +382,8 @@ fn surface_natural(s: &Surface) -> [Natural; 2] {
}
Surface::Sphere { .. } => [Natural::Periodic, Natural::Range(-FRAC_PI_2, FRAC_PI_2)],
Surface::Torus { .. } => [Natural::Periodic; 2],
// A label: the face's (u, v) are its NURBS form's.
Surface::BallPatch { .. } => [UNBOUNDED; 2],
Surface::Spline { u_knots, v_knots, points } => {
[knot_domain(u_knots, points.len()), knot_domain(v_knots, points.first().map_or(0, Vec::len))]
}
@@ -496,6 +502,10 @@ fn surface(s: &Surface, depth: usize, budget: &mut Budget) -> Result<()> {
require(tube_start.is_finite() && tube_start.abs() <= std::f64::consts::TAU, "torus tube start")?;
require(major_radius > minor_radius, "ring torus radii")
}
Surface::BallPatch { center, radius: r } => {
points(&[*center], budget)?;
radius(*r)
}
Surface::Spline { u_knots, v_knots, points: p } => grid(u_knots, v_knots, p, budget),
Surface::RationalSpline { u_knots, v_knots, homogeneous: p } => {
grid(u_knots, v_knots, p, budget)?;
@@ -578,6 +588,7 @@ impl Face {
let outward = match &self.surface {
Surface::Cylinder { frame, .. } | Surface::Cone { frame, .. } => radial(frame).1,
Surface::Sphere { frame, .. } => p - v3(frame.origin),
Surface::BallPatch { center, .. } => p - v3(*center),
Surface::Torus { frame, major_radius, .. } => {
let (o, r) = radial(frame);
p - (o + r.normalized()? * *major_radius)
@@ -651,13 +662,15 @@ impl Brep {
Surface::Cylinder { .. }
| Surface::Cone { .. }
| Surface::Sphere { .. }
| Surface::BallPatch { .. }
| Surface::Torus { .. }
| Surface::PlacedRevolution { .. }
),
"NURBS form on a surface that is not a cylinder, cone, sphere, torus or revolution",
"NURBS form on a surface that is not a cylinder, cone, sphere, ball patch, torus or revolution",
)?;
}
// A cylinder, cone, sphere, torus or revolution is a label: its geometry is its form.
// A cylinder, cone, sphere, ball patch, torus or revolution is a label: its geometry is
// its form.
require(
f.nurbs.is_some()
|| !matches!(
@@ -665,10 +678,11 @@ impl Brep {
Surface::Cylinder { .. }
| Surface::Cone { .. }
| Surface::Sphere { .. }
| Surface::BallPatch { .. }
| Surface::Torus { .. }
| Surface::PlacedRevolution { .. }
),
"a cylinder, cone, sphere, torus or revolution without its NURBS form",
"a cylinder, cone, sphere, ball patch, torus or revolution without its NURBS form",
)?;
let [u, v] = surface_natural(f.parameters());
interval(&f.domain[0], u)?;
+2
View File
@@ -109,6 +109,8 @@ impl SurfaceEval {
Surface::Torus { frame, major_radius, minor_radius, tube_start } => {
Ok(SurfaceEval::Torus { axes: Axes::new(frame), major: *major_radius, minor: *minor_radius, tube: *tube_start })
}
// A label with no (u, v) of its own: its face evaluates its NURBS form.
Surface::BallPatch { .. } => Err(unsupported("a ball patch's label (evaluate its NURBS form)")),
Surface::Spline { u_knots, v_knots, points } => {
let rows: Vec<Vec<[f64; 4]>> = points.iter().map(|r| r.iter().map(|p| [p[0], p[1], p[2], 1.0]).collect()).collect();
eval(nurbs(u_knots, v_knots, &rows)?)
+1
View File
@@ -96,6 +96,7 @@ impl Rigid {
Surface::Cone { frame: self.frame(frame), radius_at_origin: *radius_at_origin, half_angle: *half_angle }
}
Surface::Sphere { frame, radius } => Surface::Sphere { frame: self.frame(frame), radius: *radius },
Surface::BallPatch { center, radius } => Surface::BallPatch { center: self.point(*center), radius: *radius },
Surface::Torus { frame, major_radius, minor_radius, tube_start } => {
Surface::Torus { frame: self.frame(frame), major_radius: *major_radius, minor_radius: *minor_radius, tube_start: *tube_start }
}
+18 -13
View File
@@ -224,6 +224,10 @@ pub fn fillet_removing(body: &Body, edges: &[Vec3], radius: f64) -> Result<(Body
Ok((closed(body, b)?, gone))
}
/// What a refusal of a setback too short for its corner says before the least that builds (in
/// mm): SMLib certifies the corner's fill convex only from that setback on ([`fillet_setback`]).
pub const SETBACK_AT_LEAST: &str = "the smallest setback that builds is ";
/// [`fillet`] with corner setbacks: each `(at, distance)` sets back the corner at the vertex
/// nearest `at`, a vertex where every edge is rounded, so its patch starts `distance` along
/// each of its edges from the vertex (one distance for all of a corner's edges: SMLib keeps one
@@ -1173,15 +1177,15 @@ mod tests {
}
#[test]
fn g2_fillet_has_the_quintics_area() {
fn g2_fillet_takes_off_what_the_round_does() {
let body = crate::build::box_solid(Vec3::ZERO, Vec3::new(20.0, 20.0, 20.0)).unwrap();
let out = fillet_g2(&body, &[Vec3::new(20.0, 0.0, 10.0)], 2.0).unwrap();
assert!(out.validity().is_empty(), "{:?}", out.validity());
assert_eq!(out.face_count(), body.face_count() + 1);
// Independent analytic area: integrating x(t) y'(t) for the quintic
// gives 113/756 times setback² for a right-angle corner.
// The curvature section's default thumbweight is equal area: on the round's rails it takes
// off what the round does, (1 - pi/4) setback² at a right-angle corner.
let volume = out.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
let expected = 8000.0 - (113.0 / 756.0) * 4.0 * 20.0;
let expected = 8000.0 - (1.0 - std::f64::consts::FRAC_PI_4) * 4.0 * 20.0;
assert!((volume - expected).abs() < 0.02, "{volume} vs {expected}");
}
@@ -1199,7 +1203,7 @@ mod tests {
let out = fillet_g2(&body, &[Vec3::new(10.0, 10.0, 10.0)], 2.0).unwrap();
assert!(out.validity().is_empty(), "{:?}", out.validity());
let volume = out.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
let expected = 6000.0 + (113.0 / 756.0) * 4.0 * 20.0;
let expected = 6000.0 + (1.0 - std::f64::consts::FRAC_PI_4) * 4.0 * 20.0;
assert!((volume - expected).abs() < 0.02, "{volume} vs {expected}");
let body = crate::build::box_solid(Vec3::ZERO, Vec3::new(20.0, 20.0, 20.0)).unwrap();
let out = fillet_g2(&body, &[Vec3::new(20.0, 0.0, 10.0), Vec3::new(0.0, 20.0, 10.0)], 2.0).unwrap();
@@ -1241,9 +1245,10 @@ mod tests {
assert!(out.validity().is_empty(), "{:?}", out.validity());
assert_eq!(out.crossed_face_loops().unwrap(), 0);
assert_eq!(out.face_count(), 7);
// Analytic volume: the removed section has area 113 r² / 756
// and first moment ∫y dA = 13711 r³ / 486486. Axial length is 40−(left+right)y.
let removed = 40.0 * 113.0 / 756.0 * 25.0 - (left + right) * 13711.0 / 486486.0 * 125.0;
// Analytic volume: the equal-area section removes (1 - pi/4) r², and its first
// moment ∫y dA is 0.0474559669443 r³ (Green's theorem on the section at the
// square corner's equal-area thumbweight). Axial length is 40−(left+right)y.
let removed = 40.0 * (1.0 - std::f64::consts::FRAC_PI_4) * 25.0 - (left + right) * 0.0474559669443 * 125.0;
let expected = 16000.0 - 4000.0 * (left + right) - removed;
let volume = out.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
assert!((volume - expected).abs() < 0.05, "{volume} vs {expected}");
@@ -1272,7 +1277,7 @@ mod tests {
assert!(out.validity().is_empty(), "{:?}", out.validity());
assert_eq!(out.crossed_face_loops().unwrap(), 0);
let volume = out.mesh(&crate::MeshTolerance::chord(0.0001)).unwrap().measure().volume;
let expected = 6000.0 + 113.0 / 756.0 * 4.0 * 20.0;
let expected = 6000.0 + (1.0 - std::f64::consts::FRAC_PI_4) * 4.0 * 20.0;
assert!((volume - expected).abs() < 0.02, "{volume} vs {expected}");
}
@@ -1287,12 +1292,12 @@ mod tests {
assert!(fillet_g2(&body, &[Vec3::new(f64::NAN, 0.0, 0.0)], 2.0).is_err());
assert!(fillet_g2(&body, &[Vec3::new(1000.0, 0.0, 0.0)], 2.0).is_err());
// Wider than the faces (20), over their far edges, cliffs: cut as if they went on. The
// section is the quintic from 25 out on each face, its inner control points a third and
// two thirds of the way to the corner; it takes the region between it and the corner
// inside the 20 x 20 section (clipped polygon of the curve, 2e5 pieces), along 20.
// section is the equal-area one from 25 out on each face; it takes the region between it
// and the corner inside the 20 x 20 section (the area under the curve clipped to the
// square, 2e4 strips on a 4e5-piece polygon of the curve), along 20.
let wide = fillet_g2(&body, &edge, 25.0).unwrap();
let v = crate::measure(&wide).unwrap().volume;
assert!((v - 6138.1606889014).abs() < 1e-6 * 6138.1606889014, "{v}");
assert!((v - 5329.0618943703).abs() < 1e-5 * 5329.0618943703, "{v}");
assert!(wide.validity().is_empty());
}
+28 -40
View File
@@ -305,30 +305,6 @@ mod tests {
8000.0 - full + (sum - union) * hh * hh
}
/// What the strips leave of the corner block where a patch over the corner of rounds `ra`,
/// `rb`, `rc` on a, b, c lies: beyond a's end section (y < rc), b's (x > 20 − rc) and c's end
/// (z > 20 − max(ra, rb), where c's rails meet a's and b's), the box's material that no
/// strip removes, by columns along z. Outside that block such a body is the strips' (its
/// rounds are the strips' cylinders up to those ends), so its patch takes at most this
/// beyond them.
fn corner_block(ra: f64, rb: f64, rc: f64) -> f64 {
let len = |d: f64, r: f64| if d < r { r - (r * r - (r - d) * (r - d)).max(0.0).sqrt() } else { 0.0 };
let (n, depth) = (2000, ra.max(rb));
let h = rc / n as f64;
let mut kept = 0.0;
for i in 0..n {
let dx = (i as f64 + 0.5) * h;
for j in 0..n {
let dy = (j as f64 + 0.5) * h;
if (rc - dx).powi(2) + (rc - dy).powi(2) > rc * rc && dx < rc && dy < rc {
continue;
}
kept += depth - len(dx, ra).max(len(dy, rb)).min(depth);
}
}
kept * h * h
}
/// Largest angle (degrees) between the two faces' normals along edges between curved faces.
fn kink(b: &Body) -> f64 {
let rec = Faces::of(b).unwrap();
@@ -515,29 +491,41 @@ mod tests {
assert!(r.validity().is_empty(), "{:?}", r.validity());
assert!((vol(&r) - sphere).abs() < 1e-11 * sphere, "{} vs {sphere}", vol(&r));
assert!(kink(&r) < 0.01);
// Different radii: a smooth patch over the corner, more than the strips take, and no more
// than they leave in the block it lies in: its rounds end at a's section y = rc, b's
// x = 20 − rc, and c's rails where they meet a's and b's (z = 20 − ra, z = 20 − rb), the
// patch's four corners. (2, 2, 3): 1.168 beyond the strips, of the 11.86 the block keeps;
// (2, 3, 4): 3.099 of 32.69.
// Different radii: SMLib's setback corner (SmBlendSetback.h). Each round runs whole up to
// its section square to its edge at its setback, the owner's rule's (at least 1.5 r, more
// where the rails beside it cross later, scaled to the least at which the corner's G1 fill
// is certified convex), and the hole between the three sections is filled by the setback
// split: one setback patch per round and the triangle between them in three
// quadrilaterals, so 6 planes, 3 rounds and 6 patches (15 faces), G1 across every seam.
// Outside the box the sections span ([20 - d_b, 20] x [0, d_a] x [20 - d_c, 20], d each
// edge's setback) the body is the three strips', so its volume is theirs within that box's.
// The rounds a and b of one radius lie mirrored about the plane x + y = 20 with c: one
// setback. (2, 2, 3): 3.6155, 3.6155, 4.6485; (2, 3, 4): 4.7745, 5.03975, 6.366 (a, b, c).
for (rs, sel) in [((2.0, 2.0, 3.0), fillet(&ab, &[c], 3.0)), ((2.0, 3.0, 4.0), fillet_radii(&bx, &[(a, 2.0), (b, 3.0), (c, 4.0)]))] {
let r = sel.unwrap();
assert_eq!(r.face_count(), 10);
assert_eq!(r.face_count(), 15);
assert_eq!(r.unmeshed_faces(), 0);
assert!(r.validity().is_empty(), "{:?}", r.validity());
let k = kink(&r);
assert!(k < 0.05, "kink {k}°");
assert!(k < 0.01, "kink {k}°");
let (ra, rb, rc) = rs;
let ends = [
Vec3::new(20.0 - ra, rc, 20.0),
Vec3::new(20.0, rc, 20.0 - ra),
Vec3::new(20.0 - rc, rb, 20.0),
Vec3::new(20.0 - rc, 0.0, 20.0 - rb),
];
for p in ends {
assert!(vertices(&r).iter().any(|q| q.dist(p) < 1e-9), "{rs:?}: no patch corner at {p:?}");
let vs = vertices(&r);
// An edge's setback: where its round's section meets both faces, at two vertices.
let setback = |at: &dyn Fn(f64) -> [Vec3; 2], r: f64| -> Option<f64> {
vs.iter()
.flat_map(|p| [p.y, 20.0 - p.x, 20.0 - p.z])
.find(|d| *d >= 1.5 * r - 1e-9 && *d < 20.0 && at(*d).iter().all(|q| vs.iter().any(|p| p.dist(*q) < 1e-9)))
};
let da = setback(&|d| [Vec3::new(20.0 - ra, d, 20.0), Vec3::new(20.0, d, 20.0 - ra)], ra);
let db = setback(&|d| [Vec3::new(20.0 - d, 0.0, 20.0 - rb), Vec3::new(20.0 - d, rb, 20.0)], rb);
let dc = setback(&|d| [Vec3::new(20.0 - rc, 0.0, 20.0 - d), Vec3::new(20.0, rc, 20.0 - d)], rc);
eprintln!("{rs:?}: setbacks {da:?} {db:?} {dc:?}");
let (Some(da), Some(db), Some(dc)) = (da, db, dc) else { panic!("{rs:?}: a round's setback section is missing: {da:?} {db:?} {dc:?}") };
if ra == rb {
assert!((da - db).abs() < 1e-9, "{da} vs {db}");
}
let (v, s) = (vol(&r), strips(ra, rb, rc));
assert!(v < s + 0.05 && v > s - corner_block(ra, rb, rc), "{v} vs {s}");
assert!((v - s).abs() < db * da * dc, "{v} vs {s}");
}
}
+14 -5
View File
@@ -418,7 +418,15 @@ mod imp {
"not supported yet: the {what} meets a corner of this kind, which SMLib's blends do not handle yet (where its edges \
run on tangentially into each other, or meet in a way no blend corner builds)"
)),
SmError::NoRoom => KernelError::TooLarge(format!("the {what} is larger than the faces beside its edges allow")),
// A setback too short for its corner's fill (SMLib certifies it convex only from
// its least on) names that least; any other is a blend too large for its room.
SmError::NoRoom => match solidcraft_smlib::blend_fit() {
Some(solidcraft_smlib::Fit::SetbackAtLeast(least)) => KernelError::Invalid(format!(
"the setback is too short for its corner: {}{least:.4} mm (the corner's patch is convex only from there)",
crate::SETBACK_AT_LEAST
)),
_ => KernelError::TooLarge(format!("the {what} is larger than the faces beside its edges allow")),
},
e => failed(what, "operation", e),
})?;
// Checked here unless the evaluation's validation mode leaves it to the feature's end
@@ -624,14 +632,15 @@ mod tests {
#[test]
fn a_linear_radius_runs_from_the_start_end() {
// 3 at x = 0 to 1 at x = 20: (1 - pi/4) L (r0^2 + r0 r1 + r1^2) / 3 taken away, from
// either end given as the start with the law reversed.
// 3 at x = 0 to 1 at x = 20: the balls' union, a cone, takes 18.5377834 (their great
// circles, (1 - pi/4) L (r0^2 + r0 r1 + r1^2) / 3, would take 18.5988), from either end
// given as the start with the law reversed.
let b = cube();
let at = Vec3::new(10.0, 20.0, 20.0);
let law = RadiusSpline { degree: 1, knots: vec![0.0, 0.0, 1.0, 1.0], radii: vec![3.0, 1.0] };
let r = imp::variable(&b, at, Vec3::new(0.0, 20.0, 20.0), &law).unwrap();
let want = 8000.0 - QUARTER * 20.0 * 13.0 / 3.0;
assert!((volume(&r) - want).abs() < 0.05, "{}", volume(&r));
let want = 8000.0 - 18.5377834;
assert!((volume(&r) - want).abs() < 0.01, "{}", volume(&r));
let back = imp::variable(&b, at, Vec3::new(20.0, 20.0, 20.0), &law.reversed()).unwrap();
assert!((volume(&back) - volume(&r)).abs() < 1e-6 * 8000.0);
}
+147 -40
View File
@@ -545,16 +545,17 @@ mod tests {
assert_eq!((less.eval(0.0), less.eval(1.0)), (2.0, 3.0));
}
/// The material a variable fillet removes from a right-angled edge of length L, radius
/// running r0 → r1: ∫ (1 − π/4) r(t)² dt = (1 − π/4) L (r0² + r0 r1 + r1²) / 3.
/// The material a variable fillet removes from a right-angled edge, radius running r0 → r1:
/// its balls' union (`removed_by_balls`; the balls' great circles, (1 − π/4) L (r0² + r0 r1 +
/// r1²) / 3, take more), to 1e-4 of itself.
#[test]
fn variable_radius_on_a_box_edge() {
let b = box_solid(Vec3::ZERO, Vec3::new(10.0, 20.0, 30.0)).unwrap();
// The edge along y at x = 10, z = 30; radius 2 at y = 0, 5 at y = 20.
let f = fillet_variable(&b, Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0), 2.0, 5.0).unwrap();
let removed = (1.0 - std::f64::consts::FRAC_PI_4) * 20.0 * (4.0 + 10.0 + 25.0) / 3.0;
let removed = removed_by_balls(20.0, &|u| 2.0 + 3.0 * u, &open, &[]);
let v = measure(&f).unwrap().volume;
assert!(rel(v, 6000.0 - removed) < 1e-4, "{v} vs {}", 6000.0 - removed);
assert!(rel(6000.0 - v, removed) < 1e-4, "{} removed vs {removed}", 6000.0 - v);
// Started from the other end: the same solid.
let h = fillet_variable(&b, Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 20.0, 30.0), 5.0, 2.0).unwrap();
assert!(rel(measure(&h).unwrap().volume, v) < 1e-9);
@@ -564,37 +565,42 @@ mod tests {
assert!(rel(measure(&g).unwrap().volume, measure(&k).unwrap().volume) < 1e-6);
// Wider than the faces: past r = 10 the contact on the top leaves it across its far edge
// x = 0 (past r = 30 the one on the side across its bottom), cliffs. The round is built as
// if the faces went on, and what it removes is cut from the box: at each y the section of
// the r x r square at the edge less the quarter disc about (10 - r, 30 - r), inside
// [0, 10] x [0, 30], in closed form (a, b from the disc's centre: a, b in [0, r],
// a^2 + b^2 >= r^2, a >= r - 10, b >= r - 30), integrated along y by Gauss-Legendre
// between the kinks at r = 10 and 30: 2453.81232061 removed, 3546.18767939 left (to 1e-4,
// as the variable rounds above).
// if the faces went on, and what it removes is cut from the box: the balls' union inside
// the box's section, 10 along the top and 30 down the side (the great circles' would take
// 2453.81, near twice as much).
let w = fillet_variable(&b, Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0), 2.0, 50.0).unwrap();
let removed_w = removed_by_balls(20.0, &|u| 2.0 + 48.0 * u, &slab, &[20.0 * 8.0 / 48.0, 20.0 * 28.0 / 48.0]);
let vw = measure(&w).unwrap().volume;
assert!(rel(vw, 3546.1876793935) < 1e-4, "{vw} vs 3546.1876793935");
assert!(rel(6000.0 - vw, removed_w) < 1e-4, "{} removed vs {removed_w}", 6000.0 - vw);
}
/// A radius through a middle point: straight pieces (exact: each piece removes
/// (1 − π/4) L (a² + ab + b²) / 3), and smooth (the removed volume integrates the law).
/// A radius through a middle point, straight pieces 2 → 4 → 2: a corner in the law, where the
/// two sides' rounds cross and each ends where they do; the balls' union takes 39.541034
/// (the halves' great circles would take 40.0590).
#[test]
fn variable_radius_with_a_corner_through_a_middle_point() {
let b = box_solid(Vec3::ZERO, Vec3::new(10.0, 20.0, 30.0)).unwrap();
let (at, start) = (Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0));
let f = fillet_variable_points(&b, at, start, &[(0.0, 2.0), (0.5, 4.0), (1.0, 2.0)], false).unwrap();
let removed = removed_by_balls(20.0, &|u| 4.0 - 4.0 * (u - 0.5).abs(), &open, &[10.0]);
assert!((removed - 39.541034).abs() < 1e-5, "{removed}");
let v = measure(&f).unwrap().volume;
assert!(rel(6000.0 - v, removed) < 1e-4, "{} removed vs {removed}", 6000.0 - v);
assert_eq!(f.unmeshed_faces(), 0);
}
/// A radius through a middle point, smooth: the removed volume is the balls' union.
#[test]
fn variable_radius_through_a_middle_point() {
let b = box_solid(Vec3::ZERO, Vec3::new(10.0, 20.0, 30.0)).unwrap();
let (at, start) = (Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0));
let k = 1.0 - std::f64::consts::FRAC_PI_4;
let pts = [(0.0, 2.0), (0.5, 4.0), (1.0, 2.0)];
let f = fillet_variable_points(&b, at, start, &pts, false).unwrap();
let removed = 2.0 * k * 10.0 * (4.0 + 8.0 + 16.0) / 3.0;
let v = measure(&f).unwrap().volume;
assert!(rel(v, 6000.0 - removed) < 1e-4, "{v} vs {}", 6000.0 - removed);
assert_eq!(f.unmeshed_faces(), 0);
let s = fillet_variable_points(&b, at, start, &pts, true).unwrap();
let law = RadiusSpline::through(&pts, true);
let n = 20000;
let integral: f64 = (0..n).map(|i| law.eval((i as f64 + 0.5) / n as f64).powi(2)).sum::<f64>() / n as f64 * 20.0;
assert!((law.eval(0.5) - 4.0).abs() < 1e-12 && (law.eval(0.0) - 2.0).abs() < 1e-12);
let removed = removed(&law, 20.0);
let v = measure(&s).unwrap().volume;
assert!(rel(v, 6000.0 - k * integral) < 1e-4, "{v} vs {}", 6000.0 - k * integral);
assert!(rel(6000.0 - v, removed) < 1e-4, "{} removed vs {removed}", 6000.0 - v);
// Unsorted, from the other end: the same.
let r = fillet_variable_points(&b, at, Vec3::new(10.0, 20.0, 30.0), &[(1.0, 2.0), (0.0, 2.0), (0.5, 4.0)], true).unwrap();
assert!(rel(measure(&r).unwrap().volume, v) < 1e-9);
@@ -606,16 +612,18 @@ mod tests {
}
/// The radius running down to zero at one end: the blend closes to the corner, a three-sided
/// face, and the end face there is untouched. Removed (1 − π/4) L r0² / 3 when linear.
/// face, and the end face there is untouched. Removed: the balls' union (a cone to its apex
/// when linear, 22.594877; the great circles' (1 − π/4) L r0² / 3 would take 22.8945).
#[test]
fn variable_radius_down_to_zero_at_one_end() {
let b = box_solid(Vec3::ZERO, Vec3::new(10.0, 20.0, 30.0)).unwrap();
let (at, start, end) = (Vec3::new(10.0, 10.0, 30.0), Vec3::new(10.0, 0.0, 30.0), Vec3::new(10.0, 20.0, 30.0));
let k = 1.0 - std::f64::consts::FRAC_PI_4;
let cone = removed_by_balls(20.0, &|u| 4.0 - 4.0 * u, &open, &[]);
assert!((cone - 22.594877).abs() < 1e-5, "{cone}");
for (r0, r1) in [(4.0, 0.0), (0.0, 4.0)] {
let f = fillet_variable(&b, at, start, r0, r1).unwrap();
let v = measure(&f).unwrap().volume;
assert!(rel(v, 6000.0 - k * 20.0 * 16.0 / 3.0) < 1e-4, "{r0} → {r1}: {v}");
assert!(rel(6000.0 - v, cone) < 1e-4, "{r0} → {r1}: {} removed vs {cone}", 6000.0 - v);
assert_eq!(f.unmeshed_faces(), 0);
let (faces, vertices) = faces_and_vertices(&f);
assert_eq!(faces.len(), 7);
@@ -628,10 +636,9 @@ mod tests {
let pts = [(0.0, 3.0), (0.5, 4.0), (1.0, 0.0)];
let s = fillet_variable_points(&b, at, start, &pts, true).unwrap();
let law = RadiusSpline::through(&pts, true);
let n = 20000;
let integral: f64 = (0..n).map(|i| law.eval((i as f64 + 0.5) / n as f64).powi(2)).sum::<f64>() / n as f64 * 20.0;
let removed = removed(&law, 20.0);
let v = measure(&s).unwrap().volume;
assert!(rel(v, 6000.0 - k * integral) < 1e-4, "{v} vs {}", 6000.0 - k * integral);
assert!(rel(6000.0 - v, removed) < 1e-4, "{} removed vs {removed}", 6000.0 - v);
// Zero at both ends, zero in the middle, a smooth radius dipping below zero, or a radius
// too small to build: refused.
assert!(fillet_variable(&b, at, start, 0.0, 0.0).is_err());
@@ -664,11 +671,113 @@ mod tests {
crate::boolean(&plate, &wall, crate::BoolOp::Union).unwrap().unwrap()
}
/// The material a right-angled blend with the radius `law` removes along an edge of length L:
/// ∫ (1 − π/4) r² over the edge.
/// Simpson's rule for `f` on [a, b] in `n` (even) panels.
fn simpson(f: impl Fn(f64) -> f64, a: f64, b: f64, n: usize) -> f64 {
let h = (b - a) / n as f64;
(0..=n)
.map(|i| {
f(a + i as f64 * h)
* if i == 0 || i == n {
1.0
} else if i % 2 == 1 {
4.0
} else {
2.0
}
})
.sum::<f64>()
* h
/ 3.0
}
/// The material a variable round removes from a right-angled edge of length `len`: what no
/// ball reaches, the round being the union of its balls (their envelope; each ball touches it
/// along the circle square to the line of centres, not its great circle, as the radius
/// varies). The ball at s along the edge has radius `law(s / len)` (the law going on past the
/// ends along its end slopes) and its centre that far from both faces. In each plane square
/// to the edge at x, the ray from the edge at angle θ from the first face meets the union
/// first at the least entry over the balls, and the corner up to there is taken, as far as
/// `room(x, θ)` (the body's section along the ray; ∞ where it does not end): ∫ ρ²/2 dθ,
/// integrated over x, both by Simpson's rule, split at `kinks` (where the law turns).
pub(crate) fn removed_by_balls(len: f64, law: &dyn Fn(f64) -> f64, room: &dyn Fn(f64, f64) -> f64, kinks: &[f64]) -> f64 {
let h = 1e-7;
let (k0, k1) = ((law(h) - law(0.0)) / (h * len), (law(1.0) - law(1.0 - h)) / (h * len));
let r = |s: f64| {
if s < 0.0 {
law(0.0) + k0 * s
} else if s > len {
law(1.0) + k1 * (s - len)
} else {
law(s / len)
}
};
let big = (0..=1000).map(|i| law(i as f64 / 1000.0)).fold(0.0f64, f64::max);
let entry = |x: f64, c: f64, sn: f64, s: f64| {
let rr = r(s);
let p = rr * (c + sn);
let disc = p * p - (x - s) * (x - s) - rr * rr;
if rr > 0.0 && disc >= 0.0 { p - disc.sqrt() } else { f64::INFINITY }
};
let rho = |x: f64, t: f64| {
let (c, sn) = (t.cos(), t.sin());
let (lo, hi, scan) = (x - 1.5 * big, x + 1.5 * big, 400);
let step = (hi - lo) / scan as f64;
let (mut best, mut at) = (f64::INFINITY, x);
for i in 0..=scan {
let s = lo + i as f64 * step;
let e = entry(x, c, sn, s);
if e < best {
(best, at) = (e, s);
}
}
let (mut a, mut b, g) = (at - step, at + step, (5f64.sqrt() - 1.0) / 2.0);
for _ in 0..80 {
let (m1, m2) = (b - g * (b - a), a + g * (b - a));
if entry(x, c, sn, m1) < entry(x, c, sn, m2) {
b = m2;
} else {
a = m1;
}
}
best.min(entry(x, c, sn, 0.5 * (a + b)))
};
// The rays meet the union tangentially at the faces (ρ goes as the root of the angle
// there): θ = π/4 (1 − cos(π q)) makes the integrand smooth in q.
let area = |x: f64| {
let q = std::f64::consts::PI;
simpson(
|u: f64| {
let t = q / 4.0 * (1.0 - (q * u).cos());
let p = rho(x, t).min(room(x, t));
let p = if p.is_finite() { p } else { 0.0 };
0.5 * p * p * q * q / 4.0 * (q * u).sin()
},
0.0,
1.0,
128,
)
};
let mut ends = vec![0.0];
ends.extend(kinks.iter().copied().filter(|k| *k > 0.0 && *k < len));
ends.push(len);
ends.windows(2).map(|w| simpson(area, w[0], w[1], 2 * ((64.0 * (w[1] - w[0]) / len).ceil() as usize).max(1))).sum()
}
/// No end to the body's section: the faces go on past the round.
fn open(_x: f64, _t: f64) -> f64 {
f64::INFINITY
}
/// The 10 x 20 x 30 box's section about its edge x = 10, z = 30: 10 along the top, 30 down
/// the side.
fn slab(_x: f64, t: f64) -> f64 {
(10.0 / t.cos().max(1e-300)).min(30.0 / t.sin().max(1e-300))
}
/// The material a right-angled blend with the radius `law` removes along an edge of length L
/// where the faces go on past it: the union of its balls.
fn removed(law: &RadiusSpline, len: f64) -> f64 {
let n = 20000;
(1.0 - std::f64::consts::FRAC_PI_4) * (0..n).map(|i| law.eval((i as f64 + 0.5) / n as f64).powi(2)).sum::<f64>() / n as f64 * len
removed_by_balls(len, &|u| law.eval(u), &open, &[])
}
/// The volume of a fine tessellation: at a blend closed to a point `measure` takes a
@@ -749,15 +858,13 @@ mod tests {
assert!(fillet_variable_spline(&b, at, start, &unclamped).is_err());
let dips = RadiusSpline { radii: vec![2.0, 0.0, 0.0, 3.0, 2.0], ..law.clone() };
assert!(fillet_variable_spline(&b, at, start, &dips).is_err());
// A middle radius beyond the faces (up to 21.25 on the top 10 wide): the contact on the top
// leaves it across its far edge, a cliff, and the round is cut as if the top went on: at
// each y the r x r square at the edge less the quarter disc about (10 - r, 30 - r), inside
// [0, 10] x [0, 30], in closed form, integrated along y (Gauss-Legendre, split at the
// knot and where r crosses 10): 726.5334566720 removed.
// A middle radius up to 21.25 (on the top 10 wide), rising and falling so fast that the
// balls' envelope folds over itself at the peak: for the balls' family
// F(y, p) = |p - c(y)|² - r(y)² the touch arc there has F_yy down to -8.88 (where the touch
// circles move back along the edge). Refused, never built folded (the balls' great
// circles, which never fold, built it to 726.53 removed against the union's 332.2).
let big = RadiusSpline { radii: vec![2.0, 2.0, 40.0, 3.0, 2.0], ..law };
let wide = fillet_variable_spline(&b, at, start, &big).unwrap();
let vw = fine_volume(&wide);
assert!(rel(vw, 6000.0 - 726.5334566720) < 1e-4, "{vw} vs {}", 6000.0 - 726.5334566720);
assert!(matches!(fillet_variable_spline(&b, at, start, &big), Err(KernelError::TooLarge(_))));
}
#[test]
+1 -1
View File
@@ -468,7 +468,7 @@ impl SurfaceKind {
S::Plane { .. } | S::PlaneThrough { .. } => SurfaceKind::Plane,
S::Cylinder { .. } => SurfaceKind::Cylinder,
S::Cone { .. } => SurfaceKind::Cone,
S::Sphere { .. } => SurfaceKind::Sphere,
S::Sphere { .. } | S::BallPatch { .. } => SurfaceKind::Sphere,
S::Torus { .. } => SurfaceKind::Torus,
S::Spline { .. } | S::RationalSpline { .. } | S::PlacedRevolution { .. } => SurfaceKind::Freeform,
}
+7 -6
View File
@@ -344,10 +344,11 @@ fn a_mixed_convexity_corner_is_refused() {
}
/// Curvature-continuous rounds (`fillet_g2`, equivalent radius 5) of the three edges at the box
/// corner: SMLib's own corner patch between them is only tangent to the rounds, its curvature
/// jumping across their seams (it showed as a dome on the corner), so the corner is the G2 patch
/// instead, filled between the curvature rounds' setback sections: one closed valid solid, the
/// patch curvature-continuous to every round and face round it clear of the hole's corners.
/// corner: SMLib's own corner between them is its setback corner, whose fill for curvature
/// sections meets the rounds and faces curvature-continuously (SmBlendSetback.h: G2 for an order
/// 2 section), as the G2 patch filled between the curvature rounds' setback sections does: each
/// one closed valid solid, curvature-continuous across every seam round the corner clear of the
/// hole's corners.
#[test]
fn curvature_rounds_meet_at_a_g2_corner() {
let (body, edges) = box_corner();
@@ -357,9 +358,9 @@ fn curvature_rounds_meet_at_a_g2_corner() {
let v = crate::validity::validity(&g2);
assert!(v.ok(), "{:?}", v);
let (_, clear, n) = corner_seams(&g2, 0.15);
let (_, smlib_clear, _) = corner_seams(&smlib_corner, 0.15);
let (_, smlib_clear, smlib_n) = corner_seams(&smlib_corner, 0.15);
assert!(n > 0 && clear <= CURVATURE_SHARE / 5.0, "{clear} over {n} stations");
assert!(smlib_clear > 10.0 * CURVATURE_SHARE / 5.0, "SMLib's own corner patch: {smlib_clear}");
assert!(smlib_n > 0 && smlib_clear <= CURVATURE_SHARE / 5.0, "SMLib's own corner: {smlib_clear} over {smlib_n} stations");
// Both differ only within the setback cube (side 1.5 r = 7.5) at the corner.
let (a, b) = (measure(&g2).unwrap().volume, measure(&smlib_corner).unwrap().volume);
assert!((a - b).abs() < 7.5f64.powi(3), "{a} vs {b}");
+14 -3
View File
@@ -44,8 +44,10 @@ fn a_setback_off_the_rounded_corner_is_refused() {
}
/// A setback that does not reach past where the corner's rounds meet without one (on faces square
/// to each other, the radius) is refused before SMLib runs, naming that least setback; a setback
/// just past it builds.
/// to each other, the radius) is refused before SMLib runs, naming that least setback. Past it, a
/// setback still too short for SMLib's corner fill (certified convex only from a setback on,
/// smlib_blend_fit_size's SMLIB_FIT_SETBACK_AT_LEAST) is refused naming the smallest that builds,
/// at most twice the one asked for (the certificate scales the setbacks up to 2); that one builds.
#[test]
fn a_setback_not_past_the_radius_is_refused_naming_the_least() {
for s in [2.0, 5.0] {
@@ -54,7 +56,16 @@ fn a_setback_not_past_the_radius_is_refused_naming_the_least() {
other => panic!("setback {s}: {:?}", other.map(|b| b.face_count())),
}
}
let out = corner_case(Some(5.5)).unwrap();
let least = match corner_case(Some(5.5)) {
Err(KernelError::Invalid(why)) => {
let named = why.split_once(SETBACK_AT_LEAST).and_then(|(_, rest)| rest.split_once(" mm")).and_then(|(n, _)| n.parse::<f64>().ok());
named.unwrap_or_else(|| panic!("no least setback named: {why}"))
}
other => panic!("setback 5.5: {:?}", other.map(|b| b.face_count())),
};
eprintln!("setback 5.5 refused: the smallest that builds is {least}");
assert!(least > 5.5 && least <= 11.0, "{least}");
let out = corner_case(Some(least)).unwrap();
assert!(out.validity().is_empty(), "{:?}", out.validity());
// The least the refusal names, for a form to show before it runs; none at no vertex.
let b = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(60.0, 40.0, 20.0)).unwrap();
+2
View File
@@ -344,6 +344,8 @@ fn iso_record(surface: &brep::Surface, along_u: bool, fixed: f64, (a, b): (f64,
Surface::PlacedRevolution { .. } => {
return Err(KernelError::Failed("not supported yet: an iso curve of a revolved surface record".into()));
}
// A label: the face's (u, v) are its NURBS form's, whose rows are its iso curves.
Surface::BallPatch { .. } => return Err(KernelError::Invalid("an iso curve of a ball patch's label (take its NURBS form's)".into())),
})
}
+3 -3
View File
@@ -156,8 +156,8 @@ mod validity;
pub mod watch;
pub use blend::{
ChamferSide, blend_edges, chamfer, chamfer_max, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners, fillet_g2_with_g2_corners,
fillet_least_setback, fillet_max, fillet_max_each, fillet_removing, fillet_setback, tangent_chain,
ChamferSide, SETBACK_AT_LEAST, blend_edges, chamfer, chamfer_max, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners,
fillet_g2_with_g2_corners, fillet_least_setback, fillet_max, fillet_max_each, fillet_removing, fillet_setback, tangent_chain,
};
pub use blend_corner::fillet_radii;
pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fillet_variable, fillet_variable_points, fillet_variable_spline, taper};
@@ -202,7 +202,7 @@ pub use meshbody::{MAX_TRIANGLES, mesh_body};
pub use native::{body_from_smlib, smlib_body};
pub use native::{brep_record, decode_native_body, encode_native_body, native_preview_mesh};
pub use offset::with_same_surface;
pub use ops::{BoolOp, boolean, half_space, join_apart, place_matrix, split_by_plane, transform, transform_matrix, union_all, union_apart};
pub use ops::{BoolOp, apart, boolean, half_space, join_apart, place_matrix, split_by_plane, transform, transform_matrix, union_all, union_apart};
pub use point_path::{
PathPiece, PointPath, SpaceBezier, along_path, curve_path, edge_path, face_section_along, project_onto_face, sketch_record_curve,
sweep_point_path_with,
@@ -3485,8 +3485,8 @@
1.0
],
[
0.25000000000000006,
0.7500000000000002
0.25,
0.7500000000000001
]
],
"loops": [
@@ -3509,8 +3509,8 @@
0.25000000000000006
],
[
-1.6653345369377353e-16,
0.7500000000000002
-1.6653345369377348e-16,
0.75
]
]
}
@@ -3556,7 +3556,7 @@
],
[
0.9999999999999998,
0.75
0.7499999999999998
]
]
}
@@ -3575,11 +3575,11 @@
"points": [
[
0.9999999999999998,
0.75
0.7499999999999999
],
[
-1.6653345369377353e-16,
0.7500000000000002
-1.665334536937735e-16,
0.7500000000000001
]
]
}
@@ -3592,13 +3592,13 @@
"homogeneous": [
[
[
0.0,
-3.552713678800501e-15,
1.9999999999999964,
10.0,
1.0
],
[
40.00000000000001,
40.0,
1.9999999999999964,
10.0,
1.0
@@ -3606,13 +3606,13 @@
],
[
[
0.0,
-2.512147933894045e-15,
1.3778763926929864e-15,
7.071067811865485,
0.7071067811865489
],
[
28.284271247461962,
28.284271247461955,
1.3778763926929864e-15,
7.071067811865485,
0.7071067811865489
@@ -3620,13 +3620,13 @@
],
[
[
0.0,
-3.552713678800501e-15,
-4.440892098500626e-16,
8.0,
1.0
],
[
40.00000000000001,
40.0,
-4.440892098500626e-16,
8.0,
1.0
@@ -3643,32 +3643,32 @@
1.0
],
"v_knots": [
0.25000000000000006,
0.25000000000000006,
0.7500000000000002,
0.7500000000000002
0.25,
0.25,
0.7500000000000001,
0.7500000000000001
]
},
"surface": {
"frame": {
"origin": [
0.0,
2.0000000000000027,
7.9999999999999964
-20.0,
2.000000000000002,
7.999999999999997
],
"x_axis": [
0.0,
-3.1086244689504328e-15,
-2.886579864025403e-15,
1.0
],
"y_axis": [
0.0,
-1.0,
-3.1086244689504328e-15
-2.886579864025403e-15
]
},
"kind": "cylinder",
"radius": 2.0000000000000036
"radius": 2.000000000000003
}
}
],
+1
View File
@@ -55,6 +55,7 @@ pub(crate) fn surf_of(b: &Brep, face: usize) -> Option<Surf> {
Surf::Cylinder { o, a, r: *radius, convex: out }
}
Surface::Sphere { frame, radius } => Surf::Sphere { c: Vec3::from(frame.origin), r: *radius, convex: out },
Surface::BallPatch { center, radius } => Surf::Sphere { c: Vec3::from(*center), r: *radius, convex: out },
Surface::Cone { frame, radius_at_origin, half_angle } => {
let (o, z) = axes(frame)?;
let apex = o + z * (-radius_at_origin / half_angle.tan());
+3 -2
View File
@@ -56,8 +56,9 @@ pub fn union_all(bodies: &[&Body]) -> Result<Body> {
}
/// Whether B-rep bodies `a` and `b` are apart: their exact boxes, each grown by twice its
/// record's tolerance (as [`union_apart`] takes them), do not meet.
fn apart(a: &Body, b: &Body) -> Result<bool> {
/// record's tolerance (as [`union_apart`] takes them), do not meet. Never for a mesh body. The
/// way to skip work on bodies that cannot meet: a mesh's box is not the body's.
pub fn apart(a: &Body, b: &Body) -> Result<bool> {
if a.is_mesh() || b.is_mesh() {
return Ok(false);
}
+1
View File
@@ -109,6 +109,7 @@ fn kind(s: &brep::Surface) -> &'static str {
brep::Surface::Cylinder { .. } => "cylinder",
brep::Surface::Cone { .. } => "cone",
brep::Surface::Sphere { .. } => "sphere",
brep::Surface::BallPatch { .. } => "ball patch",
brep::Surface::Torus { .. } => "torus",
brep::Surface::Spline { .. } => "spline",
brep::Surface::RationalSpline { .. } => "rational spline",
+8 -1
View File
@@ -1974,8 +1974,15 @@ fn fillet_three_edges_at_a_corner() {
let a = r * r * (1.0 - PI / 4.0);
let want = a * (40.0 + 30.0 + 20.0) - 3.0 * a * r + (r * r * r - PI * r * r * r / 6.0);
assert!(rel(removed, want) < 2e-3, "removed {removed} vs {want}");
// The corner is one face: SMLib builds it as a rational patch exactly on the ball's sphere
// (a ball patch, not SMLib's sphere), between the three cylinders and the six planes; the
// record keeps its sphere, so it counts as one.
let m = measure(&f).unwrap().merged;
assert_eq!(m.face_types.get("sphere"), Some(&1), "{m:?}");
assert_eq!(
(m.faces, m.face_types.get("plane"), m.face_types.get("cylinder"), m.face_types.get("sphere")),
(10, Some(&6), Some(&3), Some(&1)),
"{m:?}"
);
}
#[test]
+8 -8
View File
@@ -1,10 +1,10 @@
# Rust target, SHA-256 of smlib-<target>.tar.gz in the release prebuilt-<SMLIB_REV> of
# tangency-labs/smlib. Written by scripts/fetch-smlib.sh --pin; never edited by hand.
aarch64-apple-darwin 3a782c2d9f89878fcb19c0ea9dfc29853204a6f6af37576722ed6334b06552c0
aarch64-pc-windows-msvc f7a5cbe28c65a8d9dd88e22939ac3c12d5b54431ad16f1b0a1f6aa5882de2b9d
aarch64-unknown-linux-gnu bb1166f65589ce2c385550708992d0798e010d6689952ae07232aeec438e5879
i686-pc-windows-msvc cadf6d7c35b3e20345e064d4369c777d62561afac5cf5b7be52da6dd6f9aa785
wasm32-unknown-unknown 250573fd99e9b109ae9f8ac16ecd4a5e55dfcd13c060763c89167bc2ee4bd4b8
x86_64-apple-darwin 13cf77518ccbd9a3095e0d2eaabb7844c94079aa4ef88ef10fe5a2bf1eb18438
x86_64-pc-windows-msvc 44b729d524b85de5b0aba2f24cdb6a9e449d644ef3ea4129dabbf5338b835018
x86_64-unknown-linux-gnu 7fe1a151932370b6ab4dee1e3fad30f62e4b9d2a1ac7bd1ca423061a15276a09
aarch64-apple-darwin b045d887901556d6c8f60a38fbbecdda765d8dafa6f7a5f72100a7deba3d1d6b
aarch64-pc-windows-msvc c5dbd7dc9221e8638cc9e09273f9e2fe19720f0a02dd40c66d709c3d8effe0c0
aarch64-unknown-linux-gnu f3ad86d6cd79570c428905548fabdb92005a78a9fc8d200d12047a2e972894ed
i686-pc-windows-msvc 1e4e3999663f4cae113149bcf84d970264b776cd762fd2b7d2dfcf49f9f35772
wasm32-unknown-unknown 33702afaa8af13e38c9aa8b894fa38f8e69b147c5cda0606c16b0c786dd46389
x86_64-apple-darwin b41d081c90d0028d76d92c08c72465da39d52976ab416c878d5ff0ecb6eccb73
x86_64-pc-windows-msvc 10252e3cd701be53697eea11ce68d29b244a2d25b677b57abf632f9f00bbc05b
x86_64-unknown-linux-gnu e6a81eb955405c815e3efc05ac3ab6bd78df83e43d244a6b33bbb390a197c2db
+1 -1
View File
@@ -1 +1 @@
26cc45185b7ee6faba5b684f83d7bc2c53c1a559
a18bbe523b9c7819f1a9da8c40a54c15f9936125
+27 -5
View File
@@ -179,6 +179,7 @@ const I64_IDS: &[i32] = &[
FACE_NURBS_V_ORDER,
FACE_NURBS_FORM,
EDGE_NURBS_FORM,
FACE_DEFINITION_REAL_COUNT,
];
/// The double arrays this mapping reads.
@@ -232,6 +233,7 @@ const F64_IDS: &[i32] = &[
FACE_TORUS_MINOR_RADIUS,
EDGE_TOLERANCE,
VERTEX_TOLERANCE,
FACE_DEFINITION_REALS,
];
// The kind values as the arrays hold them (int64); a local item shadows the glob import.
@@ -241,6 +243,7 @@ const SURFACE_CYLINDER: i64 = flat::SURFACE_CYLINDER as i64;
const SURFACE_CONE: i64 = flat::SURFACE_CONE as i64;
const SURFACE_SPHERE: i64 = flat::SURFACE_SPHERE as i64;
const SURFACE_TORUS: i64 = flat::SURFACE_TORUS as i64;
const SURFACE_BALL_PATCH: i64 = flat::SURFACE_BALL_PATCH as i64;
const CURVE_NURBS: i64 = flat::CURVE_NURBS as i64;
const CURVE_LINE: i64 = flat::CURVE_LINE as i64;
const CURVE_CIRCLE: i64 = flat::CURVE_CIRCLE as i64;
@@ -302,7 +305,8 @@ fn frame(origin: [f64; 3], axis: [f64; 3], x: [f64; 3]) -> b::Frame {
// ---- Geometry ---------------------------------------------------------------------------
/// The schema record of each face's surface, in face order, and its NURBS form when the arrays give
/// one (`FACE_NURBS_FORM`: an analytic face with the NURBS SMLib holds it as).
/// one (`FACE_NURBS_FORM`: an analytic face with the NURBS SMLib holds it as; a ball patch: its
/// net, the face's own NURBS block, with its sphere from `FACE_DEFINITION_REALS`).
fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surface>)>> {
let kinds = flat.i(FACE_SURFACE_TYPE);
if kinds.len() != n_faces {
@@ -345,10 +349,13 @@ fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surfac
if !forms.is_empty() && forms.len() != n_faces {
return Err(invalid("face NURBS forms"));
}
let mut k = [0usize; 6];
// The definitions, face by face: each defined face's count, then its reals from `defined`.
let (counts, reals) = (flat.i(FACE_DEFINITION_REAL_COUNT), flat.f(FACE_DEFINITION_REALS));
let mut defined = 0usize;
let mut k = [0usize; 7];
let mut out = Vec::with_capacity(n_faces);
for (f, &kind) in kinds.iter().enumerate() {
let slot = usize::try_from(kind).ok().filter(|s| *s < 6).ok_or_else(|| unsupported(format!("SMLib surface kind {kind}")))?;
let slot = usize::try_from(kind).ok().filter(|s| *s < 7).ok_or_else(|| unsupported(format!("SMLib surface kind {kind}")))?;
let i = at(&k, slot, "kind")?;
if let Some(c) = k.get_mut(slot) {
*c += 1;
@@ -404,10 +411,25 @@ fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surfac
tube_start: 0.0,
},
SURFACE_NURBS => next_nurbs(&mut nurbs_at)?,
SURFACE_BALL_PATCH => {
if at(counts, i, "ball patch definition")? != 4 {
return Err(invalid("ball patch definition"));
}
let d = reals.get(defined..defined + 4).ok_or_else(|| invalid("ball patch definition"))?;
defined += 4;
b::Surface::BallPatch { center: [d[0], d[1], d[2]], radius: d[3] }
}
other => return Err(unsupported(format!("SMLib surface kind {other}"))),
};
// A NURBS face's own block came first; a form follows the analytic face's canonical entry.
let form = if kind != SURFACE_NURBS && forms.get(f) == Some(&1) { Some(next_nurbs(&mut nurbs_at)?) } else { None };
// A NURBS face's or a ball patch's own block came first (the patch's is its form); a form
// follows the analytic face's canonical entry.
let form = match kind {
SURFACE_NURBS => None,
SURFACE_BALL_PATCH if forms.get(f) == Some(&1) => return Err(invalid("ball patch with a NURBS form")),
SURFACE_BALL_PATCH => Some(next_nurbs(&mut nurbs_at)?),
_ if forms.get(f) == Some(&1) => Some(next_nurbs(&mut nurbs_at)?),
_ => None,
};
// The record's torus is a ring torus. SMLib keeps horn and spindle tori too (a half disc
// revolved about a line through its rim's end, its tube touching the axis, the major
// radius a rounding below the minor): such a face is its NURBS form, unlabelled.
+61
View File
@@ -55,6 +55,67 @@ mod native_tests {
}
}
/// A 20 mm cube with every edge rounded r 2: its eight corners are SMLib's ball patches
/// (`SmBallPatch`, flat kind 6). The record keeps each as a ball patch on the sphere of
/// radius 2 about the corner pulled in by 2 along each axis, its net the NURBS form, and
/// every point of the net on that sphere. Imported back, the patches are kind 6 again with
/// the same definitions, bit for bit, and the body keeps its closed-form volume.
#[test]
fn a_rounded_cubes_ball_patches_go_round_with_their_spheres() {
let (a, r) = (20.0, 2.0);
let h = a / 2.0;
let mids: Vec<[f64; 3]> = [(0.0, 0.0), (a, 0.0), (0.0, a), (a, a)].iter().flat_map(|&(p, q)| [[h, p, q], [p, h, q], [p, q, h]]).collect();
let f = Brep::make_box([0.0; 3], [a; 3]).unwrap().fillet(&mids, 0.1, r).unwrap();
let record = f.to_native().unwrap();
record.validate().unwrap();
let patches: Vec<(&b::Face, [f64; 3], f64)> = record
.faces
.iter()
.filter_map(|face| match face.surface {
b::Surface::BallPatch { center, radius } => Some((face, center, radius)),
_ => None,
})
.collect();
assert_eq!(patches.len(), 8, "one ball patch per corner");
let mut centers: Vec<[f64; 3]> = patches.iter().map(|p| p.1).collect();
centers.sort_by(|p, q| p.partial_cmp(q).unwrap());
let inset = [r, a - r];
let want: Vec<[f64; 3]> = (0..8).map(|i| [inset[i >> 2], inset[(i >> 1) & 1], inset[i & 1]]).collect();
for (got, want) in centers.iter().zip(&want) {
assert!((0..3).all(|k| (got[k] - want[k]).abs() < 1e-9), "centre {got:?}, the corner's {want:?}");
}
for (face, c, radius) in &patches {
assert!((radius - r).abs() < 1e-12, "radius {radius}");
assert!(matches!(face.nurbs, Some(b::Surface::RationalSpline { .. })), "a ball patch's form is its rational net");
let eval = b::eval::SurfaceEval::new(face.parameters()).unwrap();
let [[u0, u1], [v0, v1]] = face.domain;
for (s, t) in [(0.25, 0.25), (0.5, 0.5), (0.75, 0.3), (0.1, 0.9)] {
let q = eval.point([u0 + s * (u1 - u0), v0 + t * (v1 - v0)]).unwrap();
let d = ((q.x - c[0]).powi(2) + (q.y - c[1]).powi(2) + (q.z - c[2]).powi(2)).sqrt();
assert!((d - r).abs() < 1e-12 * a, "a point of the patch {} off its sphere", d - r);
}
}
let back = Brep::from_record(&record, true).unwrap();
let check = back.check(CheckLevel::All).unwrap();
assert!(check.is_valid(), "{check}");
let arrays = back.arrays().unwrap();
let kinds = arrays.ints.get(&crate::flat::FACE_SURFACE_TYPE).cloned().unwrap_or_default();
assert_eq!(kinds.iter().filter(|k| **k == i64::from(crate::flat::SURFACE_BALL_PATCH)).count(), 8);
// The record numbers faces in canonical order, not SMLib's: the same definitions, each bit
// for bit, in another order.
let defs = |b: &Brep| {
let reals = b.arrays().unwrap().reals.get(&crate::flat::FACE_DEFINITION_REALS).cloned().unwrap_or_default();
let mut d: Vec<[u64; 4]> = reals.chunks(4).map(|c| [0, 1, 2, 3].map(|k| c[k].to_bits())).collect();
d.sort_unstable();
d
};
assert_eq!(defs(&back), defs(&f), "the definitions come back bit for bit");
let b = a - 2.0 * r;
let want = b.powi(3) + 6.0 * b * b * r + 3.0 * PI * r * r * b + 4.0 / 3.0 * PI * r.powi(3);
let got = back.mass_properties(MEASURE_ACCURACY).unwrap().volume;
assert!((got - want).abs() < 1e-6 * want, "volume {got} vs {want}");
}
/// A tagged body's own history, renumbered with its export's order on both sides, is the
/// identity in canonical order: the order says where each SMLib face and edge went.
#[test]
+20 -1
View File
@@ -1,5 +1,5 @@
//! The operations block of `capi/include/smlib.h` (`smlib_boolean`, `smlib_boolean_union_all`, `smlib_boolean2d`, `smlib_fillet`,
//! `smlib_chamfer`, `smlib_blend`, `smlib_blend_dropping`, `smlib_blend_setback`, `smlib_blend_setback_open`, `smlib_fillet_variable`, `smlib_chamfer_sides`, `smlib_shell`, `smlib_offset_faces`,
//! `smlib_chamfer`, `smlib_blend`, `smlib_blend_dropping`, `smlib_blend_setback`, `smlib_blend_setback_open`, `smlib_blend_fit_size`, `smlib_fillet_variable`, `smlib_chamfer_sides`, `smlib_shell`, `smlib_offset_faces`,
//! `smlib_thicken`, `smlib_draft`, `smlib_merge_faces`, `smlib_brep_solid_count`, `smlib_imprint`; the fork's `capi/src/smlib_ops.cpp`), declared by
//! hand. Part of the FFI boundary, like its parent: called only through
//! [`crate::ops`].
@@ -31,6 +31,11 @@ pub const BLEND_CURVATURE: i32 = 1;
pub const BLEND_CHORD: i32 = 2;
pub const BLEND_CHAMFER: i32 = 3;
pub const FIT_SIZE: i32 = 0;
pub const FIT_SETBACK: i32 = 1;
pub const FIT_SCALE: i32 = 2;
pub const FIT_SETBACK_AT_LEAST: i32 = 3;
/// Which selection operation: the C functions share one signature.
#[derive(Clone, Copy)]
pub enum Pick {
@@ -95,6 +100,7 @@ unsafe extern "C" {
out: *mut *mut RawBrep,
out_failed_point: *mut i64,
) -> i32;
fn smlib_blend_fit_size(out_size: *mut f64, out_what: *mut i32) -> i32;
fn smlib_blend_dropping(
brep: *const RawBrep,
kind: i32,
@@ -251,6 +257,19 @@ pub fn pick_op(kind: Pick, b: &Handle, points: &[[f64; 3]], tolerance: f64, size
})
}
/// `smlib_blend_fit_size`: the size that builds, and what it sizes (`FIT_*`), for the last blend
/// call SMLib refused as too large or a setback as too short; `None` where it names none.
#[cfg(feature = "native")]
pub fn blend_fit_size() -> Option<(f64, i32)> {
let (mut size, mut what) = (0.0f64, -1i32);
(unsafe { smlib_blend_fit_size(&mut size, &mut what) } == OK).then_some((size, what))
}
#[cfg(not(feature = "native"))]
pub fn blend_fit_size() -> Option<(f64, i32)> {
None
}
#[cfg(feature = "native")]
pub fn thicken(b: &Handle, thickness: f64, symmetric: bool) -> Result<Handle, i32> {
new_brep(|out| unsafe { smlib_thicken(b.raw(), thickness, i32::from(symmetric), out) })
+9
View File
@@ -39,6 +39,9 @@ pub const FACE_NURBS_V_ORDER: i32 = 31;
pub const SOURCE: i32 = 32;
pub const FACE_NURBS_FORM: i32 = 33;
pub const EDGE_NURBS_FORM: i32 = 34;
/// Per face whose kind carries a definition (a ball patch), in face order: how many reals of
/// `FACE_DEFINITION_REALS` are its definition (a ball patch: 4).
pub const FACE_DEFINITION_REAL_COUNT: i32 = 36;
pub const TOLERANCE: i32 = 101;
pub const EXTENT: i32 = 102;
pub const FACE_RANGE: i32 = 103;
@@ -104,6 +107,9 @@ pub const FACE_TORUS_MAJOR_RADIUS: i32 = 162;
pub const FACE_TORUS_MINOR_RADIUS: i32 = 163;
pub const EDGE_TOLERANCE: i32 = 164;
pub const VERTEX_TOLERANCE: i32 = 165;
/// The definitions counted in `FACE_DEFINITION_REAL_COUNT`, packed in face order: a ball patch's
/// sphere's centre x, y, z and radius.
pub const FACE_DEFINITION_REALS: i32 = 166;
/// The largest EDGE_TOLERANCE or VERTEX_TOLERANCE import takes (smlib.h: "at most 1").
pub const MAX_TOLERANCE: f64 = 1.0;
/// `SMLIB_FLAT_MAX_LEN`: no array is longer.
@@ -114,6 +120,9 @@ pub const SURFACE_CYLINDER: u8 = 2;
pub const SURFACE_CONE: u8 = 3;
pub const SURFACE_SPHERE: u8 = 4;
pub const SURFACE_TORUS: u8 = 5;
/// A rational patch on a sphere (SMLib's `SmBallPatch`): its NURBS block, range and trims as a
/// NURBS face's, and its sphere in `FACE_DEFINITION_REALS`.
pub const SURFACE_BALL_PATCH: u8 = 6;
pub const CURVE_NURBS: u8 = 0;
pub const CURVE_LINE: u8 = 1;
pub const CURVE_CIRCLE: u8 = 2;
+22 -6
View File
@@ -186,17 +186,19 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool, bare: &HashSet<(u32,
a.i(flat::FACE_TRIM_TYPE).push(0);
// A face with a NURBS form: what it is (its analytic entry), then the NURBS it is
// parameterised on, whose range and trims these are (FACE_NURBS_FORM). A revolution
// (SMLib has no class for one) is that form alone, a NURBS face.
let form_is_face = f.nurbs.is_some() && matches!(f.surface, b::Surface::PlacedRevolution { .. });
let spline = match &f.nurbs {
Some(n) if form_is_face => surface(&mut a, n, f.domain, analytic)?,
Some(n) => {
// (SMLib has no class for one) is that form alone, a NURBS face. A ball patch is its form
// too, as a NURBS face's block under its own kind, with its sphere as its definition.
let form_is_face = f.nurbs.is_some() && matches!(f.surface, b::Surface::PlacedRevolution { .. } | b::Surface::BallPatch { .. });
let spline = match (&f.nurbs, &f.surface) {
(Some(n), b::Surface::BallPatch { center, radius }) => ball_patch(&mut a, n, *center, *radius, f.domain)?,
(Some(n), _) if form_is_face => surface(&mut a, n, f.domain, analytic)?,
(Some(n), _) => {
surface(&mut a, &f.surface, f.domain, analytic)?;
nurbs_block(&mut a, n)?;
true
}
// SMLib's tube angle counts from the outer equator, the record's from its tube start.
None => match f.surface {
(None, _) => match f.surface {
b::Surface::Torus { tube_start, .. } => {
let [u, v] = f.domain;
surface(&mut a, &f.surface, [u, v.map(|t| t + tube_start)], analytic)?
@@ -337,6 +339,19 @@ fn nurbs_block(a: &mut Arrays, s: &b::Surface) -> R<()> {
Ok(())
}
/// A ball patch's face: its net `n` as a NURBS face's block and range under the ball patch's kind,
/// and its sphere as its definition (SMLib checks the net is on it). True: it carries trims.
fn ball_patch(a: &mut Arrays, n: &b::Surface, center: [f64; 3], radius: f64, domain: [[f64; 2]; 2]) -> R<bool> {
let [[u0, u1], [v0, v1]] = domain;
a.i(flat::FACE_SURFACE_TYPE).push(i64::from(flat::SURFACE_BALL_PATCH));
nurbs_block(a, n)?;
a.r(flat::FACE_RANGE).extend([u0, v0, u1, v1]);
a.i(flat::FACE_DEFINITION_REAL_COUNT).push(4);
a.r3(flat::FACE_DEFINITION_REALS, center);
a.r(flat::FACE_DEFINITION_REALS).push(radius);
Ok(true)
}
/// The face's surface and range; true for a B-spline surface (which carries trims).
fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2], analytic_planes: bool) -> R<bool> {
let [[u0, u1], [v0, v1]] = domain;
@@ -385,6 +400,7 @@ fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2], analytic_plane
return Ok(true);
}
b::Surface::PlacedRevolution { profile, origin, axis, matrix, forward } => revolution(a, profile, *origin, *axis, matrix, *forward, domain)?,
b::Surface::BallPatch { .. } => return Err(Unsupported::Invalid("a ball patch without its NURBS form")),
}
Ok(false)
}
+1 -1
View File
@@ -32,7 +32,7 @@ pub mod trim;
pub use check::{CheckLevel, CheckReport};
pub use measure::{MEASURE_ACCURACY, MassProperties, PointClass};
pub use ops::{Blend, BooleanOp, ChamferSecond, Hole, HoleEdge, MAX_POINTS, MAX_UNION_BREPS};
pub use ops::{Blend, BooleanOp, ChamferSecond, Fit, Hole, HoleEdge, MAX_POINTS, MAX_UNION_BREPS, blend_fit};
/// Why an SMLib call failed.
#[derive(Clone, Debug, PartialEq)]
+29
View File
@@ -8,6 +8,35 @@
use crate::{Brep, Error, Result, error, ffi, init};
/// What size builds a blend SMLib refused ([`blend_fit`]): from the refusal's own cause, never by
/// building again.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Fit {
/// The largest size (a round's radius, a chamfer's distance, a chord's width) that builds.
Size(f64),
/// The largest setback of a set-back corner that fits (the one asked for is too long).
Setback(f64),
/// The factor (< 1) a two-distance chamfer's distances are both scaled by to build.
Scale(f64),
/// The smallest setback that builds the corner (the one asked for is too short: its fill is
/// certified convex only from there).
SetbackAtLeast(f64),
}
/// The size that builds the last blend this process's SMLib refused as too large
/// ([`Error::NoRoom`]) or whose setback it refused as too short; `None` where it names no single
/// size. Read it right after the refused call: SMLib keeps only the last one.
pub fn blend_fit() -> Option<Fit> {
let (size, what) = ffi::ops::blend_fit_size()?;
match what {
ffi::ops::FIT_SIZE => Some(Fit::Size(size)),
ffi::ops::FIT_SETBACK => Some(Fit::Setback(size)),
ffi::ops::FIT_SCALE => Some(Fit::Scale(size)),
ffi::ops::FIT_SETBACK_AT_LEAST => Some(Fit::SetbackAtLeast(size)),
_ => None,
}
}
/// How [`Brep::boolean`] combines two bodies.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BooleanOp {
+21 -17
View File
@@ -286,37 +286,30 @@ fn blends_take_a_size_per_edge() {
assert!(c.blend(Blend::Round, &tops, &[2.0], 0.1).is_err());
}
/// A radius running linearly from 3 to 1 along a cube edge: (1 - pi/4) L (r0² + r0 r1 + r1²) / 3
/// taken away, whichever end is the start.
/// A variable round is the union of its balls: in each plane square to the edge, the corner cut off
/// by rays from it, each meeting the union first at the least entry over the balls, integrated
/// along the edge (a linear law's balls envelop a cone). The balls' great circles, (1 - pi/4) r²
/// per unit length, take more: 18.5988 for 3 -> 1, 39.0796 for 4, 1, 4 and 22.8945 for 4 -> 0.
/// On the 20 cube's edge y = z = 20, 3 -> 1 takes 18.5377834 whichever end is the start.
#[test]
fn a_variable_round_follows_its_law() {
let c = cube(20.0);
let q = 1.0 - std::f64::consts::FRAC_PI_4;
let knots = [0.0, 0.0, 1.0, 1.0];
let r = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &knots, &[3.0, 1.0]).unwrap();
shot(&r, "variable-3-1");
assert_close(volume(&r), 8000.0 - q * 20.0 * 13.0 / 3.0, 1e-5, "linear law");
assert_close(volume(&r), 8000.0 - 18.5377834, 1e-5, "linear law");
let back = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [20.0, 20.0, 20.0], 1, &knots, &[1.0, 3.0]).unwrap();
assert_close(volume(&back), volume(&r), 1e-7, "reversed");
assert!(c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &[0.0, 0.5, 1.0, 1.0], &[3.0, 1.0]).is_err());
assert!(matches!(c.fillet_variable([90.0, 90.0, 90.0], 0.1, [0.0; 3], 1, &knots, &[3.0, 1.0]), Err(Error::NoMatch { .. })));
// A quadratic law with control radii 4, 1, 4: r(t) = 4(1 − t)² + 2t(1 − t) + 4t².
let law = |t: f64| 4.0 * (1.0 - t).powi(2) + 2.0 * t * (1.0 - t) + 4.0 * t * t;
let n = 4000;
let r2 = (0..n).map(|i| law((i as f64 + 0.5) / n as f64).powi(2)).sum::<f64>() / n as f64;
// A quadratic law with control radii 4, 1, 4, r(t) = 4(1 − t)² + 2t(1 − t) + 4t²: 39.019820.
let quad = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 2, &[0.0, 0.0, 0.0, 1.0, 1.0, 1.0], &[4.0, 1.0, 4.0]).unwrap();
shot(&quad, "variable-quadratic");
assert_close(volume(&quad), 8000.0 - q * 20.0 * r2, 1e-5, "quadratic law");
// Down to zero at the far end: the round closes to the edge's end point.
assert_close(volume(&quad), 8000.0 - 39.019820, 1e-5, "quadratic law");
// Down to zero at the far end: the round closes to the edge's end point, 22.594877.
let zero = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &knots, &[4.0, 0.0]).unwrap();
shot(&zero, "variable-to-zero");
assert_close(volume(&zero), 8000.0 - q * 20.0 * 16.0 / 3.0, 1e-5, "down to zero");
// A law with a corner (4 → 2 → 4, straight on both halves): the round turns at the corner,
// each half taking away (1 - pi/4) (L / 2) (16 + 8 + 4) / 3; the same points joined smoothly
// (C1 cubic pieces, as RadiusSpline::through makes them) are built too.
let corner = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &[0.0, 0.0, 0.5, 1.0, 1.0], &[4.0, 2.0, 4.0]).unwrap();
shot(&corner, "variable-corner-4-2-4");
assert_close(volume(&corner), 8000.0 - q * 20.0 * 28.0 / 3.0, 1e-6, "corner law");
assert_close(volume(&zero), 8000.0 - 22.594877, 1e-5, "down to zero");
// A law down to zero inside the edge would pinch the round to a point: refused.
let pinch = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &[0.0, 0.0, 0.5, 1.0, 1.0], &[4.0, 0.0, 4.0]);
assert!(pinch.is_err(), "a law pinching to zero inside the edge");
@@ -325,6 +318,17 @@ fn a_variable_round_follows_its_law() {
shot(&smooth.unwrap(), "variable-smooth-4-2-4");
}
/// A law with a corner (4 → 2 → 4, straight on both halves) on the 20 cube's edge y = z = 20: the
/// union of the balls, with the corner ball's sphere between the halves' touch circles, takes
/// 39.542073 (the halves' great circles would take 40.0590).
#[test]
fn a_variable_round_with_a_corner_in_its_law_is_its_balls_union() {
let c = cube(20.0);
let corner = c.fillet_variable([10.0, 20.0, 20.0], 0.1, [0.0, 20.0, 20.0], 1, &[0.0, 0.0, 0.5, 1.0, 1.0], &[4.0, 2.0, 4.0]).unwrap();
shot(&corner, "variable-corner-4-2-4");
assert_close(volume(&corner), 8000.0 - 39.542073, 1e-5, "corner law");
}
/// Unequal chamfers of a cube's top edge y = 20: 5 back along the face facing `first` most and 2
/// along the other take away a triangle 5 × 2 / 2 along the 20 mm edge, either way round; 5 at
/// 30° to the first face is 5 and 5 tan 30°.
+2 -1
View File
@@ -135,7 +135,8 @@ impl Surface {
let degree = [u_knots.len().checked_sub(nu + 1).ok_or(Error::Argument)?, v_knots.len().checked_sub(nv + 1).ok_or(Error::Argument)?];
Surface::nurbs(degree, nu, nv, u_knots, v_knots, &flat, Some(&weights))
}
S::PlacedRevolution { .. } => Err(Error::Unsupported),
// A ball patch's label has no (u, v): its face's NURBS form is the surface.
S::PlacedRevolution { .. } | S::BallPatch { .. } => Err(Error::Unsupported),
}
}
+15 -1
View File
@@ -279,9 +279,16 @@ pub fn surface(s: &BSurface) -> R<(Surface, Map)> {
(bspline_surface(u_knots, v_knots, points, Some(weights))?, Map::IDENTITY)
}
BSurface::PlacedRevolution { profile, origin, axis, matrix, forward } => revolution(profile, *origin, *axis, matrix, *forward)?,
BSurface::BallPatch { .. } => return Err("a ball patch's label (its face is written as its NURBS form)".into()),
})
}
/// Whether a face is written as its NURBS form, pcurves and all: a ball patch, which STEP has no
/// entity for, is the B-spline surface it is parameterised on.
fn as_form(face: &Face) -> bool {
face.nurbs.is_some() && matches!(face.surface, BSurface::BallPatch { .. })
}
/// A record pcurve in the STEP surface's parameters: its control points through `map` (a linear
/// map, exact on a B-spline and on a NURBS's homogeneous coefficients).
pub fn pcurve(c: &Curve2, map: Map) -> R<Curve> {
@@ -455,6 +462,13 @@ impl BrepWriter<'_, '_, '_> {
return Ok(s.clone());
}
let face = face_of(self.b, f)?;
if as_form(face) {
let s = (Arc::new(surface(face.parameters()).map_err(|m| format!("face {f}: {m}"))?.0), Map::IDENTITY);
if let Some(slot) = self.surfaces.get_mut(f as usize) {
*slot = Some(s.clone());
}
return Ok(s);
}
let (s, map) = surface(&face.surface).map_err(|m| format!("face {f}: {m}"))?;
// A face on its NURBS form is written as the analytic surface it is, with no pcurves
// (STEP's are in the angle); only which way its normal points carries over.
@@ -511,7 +525,7 @@ impl BrepWriter<'_, '_, '_> {
let mut associated = Vec::with_capacity(edge.uses.len());
for u in &edge.uses {
let face = face_of(b, u.face)?;
if face.nurbs.is_some() {
if face.nurbs.is_some() && !as_form(face) {
continue;
}
let Some(Loop::Edges { edges }) = face.loops.get(u.r#loop as usize) else {
+10 -4
View File
@@ -2665,7 +2665,7 @@ fn error_rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui: &mut
#[derive(Debug, PartialEq)]
pub(crate) enum FilletRefusal {
/// In these plain words.
Plain(&'static str),
Plain(String),
/// Not again: the Setback field already flags the value and names the least it takes.
InField,
}
@@ -2680,6 +2680,12 @@ pub(crate) fn fillet_plain_error(error: &str, setback_in_field: bool) -> Option<
if setback_in_field && error.contains("the setback must be more than") {
return Some(FilletRefusal::InField);
}
// A setback too short for its corner's patch: the least that builds, as the kernel names it
if let Some((_, rest)) = error.split_once(solidcraft_engine::kernel::SETBACK_AT_LEAST)
&& let Some(least) = rest.split_once(" mm").map(|(n, _)| n.trim()).filter(|n| n.parse::<f64>().is_ok())
{
return Some(FilletRefusal::Plain(crate::i18n::format("ui.fillet_error_setback_short", &[("least", &format!("{least} mm"))])));
}
let too_large = error.contains("larger than the faces beside its edges allow");
let corner = error.contains("setback") || error.contains("G2 corner") || error.contains("corner patch");
let kernel = error.contains("SMLib")
@@ -2688,11 +2694,11 @@ pub(crate) fn fillet_plain_error(error: &str, setback_in_field: bool) -> Option<
|| error.contains("kernel internal error")
|| error.contains("the rounds would leave openings");
if too_large {
Some(FilletRefusal::Plain(tr("ui.fillet_error_too_large")))
Some(FilletRefusal::Plain(tr("ui.fillet_error_too_large").to_string()))
} else if corner && kernel {
Some(FilletRefusal::Plain(tr("ui.fillet_error_corner")))
Some(FilletRefusal::Plain(tr("ui.fillet_error_corner").to_string()))
} else if kernel {
Some(FilletRefusal::Plain(tr("ui.fillet_error_failed")))
Some(FilletRefusal::Plain(tr("ui.fillet_error_failed").to_string()))
} else {
None
}
+20
View File
@@ -1416,6 +1416,26 @@ mod tests {
/// A smooth-patch fillet reopens with its corner and setback, and OK writes them as saved; a
/// setback list the dialog cannot show as one distance is kept as it is.
/// A setback too short for SMLib's corner fill (past the least the field flags) is refused by
/// the kernel naming the smallest that builds; the dialog says it in plain words, that length
/// in them.
#[test]
fn a_setback_too_short_for_the_corner_says_the_least_that_builds() {
let refused = format!(
"invalid parameters for `solid.fillet`: the setback is too short for its corner: {}5.9620 mm (the corner's patch is convex only from there)",
solidcraft_engine::kernel::SETBACK_AT_LEAST
);
match crate::dialogs::fillet_plain_error(&refused, true) {
Some(crate::dialogs::FilletRefusal::Plain(plain)) => assert!(plain.contains("5.9620 mm"), "{plain}"),
other => panic!("{other:?}"),
}
// A blend too large for its faces keeps its own words.
assert_eq!(
crate::dialogs::fillet_plain_error("the fillet is larger than the faces beside its edges allow", true),
Some(crate::dialogs::FilletRefusal::Plain(crate::i18n::tr("ui.fillet_error_too_large").to_string()))
);
}
#[test]
fn a_saved_smooth_patch_reopens_with_its_setback() {
for (setbacks, field) in [
+10
View File
@@ -139,6 +139,7 @@ Natural normals are ∂S/∂u × ∂S/∂v.
| `cone` {frame, radius_at_origin, half_angle} | origin + v z + r(v) (cos u x + sin u y), r(v) = radius_at_origin + v tan(half_angle) | angle, periodic | height, on the side of the apex where r(v) ≥ 0 (a negative half angle narrows along z) |
| `sphere` {frame, radius} | origin + r (cos v (cos u x + sin u y) + sin v z) | longitude, periodic | latitude, [−π/2, π/2] |
| `torus` {frame, major_radius, minor_radius} | origin + (R + r cos v)(cos u x + sin u y) + r sin v z | longitude, periodic | meridian angle, periodic |
| `ball_patch` {center, radius} | its `nurbs`: a label with no (u, v) of its own | its form's | its form's |
| `spline` {u_knots, v_knots, points} | tensor-product B-spline, `points[i][j]` with i along u | u knot domain | v knot domain |
| `rational_spline` {u_knots, v_knots, homogeneous} | the same, homogeneous coefficients | u knot domain | v knot domain |
| `placed_revolution` {profile, origin, axis, matrix, forward} | M · R(angle) · profile(s): the profile revolved about origin and unit axis by the right-hand rule, then mapped by the column-major affine matrix M | `forward`: s, the profile's parameter; else the angle | `forward`: the angle, [0, 2π); else s |
@@ -159,6 +160,15 @@ only says what the face is (axis, radius). Pcurves on such a face are SMLib's
own trims, with nothing fitted. A STEP writer writes the analytic surface and
the face's edges without pcurves.
A `ball_patch` face always carries `nurbs`: the rational patch on the sphere of
`center` and `radius` that SMLib makes at the corner of three equal rounds
(`SmBallPatch`, flat surface kind 6). The patch is its own net, not a piece of
SMLib's sphere NURBS, so it goes back into SMLib as kind 6 with that sphere as
its definition, and SMLib takes it only when every point of the net is on the
sphere. The sphere is kept because its offsets are exact and collapse at its
centre. A STEP writer, with no entity for it, writes the net as the face's
B-spline surface, pcurves included.
### Parameter ranges
An edge interval and each side of a face domain are finite with min < max. In a
+1
View File
@@ -14,6 +14,7 @@ description: "Use when a SolidCraft change is slow, blocks the window, runs long
## Measure before you claim a speed-up
- Profile first. Time with `solidcraft-cli` from `main` and from your branch, alternating runs, after the load is low. On a shared machine check `cut -d' ' -f1 /proc/loadavg` and wait while it is over 24; build with `nice`.
- Report medians with the range, the machine, and the cases. The pattern to follow is `docs/perf.md`: what was slow, why, the result, and proof that nothing changed (same bodies, volumes, areas and triangle counts).
- A speed-up never skips work on a mesh's box: decide with the exact bounds (`Body::exact_bounds`, grown by the record's tolerance as `apart` in `crates/kernel/src/ops.rs` does). A mesh is for display, export, previews and mesh bodies.
- Do not rewrite kernel code for speed: the kernel is being replaced, and kernel work is paused. Report a slow case with its file under the `kernel-perf` label (CONTRIBUTING.md, "Where help is wanted most").
- Pull requests run only the tests for what they touch; geometry-heavy tests run in the merge queue. Use `cargo xtask test` with the `ci` profile locally (`skills/solidcraft-tests/SKILL.md`).
+18 -4
View File
@@ -7,13 +7,27 @@
# directory each (`gh run download <run> -R tangency-labs/solidcraft -n <artifact> -D <dir>/<artifact>`
# for junit and junit-gpu-1..4), run `cargo xtask known-failing --junit <dir>`, then drop each
# FIXED line and add each REGRESSION line with its cause, from the test's failure message.
# Baseline: develop 38fd4a4f6 (CI run 41157648345, x86_64 Linux), after the SMLib eaa998a pin.
solidcraft-cli::action_matrix::action_matrix_sample 3 baseline actions fail on SMLib: 2 shells (Bad Loop Orient on issue-616-lid-ribs; status 1001 on filleted-shelled-box, shelled again open at an inner wall), 1 fillet (SmSurfOfRevolution Test #7 on oblique_cut_of_cylinder)
# Baseline: develop 2bf58241f (CI run 41522001173, x86_64 Linux), after the SMLib a18bbe5 pin (v1.1.0-alpha).
solidcraft-cli::action_matrix::action_matrix_sample 2 baseline actions fail on SMLib: 2 shells (Bad Loop Orient on issue-616-lid-ribs; status 1001 on filleted-shelled-box, shelled again open at an inner wall)
solidcraft-cli::naming_corpus::corpus_designs_name_alike_every_build_and_fail_where_a_reference_is_gone oracle/83-boundary-loop-thicken does not load: step 8, SMLib thicken status 1001
solidcraft-doc::eval::chain_tests::a_propagated_chain_that_cannot_build_says_how_to_blend_the_edge_alone the propagated chain now builds a round (9 faces), so there is no refusal to read, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
solidcraft-engine::tests::fillet_fit_taper_closes_to_a_point SMLib fillet status 1001 on the taper fit (radius 16 closing to 0)
solidcraft-engine::tests::robustness::fillet_into_an_earlier_round R2 into the earlier round: volume 35923.387 against 35919.524, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
solidcraft-kernel::blend::corner_tests::a_round_ending_in_an_earlier_round_matches_both_together SMLib fillet status 1001 on the round that ends in the earlier one
solidcraft-kernel::blend::corner_tests::corner_blends_hold_at_any_scale at 0.01x the arc case's rounds remove 72.783 (scaled back) against 72.713 at 1x (9.6e-4 off, 1e-4 allowed)
solidcraft-kernel::blend::corner_tests::corner_blends_hold_at_any_scale at 0.01x a picked edge runs on tangentially into an unblended edge and SMLib's result is not a closed solid (the smooth-vertex End, tangency-labs/smlib#81)
solidcraft-kernel::blend_corner::tests::a_round_next_to_a_round_of_another_radius the face count is 9, not the expected count, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
solidcraft-kernel::blend_corner::tests::issue2422_vertical_after_separate_bottom_rounds SMLib's check refuses the fillet: edge and vertex tolerance 0.97 against the body's 0.01 (#2422)
solidcraft-kernel::blend_smlib::tests::smooth_edges_and_unfinished_tangent_chains_are_refused_saying_why the smooth edge builds (volume 7957.723) where a refusal is expected, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
solidcraft-kernel::corner_g2::tests::a_concave_corner_takes_the_g2_patch_too the concave G2 corner's curvature jump clear of the corners is 5.5e-2, above the bound, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
solidcraft-kernel::corner_g2::tests::a_long_setback_keeps_the_g2_patch_inside_the_box at set backs 11 to 15 the G2 corner patch bulges 2.6e-6 to 1.33e-3 past the box (the explicit corner is in progress)
solidcraft-kernel::corner_g2::tests::curvature_rounds_meet_at_a_g2_corner the corner loop's fill misses its neighbours' curvature by 1.6e-3 (tolerance 1e-3), on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
solidcraft-kernel::corner_g2::tests::set_back_8_the_g2_corner_lies_between_the_hull_and_the_prisms set back 8: the G2 corner's distance check reads 0.0611, past its bound (volume 47370.642, in range), on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
solidcraft-kernel::corner_g2::tests::the_box_corner_g2_patch_is_a_closed_solid_curvature_continuous_to_its_neighbours the fitted G2 corner patch bulges 1.7e-5 past the box, which SMLib's exact tight box now sees (the explicit G2 corner builds it inside)
solidcraft-kernel::tests::fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole the arc and tab rounds together remove 9.20067 against 9.20420 (3.8e-4 off, 1e-5 allowed)
solidcraft-kernel::issue_851_tests::blend_the_arc_top_edge_and_the_top_front_edge removes 59.148 against 60.122, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
solidcraft-kernel::issue_851_tests::fillet_the_arc_top_edge_and_the_front_upright_at_the_arc removes 31.497 against 32.310, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
solidcraft-kernel::issue_851_tests::fillet_the_top_front_edge_and_the_front_left_upright removes 42.236 against 43.187, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
solidcraft-kernel::issue_851_tests::fillet_the_top_front_edge_and_the_front_upright_at_the_arc removes 41.341 against 42.146, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
solidcraft-kernel::smooth_edges::tests::a_seam_split_in_two_edges_is_drawn_whole the side seam is one edge ([24]), not split in two, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
solidcraft-kernel::tests::fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole the partial selection runs on tangentially into an unblended edge and SMLib's result is not a closed solid (the smooth-vertex End, tangency-labs/smlib#81)
solidcraft-smlib::ops::tests::a_setback_corner_left_open_returns_its_hole the open corner returns holes of 9, 27 and 19 edges, not its one hole; SMLib's open_holes counts a seam edge its own face uses twice as free (fixed for v1.1.0-alpha.1)
solidcraft-smlib::ops::tests::fillet_takes_exactly_the_picked_edges one round on the picked edge gives 9 faces, not the expected count, on SMLib v1.1.0-alpha (a18bbe5), passed on f916a8d; cause being established
+1 -1
View File
@@ -109,7 +109,7 @@ the frozen truck-geometry PRs, a `kernel-paused` PR), for the report's coverage
| `volume_mm3`, `area_mm2` as `[min, max]` | An inclusive range, where analysis only bounds the measure: a cut removes at most its tool's volume, a join adds at most the tool's. `method` derives both ends. |
| `continuity` | `{order, edges}`: every seam `edges` names reaches `order` (`g0` … `g4`), as Continuity Analysis (`inspect.continuity`) measures it: from the faces' exact surfaces, at its default 33 stations a seam and its default tolerances (1 µm, 1°, 0.01 / mm, 0.001 / mm², 0.0001 / mm³), never loosened per case. A failure names the worst seam (lowest order; of those, the one furthest past the next order's tolerance), its faces, the order it reaches and that next order's worst value. `edges`: `blend` (default), every seam of a face a fillet or chamfer made (a round, a chamfer face or a corner patch) with the faces either side of the edge it replaced, and every seam of a corner patch with the rounds it joins: what the blend is built to meet (not where a round runs into an end face, nor where two rounds meet without a patch: that seam is the input's, tangent only where the rounded edges were); `corner`, those of them on a corner patch (a blend made from several replaced edges); or a list of points `[[x,y,z], …]`, the edges nearest them, as Continuity Analysis picks them. `exact: true`: every seam reaches `order` and not the next (a case built to an order proves the measurement reads that order, no more and no less). A constant round meets its faces G1 (curvature jumps by 1 / r), a `curvature` round G2, a chamfer G0; a `corner: g2` patch is built G2 to the rounds and faces round it. |
| `continuity` (older form) | `{near, within_mm, clear_mm, curvature_jump_per_mm}`: over every tangent seam lying wholly within `within_mm` of `near` (a corner's patch and its neighbours), the curvature jump across it (the faces' normal curvatures square to the seam, from their exact surfaces, at 65 stations a seam) is at most `curvature_jump_per_mm` at every station at least `clear_mm` along the seam from a vertex where two of those seams turn (where a neighbour's own G1 seam ends, which no surface is G2 to both sides of). A rolling ball's corner jumps 1 / r everywhere. |
| `surface_types` | The set of kinds the faces' surfaces are, over every body, as the records hold them: `plane`, `cylinder`, `cone`, `sphere`, `torus`, `bspline` (a B-spline or NURBS surface) or `revolution` (a revolved curve). Exact: none missing, none extra. |
| `surface_types` | The set of kinds the faces' surfaces are, over every body, as the records hold them: `plane`, `cylinder`, `cone`, `sphere`, `torus`, `ball_patch` (a corner's patch on a sphere, its NURBS net), `bspline` (a B-spline or NURBS surface) or `revolution` (a revolved curve). Exact: none missing, none extra. |
| `splines` | `[{degree: [p, q], spans?: [m, n]}, …]`: one entry per B-spline face, each matching one face (directions in either order); `spans`, the distinct knot intervals each way, only where the construction fixes them. |
| `boundary_edges` | The edges only one face uses (a sheet's free boundary), over every body, as the records hold them; exact or `[min, max]`. |
| `flat_pattern` | A sheet metal case's flat pattern, from the script's last `sheet.flat_pattern` result: `size_mm` ([w, h] of the outline), `area_mm2` (the outline's area less the cut-outs', relative tolerance as volume) and `bend_lines` (each `[[x, y], [x, y]]`, a bend's centre line in flat coordinates, matched one for one, either way round, and as many as the result has), positions within the box-diagonal tolerance. |
@@ -5,11 +5,11 @@
],
"what": "cut of a box by a swept G2 corner-removal tool along a projected edge chain (left-rear corner; the symmetric front-left chain works)",
"source": "analytic",
"method": "fillet: the documented curvature section, the quintic Bezier on setback s = 5 (controls p+s t1, p+2s/3 t1, p+s/3 t1, p+s/3 t2, p+2s/3 t2, p+s t2), cuts 113/21 * 25/36 = 3.73677 mm^2 off each vertical edge (Green on the Bezier): Vf = 8000 - 4*10*3.73677 = 7850.529. Tool section: the G2 blend between two lines is the degree 5 Hermite Bezier of the lines' own derivatives, controls (0.8,0) (0.8,0.04) (0.8,0.08) (0.08,0.8) (0.04,0.8) (0,0.8); the part of the profile in the material quadrant has area a = 0.360667 and first moment about the wall S = 0.102152. Swept along the corner curve (length 8.21299, total turn pi/2, curvature at most 0.269, so 1 - k*0.8 > 0 and the tube does not fold), it removes a*L - S*pi/2 = 2.8015; along l3, a straight bottom edge next to that corner (10 long on the left, 30 on the rear: the chain's naming fixes which, analysis does not), a*10 = 3.6067 or a*30 = 10.820. V lies between 7850.529 - 13.622 = 7836.907 and 7850.529 - 6.408 = 7844.121, widened by 1e-4 relative for the measurement: a cut that did nothing (7850.53) or a fillet not made falls outside. Box: the 40 x 20 x 10 box's (the fillets and the cut leave every extreme face)",
"method": "fillet: the documented curvature section, the order-2 section at the equal-area thumbweight on setback s = 5 between square planes (cov-fillet-section-curvature-box-edge-r3), cuts (1 - pi/4) * 25 = 5.36504592 mm^2 off each vertical edge (Green's theorem on it): Vf = 8000 - 4*10*5.36504592 = 7785.398. Tool section: the G2 blend between two lines is the degree 5 Hermite Bezier of the lines' own derivatives, controls (0.8,0) (0.8,0.04) (0.8,0.08) (0.08,0.8) (0.04,0.8) (0,0.8); the part of the profile in the material quadrant has area a = 0.360667 and first moment about the wall S = 0.102152. Swept along the corner curve (length 8.21299, total turn pi/2, curvature at most 0.269, so 1 - k*0.8 > 0 and the tube does not fold), it removes a*L - S*pi/2 = 2.8015; along l3, a straight bottom edge next to that corner (10 long on the left, 30 on the rear: the chain's naming fixes which, analysis does not), a*10 = 3.6067 or a*30 = 10.820. V lies between 7785.398 - 13.622 = 7771.776 and 7785.398 - 6.408 = 7778.990, widened by 1e-4 relative for the measurement: a cut that did nothing (7785.40) or a fillet not made falls outside. Box: the 40 x 20 x 10 box's (the fillets and the cut leave every extreme face)",
"body_count": 1,
"volume_mm3": [
7836.1,
7844.9
7771.0,
7779.8
],
"bbox": {
"min": [
@@ -23,4 +23,4 @@
10.0
]
}
}
}
@@ -48,7 +48,7 @@
]
],
"radius": 7,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -48,7 +48,7 @@
]
],
"radius": 8,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -72,7 +72,7 @@
]
],
"distance": 10,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -0,0 +1,14 @@
{
"sources": [
"tangency-labs/solidcraft#1207"
],
"what": "an edge that runs on tangentially into a round left out of the blend (tangent chain off) is rounded on past the round's end arc until it leaves the material",
"source": "analytic",
"method": "cube 20 (V0 = 8000), its top back edge (y=20, z=20) rounded r3: removes 20 (9 - 9 pi/4) = 38.628316. The end face's top edge (x=0, z=20, y 0..17) then rounded r1 alone, tangent_chain false: over y 0..17 it removes 17 (1 - pi/4) = 3.648231; at y=17 the edge goes on tangentially into the r3 round's end arc, and the r1 round's cylinder (axis x=1, z=19) runs on along y through the r3 round, cutting the part of its section under the round's surface z = 17 + sqrt(9 - (y-17)^2) until that surface drops below z=19 at y = 17 + sqrt(5): with t = sqrt(9 - (y-17)^2) - 2, the section's area under it is t - (t sqrt(1-t^2) + asin t)/2, integrated over y 17..17+sqrt(5) (Simpson, 200000 steps) = 0.218236. V = 8000 - 38.628316 - 3.648231 - 0.218236 = 7957.505202",
"outcome": "solid",
"body_count": 1,
"volume_mm3": 7957.505202,
"continuity": {
"order": "g1"
}
}
@@ -0,0 +1,40 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"length": 20,
"width": 20,
"height": 20,
"body_name": "Cube"
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
10,
20,
20
]
],
"radius": 3
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
0,
8,
20
]
],
"radius": 1,
"tangent_chain": false
}
}
]
}
@@ -2,13 +2,14 @@
"sources": [
"tangency-labs/solidcraft#1207"
],
"what": "an edge that runs on tangentially into a round left out of the blend (tangent chain off) is rounded on past the round's end arc until it leaves the material",
"what": "an edge that runs on tangentially into a round's end arc, its tangent chain rounded with it: the r1 round follows the end face's outline over the r3 round and down the back edge",
"source": "analytic",
"method": "cube 20 (V0 = 8000), its top back edge (y=20, z=20) rounded r3: removes 20 (9 - 9 pi/4) = 38.628316. The end face's top edge (x=0, z=20, y 0..17) then rounded r1 alone, tangent_chain false: over y 0..17 it removes 17 (1 - pi/4) = 3.648231; at y=17 the edge goes on tangentially into the r3 round's end arc, and the r1 round's cylinder (axis x=1, z=19) runs on along y through the r3 round, cutting the part of its section under the round's surface z = 17 + sqrt(9 - (y-17)^2) until that surface drops below z=19 at y = 17 + sqrt(5): with t = sqrt(9 - (y-17)^2) - 2, the section's area under it is t - (t sqrt(1-t^2) + asin t)/2, integrated over y 17..17+sqrt(5) (Simpson, 200000 steps) = 0.218236. V = 8000 - 38.628316 - 3.648231 - 0.218236 = 7957.505202",
"method": "cube 20 (V0 = 8000), its top back edge (y=20, z=20) rounded r3: removes 20 (9 - 9 pi/4) = 38.628331. The end face's top edge (x=0, z=20, y 0..17) then rounded r1 with its tangent chain: at y = 17 it runs on tangentially into the r3 round's end arc on x = 0 (convex, a quarter circle about y = z = 17), which runs on at (0, 20, 17) into the back upright (x=0, y=20, z 0..17), so the chain is those three edges; it ends at the front face y = 0 and the bottom z = 0, each square to its edge, so each straight round ends in a plane section. With A = 1 - pi/4 the r1 spandrel and c = r (10 - 3 pi)/(3 (4 - pi)) = 0.223368 its centroid's distance from either face: the top edge and the upright remove 17 A each; along the arc the spandrel, in the half plane through the r3 axis, lies at radius 3 - u from the axis (u its distance from the round's surface), and is turned through the arc's quarter turn: (pi/2) A (3 - c) = 0.935991 (Pappus). V = 8000 - 38.628331 - 34 A - 0.935991 = 7953.139216. Faces: the cube's 6, the r3 round, and the r1 chain's three (cylinder, torus, cylinder): 10.",
"outcome": "solid",
"body_count": 1,
"volume_mm3": 7957.505202,
"volume_mm3": 7953.139216025938,
"continuity": {
"order": "g1"
}
},
"faces": 10
}
@@ -33,7 +33,7 @@
]
],
"radius": 1,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -5,10 +5,10 @@
],
"what": "a curvature-continuous (G2) fillet of size 5 on a straight edge that ends on two slanted planar faces",
"source": "analytic",
"method": "prism over the polygon (0,0),(40,0),(30,20),(10,20), 20 tall: V0 = 600 * 20 = 12000. Fillet type curvature, size 5 on the top front edge (y=0, z=20, length 40): section is the quintic Bezier (control points at thirds of the setback s = 5 on each face, zero curvature at both contacts), area A = 3.73677 between the corner and the curve and first moment of y over it 3.52297. The end faces are the planes x = y/2 and x = 40 - y/2, so the strip at depth y is 40 - y long: removed = 40 A - int y dA = 145.9479. V = 12000 - removed = 11854.0521.",
"method": "prism over the polygon (0,0),(40,0),(30,20),(10,20), 20 tall: V0 = 600 * 20 = 12000. Fillet type curvature, size 5 on the top front edge (y=0, z=20, length 40): the curvature section: the order-2 section at the equal-area thumbweight between the contacts set back s along two planes square to each other (cov-fillet-section-curvature-box-edge-r3), S(t) = f(t) (e + (1 - t) (rho1 - e)) + (1 - f(t)) (e + t (rho2 - e)), f = (1-t)^3 / ((1-t)^3 + t^3), e the corner: it cuts A = 0.214601836603 s^2 off the corner, its first moment about either face M = 0.0474559669443 s^3 (Green's theorem on S), here s = 5: A = 3.85481638 and the first moment of y over it 3.64691620. The end faces are the planes x = y/2 and x = 40 - y/2, so the strip at depth y is 40 - y long: removed = 40 A - int y dA = 150.5457392. V = 12000 - removed = 11849.4543. [the curvature section at the equal-area thumbweight (the default): on the same rails it takes off what the ball's circle does, so between square planes w = -0.5252123889 and it cuts A = (1 - pi/4) s^2 = 0.214601836603 s^2, first moment about either face M = 0.0474559669443 s^3 (Green's theorem on S); V = 11791.330159]",
"body_count": 1,
"volume_mm3": 11854.052069321415,
"volume_mm3": 11791.330159265486,
"continuity": {
"order": "g2"
}
}
}
@@ -34,7 +34,7 @@
]
],
"radius": 5,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -77,7 +77,7 @@
]
],
"distance": 2,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -72,7 +72,7 @@
]
],
"distance": 9.5,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -37,7 +37,7 @@
]
],
"distance": 2,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -113,7 +113,7 @@
]
],
"distance": 2,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -72,7 +72,7 @@
]
],
"distance": 5,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -77,7 +77,7 @@
]
],
"radius": 2,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -72,7 +72,7 @@
]
],
"radius": 2,
"tangent_chain": false
"tangent_chain": true
}
},
{
@@ -86,7 +86,7 @@
]
],
"radius": 2,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -72,7 +72,7 @@
]
],
"radius": 9.5,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -72,7 +72,7 @@
]
],
"radius": 10,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -72,7 +72,7 @@
]
],
"radius": 2,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -77,7 +77,7 @@
]
],
"radius": 2,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -77,7 +77,7 @@
]
],
"radius": 2,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -77,7 +77,7 @@
]
],
"radius": 2,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -3,9 +3,9 @@
"tangency-labs/solidcraft#851",
"tangency-labs/solidcraft#854"
],
"what": "on #851's knife-edge part (tessellation/1263): the top front edge R2, then the front-left upright R2 as a fillet of its own, ending in the first round",
"source": "invariants",
"method": "the rolling ball of R2 fits on every one of these edges and where they meet; the corner between rounds that end in each other has no closed form here: one closed valid solid whose display mesh closes. Each round meets the faces it rolls on G1: graded at its seams' middles (the top front round's at z = 8 on the front and y = -19 on the top; the arc's at rho 23 on the top and rho 21, z = 8 on the wall; the uprights' at their contacts, z = 5). Where a round ends on a face or on another round the seam is a crease by construction and is not graded.",
"what": "on #851's knife-edge part (tessellation/1263): the top front edge R2, then the front-left upright R2 with its tangent chain: up the first round's end arc and along the left top edge",
"source": "analytic",
"method": "plate x 0..41.1667, y -21..0 and the corner x 41.1667..62.1667 less the cylinder r 21 about (62.1667, -21), 10 high (tessellation/1263): V0 = 10 (62.1667 x 21 - pi 21^2/4) = 9591.394099. The top front edge rounded R2 first: no ball of R2 inside the part (centre y >= -19, z <= 8) reaches its spandrel (y in [-21,-19], z in [8,10], outside the circle about (-19, 8)), so it removes the spandrel's material, from x = 0 to the wall x_w(y) = 62.1667 - sqrt(441 - (y+21)^2): Int over the spandrel of x_w(y) = 35.344727 (Simpson in y = -19 - 2 sin t, 200000 steps). Then the front-left upright (x=0, y=-21, z 0..8) with its tangent chain: at z = 8 it runs on tangentially into the first round's end arc on x = 0 (a quarter circle about y = -19, z = 8, convex), which runs on at (0, -19, 10) into the left top edge (x=0, z=10, y -19..0), ending on the back face y = 0, square to it. The three are the corner's other two edges and the arc between: the result is the part with all three edges at (0, -21, 10) rolled by the R2 ball, its corner the ball's eighth about (2, -19, 8) (the arc's round is the torus of tube centre radius 0 about the first round's axis, the ball itself). With A = (4 - pi) the R2 spandrel: the upright removes 8 A and the left top edge 19 A; in the corner cube [0,2] x [-21,-19] x [8,10] the first round left the quarter cylinder 2 pi and the ball leaves its eighth 4 pi/3: 2 pi/3. V = 9591.394099 - 35.344727 - 27 A - 2 pi/3 = 9530.777979. Faces: the part's 6, the first round, and the chain's three (cylinder, sphere, cylinder): 10. Each round meets the faces it rolls on G1: graded at its seams' middles (the top front round's at z = 8 on the front and y = -19 on the top; the uprights' at their contacts, z = 5).",
"body_count": 1,
"watertight": true,
"continuity": {
@@ -32,5 +32,7 @@
5
]
]
}
},
"volume_mm3": 9530.777979288185,
"faces": 10
}
@@ -72,7 +72,7 @@
]
],
"radius": 2,
"tangent_chain": false
"tangent_chain": true
}
},
{
@@ -86,7 +86,7 @@
]
],
"radius": 2,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -72,7 +72,7 @@
]
],
"radius": 2,
"tangent_chain": false
"tangent_chain": true
}
},
{
@@ -86,7 +86,7 @@
]
],
"radius": 2,
"tangent_chain": false
"tangent_chain": true
}
}
]
@@ -4,11 +4,11 @@
],
"what": "a curvature-continuous (G2) fillet of size 6 on one edge of a 20 mm cube",
"source": "analytic",
"method": "type curvature: the section is the quintic Bezier with control points p+s t1, p+(2s/3) t1, p+(s/3) t1, p+(s/3) t2, p+(2s/3) t2, p+s t2 with setback s = radius = 6 (perpendicular faces), as documented for the feature (zero curvature where it meets the faces). The area between the corner and that curve, by Green on the Bezier polygon (200000 points), is 5.38095 mm^2; the edge is 20 long between two planar end faces square to it: V = 8000 - 20*5.38095 = 7892.3810. Faces: 6 + 1 = 7.",
"method": "type curvature: the curvature section: the order-2 section at the equal-area thumbweight between the contacts set back s along two planes square to each other (cov-fillet-section-curvature-box-edge-r3), S(t) = f(t) (e + (1 - t) (rho1 - e)) + (1 - f(t)) (e + t (rho2 - e)), f = (1-t)^3 / ((1-t)^3 + t^3), e the corner: it cuts A = 0.214601836603 s^2 off the corner, its first moment about either face M = 0.0474559669443 s^3 (Green's theorem on S), here s = radius = 6 (perpendicular faces): A = 5.55093559 mm^2; the edge is 20 long between two planar end faces square to it: V = 8000 - 20*5.55093559 = 7888.9813. Faces: 6 + 1 = 7. [the curvature section at the equal-area thumbweight (the default): on the same rails it takes off what the ball's circle does, so between square planes w = -0.5252123889 and it cuts A = (1 - pi/4) s^2 = 0.214601836603 s^2, first moment about either face M = 0.0474559669443 s^3 (Green's theorem on S); V = 7845.486678]",
"body_count": 1,
"volume_mm3": 7892.380952374643,
"volume_mm3": 7845.486677646163,
"faces": 7,
"continuity": {
"order": "g2"
}
}
}
@@ -31,7 +31,7 @@
],
"radius": 5,
"corner": "g2",
"tangent_chain": false
"tangent_chain": true
}
}
]
+26 -1
View File
@@ -3,12 +3,37 @@
# and on a listed case that passes: a fix removes its line in the same pull request.
# Baseline: develop 242bbc89f with the SMLib fixes of 2026-10-29 (integration-overnight + b78bd4a), aarch64 Linux.
1080-fillet-equal-slope-basin-1-mm-rim-loop-4 builds to the analytic volume (rel 5e-9), but the continuity check (g1 at every blend seam) takes in the mitre's creases, G0 by the mitre ruling: the case's continuity seams need the owner's word
1080-fillet-notched-block-top-outline-r8 a blend seam grades G0 where g1 is wanted, and the volume is 10320.299 against 10198.718, on SMLib v1.1.0-alpha (smlib #124, the End at a smooth vertex); passed on f916a8d. Whether the End's G0 there is by design or a defect is being established
1080-fillet-sloped-basin-1-mm-rim-r4-2-edges builds to the analytic volume (rel 6e-9), but the continuity check (g1 at every blend seam) takes in the mitre's creases, G0 by the mitre ruling: the case's continuity seams need the owner's word
1080-fillet-sloped-basin-1-mm-rim-r4-4-edges builds to the analytic volume (rel 3e-9), but the continuity check (g1 at every blend seam) takes in the mitre's creases, G0 by the mitre ruling: the case's continuity seams need the owner's word
1207-fillet-bar-top-edge-after-boss-arc-round-r0-5 SMLib's check refuses the round: internal coincident control points in its end rib on the boss, which starts where the round and the boss are tangent (the boss's intersection marched there in spans down to 6e-8 mm)
1207-fillet-box-end-edge-into-unblended-round-partial-chain a blend seam grades G0 (1 of 5 seams) where g1 is wanted, at the end of the round's contact seam, on SMLib v1.1.0-alpha (smlib #124, the End at a smooth vertex); passed on f916a8d. Whether the End's G0 there is by design or a defect is being established
2893-chamfer-distance-and-angle-past-the-wall-refused refused for another reason: SMLib SM_ERR_INVALID_INPUT, not a check saying the angle runs past the wall
2893-chamfer-two-distances-elliptical-rim SMLib status 1001: the two arcs of the rim take their chamfer offsets each at its own midpoint, where the faces' angle differs (78.5 and 101.5 deg), and their chamfers do not meet at the tangent corners (held for the owner's ruling on unequal chamfers of a varying angle)
3369-fillet-edge-ends-on-tangent-seam two blend seams grade G0 (2 of 4) where g1 is wanted, at the end of the round's contact seam, on SMLib v1.1.0-alpha (smlib #124, the End at a smooth vertex); passed on f916a8d. Whether the End's G0 there is by design or a defect is being established
4782-fillet-bar-edge-wider-than-both-faces SMLib status 1001: a convex round past both its faces is not yet built by subtraction
5158-fillet-adjacent-plate-edges-as-thick-as-plate a blend seam grades G0 where g1 is wanted, and the volume is 1682.854 against 1681.904, on SMLib v1.1.0-alpha (smlib #124, the End at a smooth vertex); passed on f916a8d. Whether the End's G0 there is by design or a defect is being established
5158-fillet-round-just-past-a-10mm-face SMLib status 1001: the round passes 5e-6 under the box's top back edge, inside the body's tolerance (1e-5), and subtracting its removal region maps that edge to two of the region's faces (SmTopologyIntersector); 10.0001-10.04 open
851-fillet-arc-then-top-front SMLib status 1001: the top front edge's tangent chain (the edge, the arc round's end curve on the front face, the upright under it) rounded R2; along the end curve the R2 ball sits on the arc round's own centre circle, so that piece is the ball about (39.2538, -19, 8) touching front, top and wall (a torus of tube-centre radius 0), the three-edge rolling-ball corner; the same piece on a plane end arc builds (851-fillet-top-front-then-front-left-upright)
851-fillet-top-front-then-upright SMLib status 1001: the upright's tangent chain (the upright, the first round's end curve on the wall, the arc's top edge) rounded R2; along the end curve the R2 ball sits on the first round's axis, so that piece is the ball about (39.2538, -19, 8) touching front, top and wall (a torus of tube-centre radius 0), the three-edge rolling-ball corner; the upright alone builds, the upright with the end curve fails the same; the same piece on a plane end arc builds (851-fillet-top-front-then-front-left-upright)
9-fillet-corner-curvature-with-reduce-fit not supported yet: solid.fillet builds corner g2 with the exact fit only
9-fillet-corner-curvature-with-taper-fit not supported yet: solid.fillet builds corner g2 with the exact fit only
cov-fillet-g2-corner-beside-a-cylinder not supported yet: the curvature fillet builds its sections only between planar faces ("a picked edge is not on the face beside it" on the cylinder's arc)
cov-fillet-g2-five-edge-apex-corner refused: the default setback is shorter than where the rounds' contact lines meet (3.5056 mm from the apex); the corner should take a setback that works
cov-fillet-g2-four-edge-apex-corner refused: the default setback is shorter than where the rounds' contact lines meet (3.9027 mm from the apex); the corner should take a setback that works
cov-fillet-g2-mixed-corner not supported yet: SMLib sets back a corner only where its edges are all convex or all concave
cov-fillet-g2-oblique-three-edge-corner the G2 corner patch meets the new order-2 section's rounds only G1 at one face seam (its volume holds); the corner step rebuilds the patch
cov-fillet-g2-pocket-corner the G2 corner patch meets the new order-2 section's rounds only G1 at one face seam (its volume holds); the corner step rebuilds the patch
cov-fillet-turn-swept-boss-base-r1 not supported yet: solid.fillet has no `turn`; the corners are mitred (12508.9676, the Mitre case's volume)
cov-fillet-turn-swept-pocket-sharp-corner-refused-r1 not supported yet: solid.fillet has no `turn`, so the sharp concave corners are mitred, not refused
cov-junction-boss-tilt-20-root-round SMLib fillet status 1001 on a cylinder tilted 20 degrees into the plate (also before the blend rework)
cov-junction-tee-equal-round equal pipes' intersection is two plane ellipses crossing: their exact conic-pair rounds meet at the crossings in a corner the old corner code cannot join (built by the traced rounds before smlib#77); the corner step
cov-junction-y-junction-120-round refused: the faces between the three intersection curves narrow to nothing at the two triple points, so the room check trips; a three-edge concave corner (the corner step)
every-block-cylinder-tangent-concave-arc-r2-partial-chain a blend seam grades G0 where g1 is wanted, at the end of the round's contact seam, on SMLib v1.1.0-alpha (smlib #124, the End at a smooth vertex); passed on f916a8d. Whether the End's G0 there is by design or a defect is being established
every-box-corner-three-edges-r3-curvature-setback-3-5 not supported on SMLib v1.1.0-alpha (smlib #125: setbacks only at the Setback's corners, the old setback paths deleted); passed on f916a8d. Whether this is a refusal that should name a size or a corner the Setback should build is being established
every-fillet-after-fillet-curvature-front-then-side-r3-partial-chain a blend seam grades G0 where g2 is wanted, at the end of the round's contact seam, on SMLib v1.1.0-alpha (smlib #124, the End at a smooth vertex); passed on f916a8d. Whether the End's G0 there is by design or a defect is being established
report-drafted-peg-front-and-concave-rounds-together-r2 not supported yet: SMLib refuses the corner where the drafted front top edge runs tangent into the concave arc (on the cone) as of no kind its blend corners take
report-fillet-curvature-three-edges-at-bar-corner the G2 corner patch meets the new order-2 section's rounds only G1 at one face seam (its volume holds); the corner step rebuilds the patch
report-fillet-curvature-three-edges-at-bar-corner-solver-residues the G2 corner patch meets the new order-2 section's rounds only G1 at one face seam (its volume holds); the corner step rebuilds the patch
report-fillet-curvature-three-edges-at-bar-corner-solver-residues-outside-setback not supported on SMLib v1.1.0-alpha (smlib #125: setbacks only at the Setback's corners, the old setback paths deleted); passed on f916a8d. Whether this is a refusal that should name a size or a corner the Setback should build is being established
report-fillet-g2-corner-of-bar-running-into-cylinder not supported on SMLib v1.1.0-alpha (smlib #125: setbacks only at the Setback's corners, the old setback paths deleted); passed on f916a8d. Whether this is a refusal that should name a size or a corner the Setback should build is being established
report-fillet-g2-corner-of-bar-running-into-cylinder-outside-setback not supported on SMLib v1.1.0-alpha (smlib #125: setbacks only at the Setback's corners, the old setback paths deleted); passed on f916a8d. Whether this is a refusal that should name a size or a corner the Setback should build is being established
@@ -0,0 +1,27 @@
{
"sources": [],
"what": "two coaxial frusta joined, R10 to 6 over z 0..8 and R6 to 1 over z 8..9: the convex circle between their cones rounded r2",
"source": "analytic",
"method": "both cones turn about z: the ball's centre runs on a circle about z, so the round is the ball's arc turned about z: rational, of degrees (2, 2). V0 = the frusta, pi h (R^2 + R r + r^2) / 3; the round removes 2 pi times the first moment about z of its section (the ball R2 tangent to both profile lines): 5.065333649310.",
"body_count": 1,
"volume_mm3": 1681.969921328409,
"tolerance": {
"rel": 1e-06
},
"surface_types": [
"bspline",
"cone",
"plane"
],
"splines": [
{
"degree": [
2,
2
]
}
],
"continuity": {
"order": "g1"
}
}
@@ -0,0 +1,49 @@
{
"commands": [
{
"command": "solid.cone",
"params": {
"radius": 10,
"height": 8,
"top_radius": 6,
"base": [
0,
0,
0
],
"operation": "new",
"body_name": "Top"
}
},
{
"command": "solid.cone",
"params": {
"radius": 6,
"height": 1,
"top_radius": 1,
"base": [
0,
0,
8
],
"operation": "join",
"targets": [
"Top"
]
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
0,
6,
8
]
],
"radius": 2
}
}
]
}
@@ -0,0 +1,34 @@
{
"sources": [],
"what": "a cone (apex (0, 0, 10 sqrt 2), half-angle 45 deg) and a post R10 tilted 20 deg through the origin, both touching the ball R10 about the origin along circles that do not meet, joined: the concave edge where the cone leaves the post rounded r1",
"source": "analytic",
"method": "the offsets by 1 (the post R11, the cone with its apex sqrt 2 higher) both touch the ball R11 about the origin, so they meet in two plane conics (the cone and the post are each the ball's sphere less a multiple of the square of their contact circle's plane, and their difference is two planes); the spine is the conic round the post, and the round, between contact lines that are rational on both quadrics, is a rational B-spline of degrees (6, 2). V0 = the cone and the post less their overlap, about the cone's apex (where the cone gives no flux): the post's wall below the edge and its bottom cap: 21763.823377543333; the round adds 1.884007899126, (1/3) of the flux of the position through the boundary of the material the round adds: the round's surface as the ball's circles about the spine from contact to contact, the strips of the faces between the edge and the contact lines, each written out exactly and integrated by Gauss-Legendre quadrature (24 points a panel), about the apex. The round's faces, each of those degrees: a closed round is two faces (split at the seam of its closed surface).",
"body_count": 1,
"volume_mm3": 21765.70738544246,
"tolerance": {
"rel": 1e-06
},
"surface_types": [
"bspline",
"cone",
"cylinder",
"plane"
],
"splines": [
{
"degree": [
6,
2
]
},
{
"degree": [
6,
2
]
}
],
"continuity": {
"order": "g1"
}
}
@@ -0,0 +1,53 @@
{
"commands": [
{
"command": "solid.cone",
"params": {
"radius": 24.14213562373095,
"height": 24.14213562373095,
"top_radius": 0,
"base": [
0,
0,
-10
],
"operation": "new",
"body_name": "Hill"
}
},
{
"command": "solid.cylinder",
"params": {
"radius": 10,
"height": 30,
"base": [
0,
0,
0
],
"axis": [
0.3420201433256687,
0,
0.9396926207859084
],
"operation": "join",
"targets": [
"Hill"
]
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
1.46857451100787,
10,
4.034875307780737
]
],
"radius": 1
}
}
]
}
@@ -0,0 +1,45 @@
{
"sources": [],
"what": "two pipes touching at one point (R10 about y = 0, z = -10 and R5 about x = 0, z = -5, both touching z = 0 at the origin from below), joined and cut at z = -2: the concave arc of their edge on the +y side, from (4, 6, -2) down through (0, 10, -10) to (-4, 6, -2), rounded r1",
"source": "analytic",
"method": "the offsets by 1 (R11 and R6) touch at (0, 0, 1) as the pipes do at the origin, so they meet in a quartic with a node there, which is rational (the lines through the node meet it once more); the round is a rational B-spline of degrees (12, 2). V0 = the two cut pipes less their overlap, integral of 4 sqrt(25 - (z+5)^2) sqrt(100 - (z+10)^2) over z in [-10, -2]: 13437.742049099792; the round adds 4.961081486747, (1/3) of the flux of the position through the boundary of the material the round adds: the round's surface as the ball's circles about the spine from contact to contact, the strips of the faces between the edge and the contact lines, and its end pieces in the plane z = -2 (bounded by the round's section there), each written out exactly and integrated by Gauss-Legendre quadrature (24 points a panel). The round's faces, each of those degrees: four, as the body's topology splits the edge and the round.",
"body_count": 1,
"volume_mm3": 13442.703130586538,
"tolerance": {
"rel": 1e-06
},
"surface_types": [
"bspline",
"cylinder",
"plane"
],
"splines": [
{
"degree": [
12,
2
]
},
{
"degree": [
12,
2
]
},
{
"degree": [
12,
2
]
},
{
"degree": [
12,
2
]
}
],
"continuity": {
"order": "g1"
}
}
@@ -0,0 +1,71 @@
{
"commands": [
{
"command": "solid.cylinder",
"params": {
"radius": 10,
"height": 40,
"base": [
-20,
0,
-10
],
"axis": "X",
"operation": "new",
"body_name": "Pipes"
}
},
{
"command": "solid.cylinder",
"params": {
"radius": 5,
"height": 40,
"base": [
0,
-20,
-5
],
"axis": "Y",
"operation": "join",
"targets": [
"Pipes"
]
}
},
{
"command": "solid.box",
"params": {
"length": 100,
"width": 100,
"height": 50,
"corner": [
-50,
-50,
-2
],
"operation": "cut",
"targets": [
"Pipes"
]
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
4.207354924039483,
9.732263392975257,
-7.701511529340699
],
[
-4.207354924039483,
9.732263392975257,
-7.701511529340699
]
],
"radius": 1
}
}
]
}
@@ -0,0 +1,33 @@
{
"sources": [],
"what": "a frustum R10 narrowing at 10 deg, cut by the plane z = 15 + 0.3 x: its elliptical top rim rounded r1.5",
"source": "analytic",
"method": "the ball's centre runs on the cone moved 1.5 in (its apex 1.5 / sin 10 deg lower) and on the plane moved 1.5 down: a plane section of a cone, an ellipse (rho = K / (1 + 0.3 tan 10 deg cos phi) about the axis), so the round is a rational B-spline of degrees (4, 2). V0 = 3565.939077141579 (the bottom disc gives no flux about the origin, the cone X.n = 10 sin 10 deg / tan 10 deg over its area, the top X.n = 15 / sqrt 1.09 over its area); the round removes 16.021667699147, (1/3) of the flux of the position through the boundary of the material the round removes: the round's surface as the ball's circles about the spine from contact to contact, the strips of the faces between the edge and the contact lines, each written out exactly and integrated by Gauss-Legendre quadrature (24 points a panel). The round's faces, each of those degrees: a closed round is two faces (split at the seam of its closed surface).",
"body_count": 1,
"volume_mm3": 3549.917409442432,
"tolerance": {
"rel": 1e-06
},
"surface_types": [
"bspline",
"cone",
"plane"
],
"splines": [
{
"degree": [
4,
2
]
},
{
"degree": [
4,
2
]
}
],
"continuity": {
"order": "g1"
}
}
@@ -0,0 +1,53 @@
{
"commands": [
{
"command": "solid.cone",
"params": {
"radius": 10,
"height": 30,
"top_radius": 4.710190578746051,
"base": [
0,
0,
0
],
"operation": "new",
"body_name": "Post"
}
},
{
"command": "solid.cylinder",
"params": {
"radius": 100,
"height": 100,
"base": [
0,
0,
15
],
"axis": [
-0.2873478855663454,
0,
0.9578262852211513
],
"operation": "cut",
"targets": [
"Post"
]
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
5.013316645912501,
5.013316645912501,
16.503994993773752
]
],
"radius": 1.5
}
}
]
}
@@ -0,0 +1,33 @@
{
"sources": [],
"what": "a cylinder R10 cut by the plane z = 20 + x/2: its elliptical top rim rounded r2",
"source": "analytic",
"method": "the ball's centre runs on the cylinder R8 and on the plane moved 2 down, so the spine is their ellipse; the contact lines are the ellipse moved along each face's normal, rational, and the round (the circles of the ball between them, rational quadratic arcs whose weights are rational in the ellipse's parameter) is a rational B-spline of degrees (4, 2). V0 = 2000 pi (the plane's tilt cancels round the axis); the round removes 68.775042921067, (1/3) of the flux of the position through the boundary of the material the round removes: the round's surface as the ball's circles about the spine from contact to contact, the strips of the faces between the edge and the contact lines, each written out exactly and integrated by Gauss-Legendre quadrature (24 points a panel). The round's faces, each of those degrees: a closed round is two faces (split at the seam of its closed surface).",
"body_count": 1,
"volume_mm3": 6214.41026425852,
"tolerance": {
"rel": 1e-06
},
"surface_types": [
"bspline",
"cylinder",
"plane"
],
"splines": [
{
"degree": [
4,
2
]
},
{
"degree": [
4,
2
]
}
],
"continuity": {
"order": "g1"
}
}
@@ -0,0 +1,52 @@
{
"commands": [
{
"command": "solid.cylinder",
"params": {
"radius": 10,
"height": 40,
"base": [
0,
0,
0
],
"operation": "new",
"body_name": "Post"
}
},
{
"command": "solid.cylinder",
"params": {
"radius": 100,
"height": 100,
"base": [
0,
0,
20
],
"axis": [
-0.4472135954999579,
0,
0.8944271909999159
],
"operation": "cut",
"targets": [
"Post"
]
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
7.0710678118654755,
7.0710678118654755,
23.535533905932738
]
],
"radius": 2
}
}
]
}
@@ -0,0 +1,28 @@
{
"sources": [],
"what": "a ball R10 and a frustum about z through its centre (R5 at z = 0 to R3 at z = 20) joined: the concave circle where the cone leaves the ball rounded r3",
"source": "analytic",
"method": "the ball's centre (outside both) runs on the sphere R13 and on the cone moved 3 out, both about z: a circle, so the round is the ball's arc turned about z: rational, of degrees (2, 2). V0 = the ball and the frustum less their overlap (the frustum below the edge at z = 9.1265, the ball's cap above it): 4597.853510151037; the round adds 2 pi times the first moment of its section: 9.688315962175.",
"body_count": 1,
"volume_mm3": 4607.541826113212,
"tolerance": {
"rel": 1e-06
},
"surface_types": [
"bspline",
"cone",
"plane",
"sphere"
],
"splines": [
{
"degree": [
2,
2
]
}
],
"continuity": {
"order": "g1"
}
}
@@ -0,0 +1,47 @@
{
"commands": [
{
"command": "solid.sphere",
"params": {
"radius": 10,
"center": [
0,
0,
0
],
"operation": "new",
"body_name": "Ball"
}
},
{
"command": "solid.cone",
"params": {
"radius": 5,
"height": 20,
"top_radius": 3,
"base": [
0,
0,
0
],
"operation": "join",
"targets": [
"Ball"
]
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
0,
4.087346743853332,
9.12653256146668
]
],
"radius": 3
}
}
]
}
@@ -0,0 +1,33 @@
{
"sources": [],
"what": "a ball R10 cut flat at z = 4: its rim rounded r2",
"source": "analytic",
"method": "the ball's centre runs on the sphere R8 and the plane z = 2: the circle of radius sqrt 60 about z, so the round is the ball's arc turned about z, a rational B-spline of degrees (2, 2). V0 = 4000 pi / 3 - 288 pi; the round removes 2 pi times the first moment about z of its section (Pappus), from contact (sqrt 60, 4) on the plane to (1.25 sqrt 60, 2.5) on the sphere: 22.492758031524. The round's faces, each of those degrees: a closed round is two faces (split at the seam of its closed surface).",
"body_count": 1,
"volume_mm3": 3261.518762521006,
"tolerance": {
"rel": 1e-06
},
"surface_types": [
"bspline",
"plane",
"sphere"
],
"splines": [
{
"degree": [
2,
2
]
},
{
"degree": [
2,
2
]
}
],
"continuity": {
"order": "g1"
}
}
@@ -0,0 +1,51 @@
{
"commands": [
{
"command": "solid.sphere",
"params": {
"radius": 10,
"center": [
0,
0,
0
],
"operation": "new",
"body_name": "Ball"
}
},
{
"command": "solid.cylinder",
"params": {
"radius": 100,
"height": 100,
"base": [
0,
0,
4
],
"axis": [
0,
0,
1
],
"operation": "cut",
"targets": [
"Ball"
]
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
0,
9.16515138991168,
4
]
],
"radius": 2
}
}
]
}
@@ -0,0 +1,9 @@
{
"what": "the corner of a half cylinder's top (the top's arc, its straight edge and the upright seam where the flat face meets the cylinder) filleted 2 as a curvature-continuous (G2) fillet",
"source": "invariants",
"method": "a half disc R10 (the arc from (10, 0) round to (-10, 0) and its diameter) extruded 10; the three edges at (10, 0, 10); the fillet type curvature (G2 sections and G2 corner patches): every seam of a round and of the corner patch with its neighbours G2, one valid body",
"body_count": 1,
"continuity": {
"order": "g2"
}
}
@@ -0,0 +1,78 @@
{
"commands": [
{
"command": "sketch.create",
"params": {
"plane": "XY",
"name": "Half"
}
},
{
"command": "sketch.arc.center_point",
"params": {
"center": [
0,
0
],
"start": [
10,
0
],
"end": [
-10,
0
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
-10,
0
],
[
10,
0
]
]
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"sketch": "Half",
"distance": 10
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
7.0710678118654755,
7.0710678118654755,
10
],
[
0,
0,
10
],
[
10,
0,
5
]
],
"radius": 2,
"type": "curvature"
}
}
]
}
@@ -0,0 +1,9 @@
{
"what": "the 5 slant edges of a 5-sided pyramid filleted 2 as a curvature-continuous (G2) fillet: a convex corner of 5 edges at the apex",
"source": "invariants",
"method": "a regular 5-gon of circumradius 20 lofted to the apex (0, 0, 30); its 5 slant edges; the fillet type curvature (G2 sections and G2 corner patches): every seam of a round and of the corner patch with its neighbours G2, one valid body",
"body_count": 1,
"continuity": {
"order": "g2"
}
}
@@ -0,0 +1,175 @@
{
"commands": [
{
"command": "sketch.create",
"params": {
"plane": "XY",
"name": "Base"
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
1.2246467991473533e-15,
20.0
],
[
-19.02113032590307,
6.18033988749895
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
-19.02113032590307,
6.18033988749895
],
[
-11.755705045849465,
-16.180339887498945
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
-11.755705045849465,
-16.180339887498945
],
[
11.755705045849458,
-16.180339887498953
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
11.755705045849458,
-16.180339887498953
],
[
19.021130325903073,
6.180339887498944
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
19.021130325903073,
6.180339887498944
],
[
1.2246467991473533e-15,
20.0
]
]
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "sketch.create",
"params": {
"plane": {
"origin": [
0,
0,
30
],
"x_dir": [
1,
0,
0
],
"y_dir": [
0,
1,
0
]
},
"name": "Top"
}
},
{
"command": "sketch.point",
"params": {
"point": [
0,
0
],
"id": "apex"
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.loft",
"params": {
"sections": [
{
"sketch": "Base"
},
{
"sketch": "Top",
"point": "apex"
}
]
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
6.123233995736766e-16,
10.0,
15.0
],
[
-9.510565162951535,
3.090169943749475,
15.0
],
[
-5.877852522924733,
-8.090169943749473,
15.0
],
[
5.877852522924729,
-8.090169943749476,
15.0
],
[
9.510565162951536,
3.090169943749472,
15.0
]
],
"radius": 2,
"type": "curvature"
}
}
]
}
@@ -0,0 +1,9 @@
{
"what": "the 4 slant edges of a 4-sided pyramid filleted 2 as a curvature-continuous (G2) fillet: a convex corner of 4 edges at the apex",
"source": "invariants",
"method": "a regular 4-gon of circumradius 20 lofted to the apex (0, 0, 30); its 4 slant edges; the fillet type curvature (G2 sections and G2 corner patches): every seam of a round and of the corner patch with its neighbours G2, one valid body",
"body_count": 1,
"continuity": {
"order": "g2"
}
}
@@ -0,0 +1,155 @@
{
"commands": [
{
"command": "sketch.create",
"params": {
"plane": "XY",
"name": "Base"
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
1.2246467991473533e-15,
20.0
],
[
-20.0,
2.4492935982947065e-15
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
-20.0,
2.4492935982947065e-15
],
[
-3.673940397442059e-15,
-20.0
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
-3.673940397442059e-15,
-20.0
],
[
20.0,
-4.898587196589413e-15
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
20.0,
-4.898587196589413e-15
],
[
1.2246467991473533e-15,
20.0
]
]
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "sketch.create",
"params": {
"plane": {
"origin": [
0,
0,
30
],
"x_dir": [
1,
0,
0
],
"y_dir": [
0,
1,
0
]
},
"name": "Top"
}
},
{
"command": "sketch.point",
"params": {
"point": [
0,
0
],
"id": "apex"
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.loft",
"params": {
"sections": [
{
"sketch": "Base"
},
{
"sketch": "Top",
"point": "apex"
}
]
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
6.123233995736766e-16,
10.0,
15.0
],
[
-10.0,
1.2246467991473533e-15,
15.0
],
[
-1.8369701987210296e-15,
-10.0,
15.0
],
[
10.0,
-2.4492935982947065e-15,
15.0
]
],
"radius": 2,
"type": "curvature"
}
}
]
}
@@ -0,0 +1,9 @@
{
"what": "a step's corner (the concave step edge, the lower box's convex front top edge, the upper box's convex upright edge) filleted 2 as a curvature-continuous (G2) fillet",
"source": "invariants",
"method": "a 30 x 20 x 10 box with a 15 x 20 x 10 box joined on its top at x 0..15; the three edges at (15, 0, 10); the fillet type curvature (G2 sections and G2 corner patches): every seam of a round and of the corner patch with its neighbours G2, one valid body",
"body_count": 1,
"continuity": {
"order": "g2"
}
}
@@ -0,0 +1,56 @@
{
"commands": [
{
"command": "solid.box",
"params": {
"corner": [
0,
0,
0
],
"length": 30,
"width": 20,
"height": 10,
"body_name": "Step"
}
},
{
"command": "solid.box",
"params": {
"corner": [
0,
0,
10
],
"length": 15,
"width": 20,
"height": 10,
"operation": "join"
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
15,
10,
10
],
[
22.5,
0,
10
],
[
15,
0,
15
]
],
"radius": 2,
"type": "curvature"
}
}
]
}
@@ -0,0 +1,9 @@
{
"what": "the three edges at a top corner of a block drafted 15 deg (an oblique convex corner) filleted 2 as a curvature-continuous (G2) fillet",
"source": "invariants",
"method": "a 30 x 20 rectangle extruded 10 with a 15 deg inward taper; the top front edge, the top left edge and the drafted edge between them at the corner near (0, 0, 10); the fillet type curvature (G2 sections and G2 corner patches): every seam of a round and of the corner patch with its neighbours G2, one valid body",
"body_count": 1,
"continuity": {
"order": "g2"
}
}
@@ -0,0 +1,107 @@
{
"commands": [
{
"command": "sketch.create",
"params": {
"plane": "XY",
"name": "Base"
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
0,
0
],
[
30,
0
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
30,
0
],
[
30,
20
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
30,
20
],
[
0,
20
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
0,
20
],
[
0,
0
]
]
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"sketch": "Base",
"distance": 10,
"taper": "-15 deg"
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
15,
2.679491924311227,
10
],
[
2.679491924311227,
10,
10
],
[
1.3397459621556136,
1.3397459621556136,
5
]
],
"radius": 2,
"type": "curvature"
}
}
]
}

Some files were not shown because too many files have changed in this diff Show More