Commit Graph
412 Commits
Author SHA1 Message Date
Yeonghun LeeandClaude Opus 5.5 316b49cd84 Extrude from a whole face rounded by fillets: join, and cuts whose walls only touch (#2040) (#2041)
* Extrude: a join from a whole face rounded by fillets sweeps the face itself (#2040)

A face's sketch fits an edge that is no line or arc (the quarter-ellipses
where fillets meet a slanted face) as splines a hair off the body's edges,
so a join from the face touched the body along a face it did not match and
failed with "boolean Union: no result" after minutes. When the profile is
a whole planar face of the one target, the join now sweeps the body's own
face, which shares its edges and is sewn on along it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Extrude: a cut into a whole face keeps the walls that cut and moves the others off (#2040)

A cut from a whole planar face sweeps walls from each of its edges. Where
the face beside an edge turns away from the sweep (n_N . -n_P >= 0) the wall
only touches the body along that edge, or lies in the face beside it, and
the boolean failed on it with "no result". The cut now keeps the walls that
go into the body, exactly (the body's own edges), runs the outline round
the others well outside the body, and starts a little out from the face.
A cut whose walls all only touch takes a slab beyond its depth plane.

Not covered: a cutting wall tangent to the face beside a touching one (a
round's wall meeting an end face, as in the issue's part); the boolean
still fails there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Extrude: leave a whole face outlined by lines and arcs to the profile's own tool

Only a face whose outline the projection had to fit needs the body's own face as the tool. A square drawn over a box's top already meets the body exactly, and the swept face lost the names a sketch on the new top attaches to (stacked_face_tests, #489).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Run the whole cut tool from a face under the kernel guard (#2040)

Review: `builder::vertex`, `builder::line`, `Face::try_new` and
`builder::translated` ran outside `crate::guard`; only the sweep was
guarded. `cut_tool` now runs all of it under one guard.

Adds an end-to-end test of a cut into a face the body stands out past
below it (the block's top strip between its rounds): it takes only
what lies over the face.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: the slab cut of whole_face_tool, on a spline prism (#2040)

A prism on a closed spline standing on a wider plate, cut 3 into its whole
top face: the slab takes the face's area times the depth and leaves the
plate alone. A test-only counter asserts the slab was the tool, so the test
cannot pass through the profile's own tool. Shortens the name of the
rounded block's top-face cut test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Kernel: the face's outline as a Vec now that polyline returns a closed chain (#2040)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:41:41 +00:00
Luke JonesandCodex 26c0c19ef7 Preserve imported part names and IGES assembly structure (#1609)
Build on #1426's import components and nested STEP assembly instances.
Replace placeholder solid labels with meaningful part or instance names.
Translate IGES groups and subfigure instances through the existing STEP
component path, preserving nesting and placements and sewing surfaces
within each group. Bound assembly walks and reuse work buffers.

Related to:
- tangency-labs/solidcraft#1386
- tangency-labs/solidcraft#1426

Co-authored-by: Codex <noreply@openai.com>
2026-10-21 00:39:08 +00:00
raykholo 2f43ad5c5b Loft: a circle lofted to a non-star-shaped profile is split once round (#2570) (#3670)
* claim: #2570

* Loft: a circle lofted to a non-star-shaped profile is split once round (#2570)

A circle section was split at the lead section's vertex directions, kept in
the lead's order and forced to increase by whole turns. A non-star-shaped
lead (a C, a comb) turns back on itself, so the circle wound round many
times and closed with a negative arc: self-crossing walls and a huge mesh
(which froze the web build while meshing it).

The circle is now split exactly once round: at the lead's directions when
they go round once in order (clockwise on a plane facing the other way),
otherwise at the lead's perimeter fractions, evenly as the fallback. Every
arc is finite and positive and they total one turn; the unbounded loop is
gone. Section preparation is split out as loft_section_loops so the
resampled sections can be checked.
2026-10-21 00:30:58 +00:00
rlartotandClaude Sonnet 5.5 6a8604c55d Fix lofts through sections turning past 90 degrees (#2847) (#3452)
* claim: #2847

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* loft: keep sections turned past 90 degrees facing the same way (#2847)

The loft oriented each section by the sign of its normal against the first section's, so on planes turning about an axis every section beyond 90 degrees was reversed and the side faces folded. Follow the facing neighbour to neighbour instead.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 00:15:41 +00:00
bherbruckandClaude Opus 5.5 d2188fb7ab IGES import: a B-spline curve over a tiny knot span keeps its faces (#3885) (#3886)
A curve written over a knot span of 1e-7 or less was refused on the STEP side
("knot vector consists single value"), and both faces it bounds were lost. Its
knots are moved and scaled onto (0, 1), which leaves its shape as it is.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:13:28 +00:00
bherbruckandClaude Opus 5.5 438abbcf82 STEP conformance sweep: the cones to a point read right now (#258) (#3901)
cone_point and cone_point_vertex join the sweep now that #3871 reads a cone
bounded only by its base circle up to its point.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:03:46 +00:00
bherbruckandClaude Opus 5.5 94bd8719aa STEP import: a void reads as a cavity whichever way its shell flag points (#3730) (#3872)
A BREP_WITH_VOIDS void's faces face into the cavity, so it encloses volume of
the opposite sign to the outer shell. Its ORIENTED_CLOSED_SHELL flag alone
doesn't tell: SOLIDWORKS writes .F. over a shell already facing into the
cavity, SolidCraft .T., and flipping by the flag added the cavity's volume. A
void enclosing volume of the outer shell's sign is now turned inside out.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:54:54 +00:00
bherbruckandClaude Opus 5.5 fc3dedfd5c STEP import: regression sweep over the step-conformance parts of known shape (#3880)
* STEP import: regression sweep over the step-conformance parts of known shape

Fifteen generated parts from step-conformance (CC0): plain, NURBS, seamed,
placed, inch and far-away cylinders, domes, a grooved shaft, a plate with 100
holes, a 3.6 m disc, a 0.5 mm pin, a B-spline half-torus and a washer. Each
reads back with its exact volume, its box and its face count. The half-torus
on a TOROIDAL_SURFACE and the cones to a point are left out until they read
right.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* STEP conformance sweep: the pin's volume as FRAC_PI_8 (clippy)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:07:47 +00:00
bherbruckandClaude Opus 5.5 5638259737 STEP import: a cone bounded only by its base circle reaches its point (#258) (#3871)
A cone face written with no vertex at its point has the base circle as its only
loop (or that circle and a VERTEX_LOOP at the point, which has no edges). Its
surface was spanned over the circle alone, a sliver, so a solid cone came in as
a flat disc. Such a face is now spanned to the apex, and its circle is joined to
the apex by a seam line out and back, as a cone with a seam is written.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 22:35:12 +00:00
NOWN-DEVandClaude Opus 5.5 090119a5c5 Boolean: a small hole in a large body's face stays a hole when the face is divided (#2579) (#2580)
* Boolean: a small hole in a large body's face stays a hole when the face is divided

A boolean divides faces at a tolerance that follows the larger operand (a few
millimetres on a part a few metres long). A small hole's rim, two half circles
when read from STEP, then became two chords: its polygon in the face's
parameters had no area, and the sign that tells a hole from a face piece came
out at random. The hole became a face of its own, inside out, and the face it
was cut in stayed whole. A box cut round such a hole gave a piece without it,
and the shared part of that piece and a pin in the hole came out negative
("malformed") or the boolean failed.

divide_face now divides a loop whose polygon is flat again, finer (down to a
hundredth of the loop's own size), before reading its sign. Only the polygon
that places the loop changes; loops that have an area are untouched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Kernel: the long-plate hole test in tests.rs; a flat loop's size from its box

Review: the test moves into the kernel's test file (no file of its own),
and finer_if_flat takes the loop's size from the diagonal of the box round
its sample points instead of the largest distance between any two.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac7mP9rthK3WsPpPRvCsF

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 21:56:33 +00:00
raykholo 5ba8c815a6 IGES import: trim curves with reversed pieces no longer drop their faces (#3358) (#3359)
* claim: #3358

* IGES import: trim curves with reversed pieces no longer drop their faces

A curve on a surface (142) gives each trim loop twice: B in the surface's
parameter space and C in model space, running the same way. Some exporters
write C as a composite curve (102) whose pieces are in order but some of
them end to start. The reader passed those pieces on as they were, so the
loop did not close and the STEP reader left the face out ("face boundary is
not a closed loop").

The pieces of C are now turned to run head to tail, starting at S(B(start)),
the point where the parameter-space curve begins on the surface. That keeps
the loop's sense from the file, so holes still run against the outer loop.
A piece whose ends are one vertex at the sewing tolerance keeps its
direction.

On a 9,276-face trimmed-surface car model, 7,887 faces now import instead
of 3,882.
2026-10-20 17:32:48 -04:00
bherbruck 687caad24c IGES import: a revolved arc is a cap, not a whole sphere (#3806) (#3813)
A surface of revolution (120) wrote its arc generatrix as a whole STEP circle, which
turns into a whole sphere twice over when the arc is centred on the axis; the trim could
not pick the cap out of it and the face meshed as the full ball. An arc in a plane through
the axis now comes in as a sphere or torus, any other arc as an exact rational B-spline of
just the arc.
2026-10-20 20:56:35 +00:00
932d8d6b4f Tessellation: trimmed faces of display meshes stay on their surfaces (#1653)
* Tessellation: trimmed faces of display meshes stay on their surfaces

Display meshes triangulate a trimmed face in parameters scaled by the
surface's speed along each, so a strip stretched along one parameter is
not fanned across, and every boundary segment stays a constraint: one that
crosses another is split where they meet. Boolean meshes keep truck's way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* Tessellation: one boundary rule for every mesh; drop the #1648 retry

A boundary segment that crosses one already added is split where they
meet in Boolean meshes too, which keeps a pinched face (#1648) whole
without the snap-and-retry. Faces whose segments all go in mesh as
before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* Robustness goldens: seeds 1, 23, 55, 64 and 86 re-recorded on current main for #1653

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT

* Thread preview test: wait for the stale fit error to clear before the next preview

After switching back from M6x1.3 to M8x1.3, wait_feature_preview stopped at the M6 error still on
screen when the new preview started a few frames later (seen on Linux CI with #1653's meshing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT

* Robustness goldens: the slow seeds 14, 20 and 40 re-recorded for #1653 from the merge queue

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:49:45 +00:00
bherbruckandClaude Opus 5.5 e98e065584 Open saved designs from the model they were saved with (#3053) (#3102)
* WIP: adopt the saved model on open (#3053)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* WIP: tests

* WIP: bench-open opens as Open does

* docs: opening from the saved model

* Tests: an edit after opening evaluates from the feature it changes

* clippy

* Read SOLIDCRAFT_NO_MODEL_CACHE through the env helper

* Open: no picture from the model when the model itself opens; cancel test evaluates

* Saved model: bodies passed through found without writing their record; states list runs of bodies

* Adopted results go into the evaluation cache: an undone edit, or the marker rolled forward, takes them again

* Model cache size guard: the saved model's writer

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:24:34 +00:00
rlartotandClaude Opus 5.5 c64a611818 Boolean: fail an unverifiable intersection instead of keeping what it should trim (#3244) (#3344)
* claim: #3244

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fail an unverifiable intersection instead of keeping what it should trim (#3244)

When every route of the general boolean fails, it falls back on the sampled answer
(one body whole, or nothing) with a "not verified ... that part is ignored" warning,
even when the bodies' facets cross well past their faceting error. For an
intersection that keeps exactly the material it was asked to trim: teeth standing
0.27 mm out of a trimming cylinder came back untrimmed, and `solidcraft-cli run`
exited 0 with a part 16.918 mm in radius.

An intersection whose sampled answer is contradicted by facets crossing by more than
twice the depth the warning reports (6 x the check mesh tolerance, three times the
faceting error of two meshes) now fails, and the model is kept. Cuts and unions keep
the fallback: shelling a body in two pieces relies on it (a cavity tool grazing the
other piece), with the same depth and ignored volume as this case.

Fixes #3244

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Leave an empty unverified intersection as it was (#3244)

Only a doubted intersection that keeps one body whole is refused. An empty
one ("the bodies are apart") stays a result reported unverified: the clearance
and interference checks rely on it, and main's new
a_pin_through_a_d_bore_of_its_size_still_overlaps_the_flat (#2714) expects it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:37:43 +00:00
bherbruckandClaude Opus 5.5 47108fa063 Open: show a design as its bodies are meshed (#3053) (#3178)
* Open: show the design as its bodies are meshed, off the UI thread (#3053)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Open: hold screenshots until the design is meshed; profile lines

* Tests: time-bounded opens

* Open: a Loading parts n/N note while bodies are meshed

* Open: frame the design from outlines while it goes into the window

* Outline bounds: vertices and edges only; test

* Frame watch: the window thread's frame times by open phase (SOLIDCRAFT_PROFILE_FRAMES)

* Open: the picture grows in mesh order and stays once complete; refreshes pace themselves; named frame steps; watchdog test

* Open: outlines measured on the file worker and kept per body

* Open: draw the Home picture under the opened file's own key once meshed

* Format the open wait

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:27:42 +00:00
raykholo ca9ebb44db Sheet metal: refuse a flat pattern that no longer matches the body (#3324) (#3325)
* claim: #3324

* Sheet metal: refuse a flat pattern that no longer matches the body

A feature that is not sheet metal can change a sheet body's material
without its sheet model knowing: a mirror combined into it, an extrude
joined onto it, a fillet or chamfer on a corner. The flat pattern, the
DXF and the bend table then wrote the sheet as it was before, a part
other than the one in the design, with no warning.

The evaluator now notes, per sheet, the first such feature: one that
changes the body's shape while its sheet model stays the same. That
feature warns, and sheet.flat_pattern, sheet.export_dxf (also with
per_body) and sheet.export_bend_table refuse with "not supported yet",
naming it. A sheet metal feature, or machining the sheet keeps as
cut-outs, rebuilds the body from the model: it clears the note and
warns that the change is not kept. Moving the body, splitting its faces
and cosmetic threads leave the flat pattern as it is.

* Sheet metal: a modelled thread is refused in the flat pattern too

Only a cosmetic thread leaves a sheet body's material as it is. A
modelled thread cuts the thread profile into the hole, which the flat
pattern does not have, and a later sheet metal feature dropped it
without a word. It now marks the sheet like a fillet does: it warns,
the flat pattern, DXF and bend table refuse naming it, and the feature
that rebuilds the body says the thread is not kept.
2026-10-20 12:07:24 +00:00
jb69andClaude Opus 5.5 e44dd4ff9e Loft: refuse consecutive sections that lie in one plane (#3412)
A loft through two sketches on the same plane built a flat body of zero
volume and reported success, so the mistake surfaced later, if at all
(a measure of 0, or a boolean that did nothing).

`loft_wires` now checks each pair of consecutive sections: parallel
planes with the second section's centroid in the first's plane are
refused with "loft sections i and i+1 lie in the same plane (no volume
between them)". The tolerance scales with the section's distance from
its plane's origin.

Test: two circles on XY; the loft fails with that message and leaves no
body.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:43:14 +00:00
rlartotandClaude Sonnet 5.5 c5b637600c Chamfer: one pick follows a straight edge's curved tangent chain at any part size (#2672) (#3295)
* claim: #2672

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Chamfer: one pick follows a straight edge's curved tangent chain at any size (#2672)

chamfer_tool built a single-edge cutter for a straight edge even when the
edge runs on tangentially into an arc (a slot side into its end). The
cutter's overshoot (max(2*distance, 1% of the part size)) is probed for
material beyond the edge ends; on a small part the probe hit the arc's
material and the cutter was refused, so the blend followed the whole
chain. Past a bounding-box diagonal of about 200 mm the overshoot cleared
the arc, the cutter was accepted, and only the straight edge was chamfered,
silently.

A straight edge whose smooth chain contains a curved edge no longer gets a
cutter; it takes the chain-following blend, as at small sizes and as a
fillet does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 11:21:32 +00:00
AlF c83446c6c9 Fix issue 3120, import of STEP file not correct (#3208) 2026-10-20 11:07:50 +00:00
bherbruckandClaude Opus 5.5 349a6c6fc4 perf: a copy placed for showing doesn't turn its revolved faces again (#3044) (#3104)
Moving an occurrence places every body under it again, and each placed copy
searched every revolved face for where it sits to turn it away from its seam,
although its source had been turned already. On the Prusa Core One assembly
that search was most of the 12.7 s a move of the top occurrence took. A body
now records that its revolved faces are turned, and world placement
(`place_matrix`) skips the search for such a body. Modelling moves (pattern
copies, hole tools) keep `transform_matrix`, which still turns them in the
copy's own frame: where a face wraps all the way round the turn it finds
depends on those coordinates, and later booleans depend on it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:01:56 +00:00
bherbruckandClaude Opus 5.5 f6cf8ceaad perf: an instanced STEP part's first instance shares its display mesh (#2814) (#3047)
Later instances of a part showed a mesh shared through the first instance,
but the first instance meshed its own, so every repeated part was meshed
twice. The first instance of a part placed more than once now shows the
shared mesh too; a part placed once keeps its own (no second copy).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:01:50 +00:00
SS-ENandClaude Opus 5.5 ac757960dc Boolean retries: a candidate whose edges run far outside the operands is discarded unmeshed (#3020) (#3185)
The retries in boolean_whole_inner mesh every candidate to compare its
volume. A nudged union at the coarse tolerance could come out with a face
on a cone whose trim loop ran metres along the cone's continuation (edges
up to 12.7 m outside a 155 mm part); meshing it took seconds to minutes per
face and once never finished, though its volume (+-1e10 mm3) failed the
check anyway.

check() first samples the candidate's edges (16 per edge) and discards a
candidate with a point further outside the operands' mesh box than 5% of
their size (or a non-finite point): no edge of a union, cut or
intersection can lie there. The reason goes into the new
BooleanDiagnostics::rejected (omitted from JSON when empty). Results of
sound candidates are unchanged.

On the reporting design the PitchTail join drops from 18.7 s to 4.3 s with
the same result; the regression tests synthesize a cone face whose loop
runs off its surface.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:45:27 +00:00
Artem Sushchev ae2ab87b99 Cut: a body with a sealed void no longer comes out empty (#3175)
truck placed a face that no intersection divides (every face of a shell
that meets nothing of the other) by counting the faces a ray crosses,
taking each face's surface as stored. A revolved body's reversed faces
counted the wrong way, and an inward shell (a void, a cut's inverted
tool) was read as a solid: a cut beside a sealed void came out empty,
refused or, with a tool whose bounding box covers the target, taken with
the body gone. Intersections of such bodies came out empty as well.

In an AND the crossings now count as each face is oriented, and an
inward shell holds what lies outside it. An OR keeps truck's count.

The corrected cut let a folded offset through: an arc edge next to a
face it meets tangentially had its ends moved past its middle, and the
arc went the long way round (robustness seed 73's shell). The offset
refuses an arc whose middle crossed its chord, as it does a straight
edge that turned round.

Goldens: seeds 0, 45 and 77 had a cut refused as empty that now takes
what it should; their volumes are checked by hand in the PR.
2026-10-20 10:30:13 +00:00
Mateusz SychandMateuszSychXpl 8a562f2719 Mesh faces whose boundary touches itself within tolerance (#1648) (#1850)
A face pinched to a point, where its boundary touches itself or another of its loops within
TOLERANCE (a sphere touching a box's edge leaves one on the box), had a constraint refused by the
CDT, and the fallback cut across the face: it came out with no triangles or without those at the
pinch. When a constraint is refused, the vendored truck-meshalgo now puts every boundary point
within TOLERANCE of a segment into it (at most 8192) and triangulates the face again. Faces whose
segments all go in mesh as before.

The regression tests live in crates/kernel/src/thin_face_tests.rs, and the #1648 property test in
boolean_props.rs is no longer ignored. Robustness seeds 20 and 55 are recorded with their Linux
values.

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-10-20 08:28:21 +00:00
37a8402cae surface.blend: seam alignment, chain-topology refusal and a dialog (#764) (#2488)
* kernel: a blend runs along chains of several faces and lines closed rims up (#764)

A blend's chain may run along the boundaries of several faces (a tube's rim
along the halves of its wall): each edge continues from its own best-facing
face, read station by station. Before, a chain had to be the edges of one face,
so every rim of a revolved or cylindrical surface was refused.

Two closed chains blend from matching seams: the second chain's start (searched
by arclength, then narrowed) and direction are the ones that put each station
closest to its partner, so rims whose seams sit apart no longer twist. A
picked `seam` point on each chain fixes the start instead. A closed chain
against an open one is refused. Closed chains leave towards each other's
centroid, not a point on the rim.

Stations are read on the edges' own curves, not on the chords of the length
table (they sat up to the chord's sagitta inside a curved edge), and the
reported gap is measured to the curves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* kernel: closed blend chains run round the same way; Hermite strip and seam tests (#764)

The second of two closed chains runs round the same way as the first (by
their area vectors), so a picked seam turns the blend by just the angle
between the seams instead of folding it over. The nearer pairing decides only
when the loops stand almost at right angles.

Tests for the issue's gates: the blend between two offset box rims is the
analytic cubic Hermite strip (control points from the chord) and keeps the
top faces' normal along both seams; picked seams a quarter apart turn the
blend a quarter round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* surface.blend: `seam` picks where each chain's stations start (#764)

A point on each chain, saved with the feature (one pending format change),
moved with the design like `align`, and refused when it is not two points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* surface.blend: a test between two tube rims, their seams picked and saved (#764)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* Blend dialog: edges at each end, continuity and bulge per end, stitch (#764)

SURFACE › CREATE › Blend Surface opens a dialog: the edges the blend leaves
and the ones it arrives at, each end's continuity (G0–G3) and bulge, and
Stitch to both bodies. Edit Feature brings it back and keeps what the dialog
does not show (align and seam points). Labels in every locale catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* ROADMAP: M8 Blend runs along several faces, pairs closed rims and has a dialog (#764)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* kernel: a blend station on the vertex between two support faces is read on the neighbour (#764)

A station where a tube wall's two halves meet sits at a corner of one half's
parameters; with a leave direction that turns round the wall (tubes off each
other's axis) the shape could not be read there and the blend was refused. The
neighbouring chain edge's face holds the same point and reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* blend: review clean-ups (#764)

- ChainLine keeps one list of samples instead of four parallel lists.
- frames_on and boundary_gap take the chain's ChainLine instead of rebuilding it.
- A station is read on the neighbouring face only when it sits on the vertex the two edges
  share, each face once, and each face keeps its own walk.
- The seam rule (a closed chain starts at it, an open one runs from the nearer end) lives in
  ChainLine::start_at_seam; pair_with reuses the start of its both-ways search.
- support_faces projects with curveblend::project; loop_area sits with plane_through.
- surface.blend parses `align` and `seam` through one point_pair; the Blend and Ruled dialogs
  share body_and_edges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 04:26:12 +00:00
Erlis VidalandClaude Opus 5.5 9e15995a74 A cut ending in a face of its target no longer leaves the hole closed by a skin (#2827)
A cut whose tool ends exactly on a face of the target (a hole cut from a pocket's floor up to the
plate's top face) could come out of the general boolean with the target's face whole over the hole
and the tool's end face under it: a skin of no thickness that keeps the hole closed. No volume
shows it and the sampled membership checks see the hole either way, so the result was kept. The
result check now refuses a body with a planar face lying back to back on another of its own planar
faces with nothing behind part of it (`coplanar::has_skin`), and the retries take the cut.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 04:15:37 +00:00
fe3b14e798 refactor: Aabb2 and point-built Aabb3 for every hand-rolled box (#2428)
* Iterate instead of indexing in nine loops

Replace index loops that look up neighbours by position with enumerate,
zip(cycle().skip(1)) for wrap-around pairs and zip(skip(1)) for
consecutive pairs. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* geom: ring_edges for the edges of a closed ring

Every walk over a polygon's edges, the last back to the first, spelled
x.iter().zip(x.iter().cycle().skip(1)); eighteen of them now call
geom::ring_edges. Three redundant take(len) calls go with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* geom: Ring, a closed ring that knows it is closed

A Ring holds items in order around a closed polygon, loop or fan; edges()
walks each with the next and the last with the first, corners() each
with both neighbours. Ring<Vec2> has signed_area and even-odd contains.

Loop2::polyline returns a Ring, as do doc's face_loops and
boundary_loops, kernel's coplanar wire samples, CapLoop's segments,
tess pieces' rings, the gizmo's plane quads and the navigation cube's
facets. Their walks take edges() or corners() instead of wrapping an
index; sketch's text-hole test and doc's poly_area become contains and
signed_area. Loop2::joints pairs each segment with the next.

Replaces the ring_edges free function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* geom: Chain, Ring and Path; bounds for a chain of points

Ring becomes Chain<T, Closed> beside Path = Chain<T, Open>: edges()
closes a ring and stops at a path's end, Path::close makes the ring,
and bounds() (the empty box for no points) serves both. Eight
hand-rolled min/max folds over a loop's polyline call bounds(); doc's
flat_polyline returns a Ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* geom: area and inside on Ring for any planar point

Ring's signed_area and contains take any Planar point (Vec2 or
[f64; 2]). The cam profile in joints_cam is a Ring: its own even-odd
test goes, and its edge walks, gap and area take edges(). face_params'
loop area, the text path's winding, tess pieces' ring area and the
copies in three tests call signed_area or contains; the formulas are
the same term for term.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* doc: sheet's closed outlines go through Ring

dedup takes and returns a Ring (dropping a last point back on the first
is what makes it a ring's), near_outline walks a Ring's edges, and the
copy of it beside fold checks calls it. point_in goes: its callers ask
the Ring. Its formula anchored each crossing on the other end of the
edge, so the two can differ only for a point on an edge to the last
digit; one caller sets such points aside first, the others test a
region's interior point, a cut's midpoint or another outline's corners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* doc: cam tests build their outlines with Ring::from

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* geom: area_vector on Ring<Vec3>; three Newell sums use it

Ring<Vec3>::area_vector is half the sum of a × b round the ring. heal's
loop_area, lip's outer-loop turning and the revolve's loop orientation
summed it by hand with a wrapped index; they take it from the Ring
(the half is exact, and two of them only read its sign).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* geom: Aabb3 from points; boxes built and asked through it

Aabb3 collects from points (skipping non-finite ones, as add does) and
gains inflated, contains and meets. Fifteen hand-rolled min/max folds
over points or boxes in kernel, doc and engine become collect or union,
and their "within tol of the box" tests become inflated(tol).contains.
blend's refused-edge boxes and naming's face index hold an Aabb3.

A NaN point is now outside every box, where the old negated
comparisons let it in; an infinite point no longer stretches a box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* geom: Aabb2, the plane's box; 2D folds and box tuples use it

Aabb2 mirrors Aabb3 (collect, add, union, inflated, contains, meets)
and adds relative, a point's place in the box. sketch's link and
link_loops each had that formula; link_loops' boundaries, prism's
segment and loop boxes, profiles' shape boxes and grid, and ends'
segment box hold an Aabb2 instead of a (low, high) pair. Folds in the
cam, sketch bounds, sheet's perforation grid and the sketch clipboard
collect one.

Boxes are built from the same terms (a round's box keeps c - r - tol).
As with Aabb3, a NaN point is outside every box and an infinite point
no longer stretches one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* kernel: prism's segment boxes are plain Aabb2s

The SegBox alias named a (low, high) pair; Aabb2 says it itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* geom: Chain::bounds is an Aabb2; the boxes #2382 left as pairs follow

Chain::bounds returns an Aabb2. Its callers keep it: sheet's hole and
cut boxes (loop_box), letter boxes, island outlines and line-near-cut
test use meets, contains and inflated; profile's outline overlap and
max_clearance, profiles' box tests and the cam test read its corners.
flat_size and the flat pattern's JSON size are the union of the loops'
boxes (0 × 0 for no loop, where the pair fold gave -∞).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* geom: Aabb3::intersection; hand-grown and hand-met boxes use the box

Aabb3::intersection is the box both hold. ops' four overlap boxes of a
boolean's operands take it, and seven boxes grown by hand (ops' body
boxes, the distance set's bounds, coplanar's face box, eval_more's
padded mesh box, compare's group and region boxes) call inflated with
the same amount on every side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-20 03:44:49 +00:00
Colin Faulkinghamandbherbruck fa3ada1983 claim: #2708 (#2739)
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-20 03:25:05 +00:00
Karl Von Richter 759e22b582 Try exact local cuts after invalid empty numerical results (#1765) (#1858)
Two empty numerical cut attempts stopped the boolean retry sequence before
the exact geometric strategies ran. When the result guard refuses that empty
candidate, the exact strategies (sliced planar, pushed walls, local prism,
sphere lens, seam halves) now try the cut first; if none recovers it, the
original empty candidate and diagnostics are kept, before any displaced
numerical retry.

Rebased onto main as one commit (the branch's own changes, its merges of main
dropped). The only conflict was the new engine regression test next to
main's #1985 STEP export tests in boolean_regression_tests.rs; both are kept.
2026-10-20 03:25:02 +00:00
14b7529465 Loft guide rails (#201) (#3371)
* kernel: loft guide rails (#201)

A loft follows one or two guide rails between its sections: stations are added between each two
sections, each the plain loft there moved so that the point a rail guides lands where the rail
crosses the station's plane — turned about the loft's direction and scaled (or stretched across
to the rail) with one rail, carried, turned and sized between two. The sections and caps stay
exact; the smooth loft runs through sections and stations. Solid and sheet lofts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* doc, engine: solid.loft and surface.loft take guide rails (#201)

`rails` (one or two {sketch, curves}) and `rail_fit` (scale | stretch), stored on Loft and
SurfaceLoft (one pending format note), parsed by the sweep's rail parser (now shared) and built
through the kernel's railed loft. Rails on a loft to a point, or on a surface loft with edge
sections or end continuity, are refused as not supported yet. The rails' sketches are walked as
the feature's references and their components placed like a sweep's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* ui: Rail, Second Rail and Rail Fit in the Loft and Surface Loft dialogs (#201)

The rail inputs and fit row the sweep dialogs have, shared through dialogs_sweep; Edit Feature
brings the rails back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* docs(roadmap): loft guide rails (#201)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* loft rails: review clean-ups (#201)

- Sweep and loft rails share one turn-and-size map (`sweep_path::rail_frame`).
- A station's middle and the loft's direction come from one curve through the sections'
  middles (exact derivative) instead of two more plain-loft evaluations per station; at most
  33 wires, so railed faces stay light.
- Nearest points on a section use `param_on` (kept only where the search comes nearer); the
  size uses `Aabb3`; one B-spline builder; anchors keep their nudge.
- The railed and plain lofts share one path in `loft_with_rails` and `loft_sheet_with_rails`.
- The engine leaves the not-yet-supported refusals to the design's evaluation; both lofts start
  their rails from the first section's first point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

* loft rails: refuse rails on sections with holes, with a test (#201)

Main's lofts now take sections with holes (#2447); rails shape only the
outlines, so a railed loft of rings is refused until the rails carry the
holes too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 03:22:30 +00:00
Rifqi Al Fatihandbherbruck 46a0b25280 STEP import: CJK names read back whole (#2344) (#2347)
Adjacent \X2\...\X0\ runs, as step_out writes one per character, now
decode: the backslash opening the next run was pushed as a literal, so a
name exported as 圆角 came back as 圆\X2\89D2\X0\ (also every linked
component insert, which round-trips through STEP).

STEP and IGES bytes that are not UTF-8 are decoded in their detected
legacy encoding (chardetng + encoding_rs) instead of from_utf8_lossy, so
GBK or Shift_JIS names from vendor files no longer turn into U+FFFD.

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-20 02:32:50 +00:00
bherbruckandClaude Opus 5.5 d0afc31632 Save the evaluated model with every design: exact, stored once, scaled caps (#3053) (#3093)
* Model cache written by default: exact bodies, stored once, scaled caps (#3053)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* bench-open: say why bodies were not kept

* Exact planes and revolutions; budgets for large bodies (#3053)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Native B-rep: rebuild shells as the kernel made them (closed and seam edges) (#3053)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: the model cache and the exact native B-rep (#3053)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* bench-open: time a save without the cache too

* Model cache entries are Deflate-compressed (#3053)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Model cache: one that does not fit the design file is left out, never a failed save (#3053)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 02:31:08 +00:00
Artem Sushchev 80719744cf Union: when the slab past pushed-apart coplanar faces can't be cut off, trim it by editing faces (#2801) 2026-10-20 02:11:23 +00:00
bherbruckandClaude Opus 5.5 adaac4d1b6 perf: STEP import reads and orients distinct parts in parallel (#2814) (#3046)
Converting an assembly went one occurrence at a time: a part's solid was read
from the file and oriented on the calling thread, and only runs of four or
more solids in one representation used the workers. Each step now takes the
next solids across occurrences and parts (up to 64, stopping where a part not
yet oriented comes back), and the workers read and orient the new parts side
by side, each taking the next one when it is done. Later instances still only
place the part their first instance oriented.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 00:26:36 +00:00
e661e6f809 inspect.clearance: pins on one cylinder with their hole are answered or reported in seconds (#3027)
* inspect.clearance: pairs whose bodies did not change are not measured again

Each clearance check measured every close pair again, though between two
checks usually only a few bodies change or move. Results are now kept per pair,
keyed by both bodies' geometry and placement and the check's settings; a pair
whose two bodies are unchanged is taken from that cache. With 40 overlapping
parts, a check after moving one of them took 0.8 s instead of 23 s (741 of 780
pairs reused). Pairs that could not be worked out are tried again each time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* inspect.clearance: boxes and placed keys exactly as before the reuse

The pair boxes are taken over the mesh's triangles with finite corners, as a
distance shape's are, so pruning is unchanged. A placed body's identity now
includes its model body's own frame, so two placements are never mistaken for
one another.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* inspect.clearance: a large body is meshed once for all its pairs

Each pair that touches cuts the larger body down to the pair's region with a
kernel boolean, and that boolean meshed the whole body twice: once for its
checks and once inside the intersection. A sheet with 600 faces paid about
2.5 s per pair for meshes that were the same every time.

A body can now keep its meshes for reuse (Body::keeping_meshes); its booleans
take the meshes from there, and the intersection takes its shell meshes
from there too (truck-shapeops: and_with_history_meshed). Clearance uses such
copies for one check. Results are unchanged to the bit. On a 3 x 2 m sheet
with bends and 300 holes, the clip took 0.23 s per pair instead of 2.6 s, and
24 rivets took 5.6 s instead of 16.5 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* inspect.clearance: pins on one cylinder with their hole are answered or reported in seconds

A pin seated line to line in its hole, on its axis, and with a blend running into its wall
(a fillet under a head) sent the boolean through minutes of retries that ended unchecked; a
pin a hair off its hole's axis was passed as clear. Clearance now asks first whether the pair
holds such a pin (from the faces' surfaces alone). On its axis the boolean gets a few tries,
which answer plain pins at once; a hair off, or when those tries fail, the shared volume is
worked out apart by coaxial_overlap, which now also takes the zero-offset, equal-radius case,
moves the hole's wall when a blend keeps the pin's from moving, and checks its band more
tightly. What neither answers is reported as unchecked, naming the pin.

The kernel gains with_attempt_budget: the booleans run inside it get a number of tries of
the kernel's own intersection between them, the same on every machine.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* inspect.clearance: the pin tests sit beside the other measured pairs, not at the file's end

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* inspect.clearance: the bodies left whole meet their booleans with their meshes kept

A body that meets several others at one size (a bracket with balls pressed into it) was
meshed again for each pair's intersect: only the clipping used the copies that keep their
meshes. The intersect, the pin work and the union check now take those copies too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Near-coaxial overlap: ten tries per intersection, enough for a flush head pulled back

#1984's headed pin made before its plate needs more than eight.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aac7mP9rthK3WsPpPRvCsF

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-20 00:09:50 +00:00
Colin Faulkingham b622a46624 Fix large modeled threads and thin crest reconstruction (#2209)
* Fix large modeled threads and thin crest reconstruction

Helix stations follow the pitch (radial chord error under 1% of it, 48 to
720 stations), and the faceted rebuild retries at the BSP's precision when
the usual weld tolerance collapses a thin crest facet.

Rebased onto main as one commit (the branch's own commits touched UI files
that later moved to #2263 and #2264). Adapted to main's UnitVec3 thread
axis: `*k` in thread_groove and `*cylinder.axis` in the new flange test.

* Fix icon review revisions for same-timestamp edits
2026-10-20 00:06:44 +00:00
fkirill 3ffd8efffc Loft: preserve holes through matching sketch sections (#2435) (#2447)
Rebase the complete PR change onto current main, preserving changes previously stored in merge commits.
2026-10-19 23:57:07 +00:00
33d7a86d50 Extrude: a tapered side drawn in collinear pieces is one face too (#1788) (#2583)
#1949 joined the straight runs of a profile into one side face for straight
extrudes; tapered extrudes (extrude_tapered, extrude_tapered_between) still
sloped each piece as its own face, so a drafted mirrored profile kept the
edge. They now join the runs the same way (NumberedLoop::merged_lines), the
sloped side generated by every piece under it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-19 23:56:46 +00:00
62b49f9f89 Boolean: a pin in a bore of its size is clear at every placement (#2714) (#2715)
* Boolean: a pin in a bore of its own size is clear of it, whatever its facets

A pin whose wall is in other faces than the bore's (turned from a profile: three
faces; the bore two) is faceted at other angles. Where one body's faces end on
the shared cylinder (the pin's wall through the end face of the tube, the pin's
end inside the bore) the facets cross by the facetings' error, so the sampled
shortcut doubted its own "apart" answer, every retry failed on the coincident
walls, and the result came back unverified after seconds: inspect.clearance
left such pairs unchecked ("could not be intersected there").

The shortcut now leaves out crossings that lie on a cylinder both bodies have
faces on (axes and radii within the kernel's same-surface tolerance, where both
faces run). A pin a hair off its hole's axis, or one pushed into a narrower
bore, is still not taken for apart.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Boolean: skip only the shared cylinder's own crossings, not a flat along it

The shortcut left out every crossing near a cylinder both bodies have faces
on, from any face: a pin through a D-bore whose flat is 0.022 mm inside it
came back as contact. Mesh::crosses_except now hands the predicate the two
triangles' faces, and only crossings between the faces on the shared
cylinder, or between one of them and an end face across its axis that keeps
to its body's side of the cylinder (within the facetings' error), are left
out. The D-bore's end face reaches into the cylinder to the flat, so the
pin's wall through it counts again; a flat, key or rib along the axis always
counts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Boolean: a pin in a bore of its size is clear at every placement

The sampled shortcut's check that no facet has material under it on the
wrong side of the other body still caught the facetings of a shared
cylinder. A facet of a pin's end inside the bore (or of the bore part's
end face round the pin) with all three corners on the rim has its middle
just inside the cylinder. Where the other body's chords reach further in
at that angle, the point under it lies inside the other mesh, though the
bodies only touch. Which facets do so depends on the face splits and the
placement, so the same pair read as clear at one placement and as "not
verified" at another.

Such points are no evidence now when they lie on the shared cylinder,
under a facet of one of its end faces: the end faces and band the
crossing check already leaves out. A pin 0.02 mm larger is no shared
cylinder, and its overlap is still found.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-19 23:56:40 +00:00
LoohiandClaude Opus 5.5 f97a84c100 inspect.clearance: measure the 0.2 mm gap on trimmed faces (#2283) (#2337)
* claim: #2283

* inspect.clearance: a point straight past an edge's end no longer passes as on the face (#2283)

A witness point on a face's surface is kept only if it lies on the face
itself; near the face's boundary the nearest edge decides which side it is
on. A point straight past the end of an edge (on the line through it) was
meant to be left to the other edge meeting there, but "straight" was an
absolute 1e-9 of the body size. With a slightly tilted axis (1.5e-6 rad, as
a STEP round trip leaves it) a point 0.2 mm past the end of a knuckle's seam
edge is 4.5e-6 mm off that line, so its side came down to rounding and the
point on the knuckle's extended cylinder, level with the frame band's rim,
was taken as contact (0.000) at some angles.

Straight along is now relative to the distance from the edge (1e-3 of it),
so the rim edge meeting there decides. Regression test: a hinge knuckle
turned 30-80 degrees between two bands reads 0.2 mm at every angle, with
both witness points on the bodies (41 degrees read 0.0 before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:56:34 +00:00
d90ec9586c Delete Face: a face that only loses whole loops keeps its own surface (#1927)
Deleting a pocket or a boss from an imported part failed with "face N cannot
close: cannot attach a plane to a wire that is not on one plane". The face
around the gap only loses an inner loop, but it was rebuilt as a new plane
through its outer boundary, and an imported boundary is often a few 1e-5 mm off
the plane: within the planar test, too far for a plane fitted to it. Such a face
now keeps its surface and only drops the loop.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-19 19:14:09 -04:00
fkirill 54b00fa7b1 Chamfer straight lips at tangent wall junctions (#2408) (#2490)
Thanks @fkirill.
2026-10-19 18:04:24 -04:00
NOWN-DEV 53a9260aeb Interference: near-coaxial pins after a STEP round trip, with flush heads, in either order (#1984)
Interference: near-coaxial pins after a STEP round trip, with flush heads, in either order.
2026-10-19 17:22:56 -04:00
fkirill 983fbe2b14 Fix lip and groove on round objects (#2751) (#2790)
Thanks @fkirill.
2026-10-19 16:34:50 -04:00
02c77df5f7 inspect.clearance: mesh each body once per check instead of once per pair (#2867)
* inspect.clearance: pairs whose bodies did not change are not measured again

Each clearance check measured every close pair again, though between two
checks usually only a few bodies change or move. Results are now kept per pair,
keyed by both bodies' geometry and placement and the check's settings; a pair
whose two bodies are unchanged is taken from that cache. With 40 overlapping
parts, a check after moving one of them took 0.8 s instead of 23 s (741 of 780
pairs reused). Pairs that could not be worked out are tried again each time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* inspect.clearance: boxes and placed keys exactly as before the reuse

The pair boxes are taken over the mesh's triangles with finite corners, as a
distance shape's are, so pruning is unchanged. A placed body's identity now
includes its model body's own frame, so two placements are never mistaken for
one another.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* inspect.clearance: a large body is meshed once for all its pairs

Each pair that touches cuts the larger body down to the pair's region with a
kernel boolean, and that boolean meshed the whole body twice: once for its
checks and once inside the intersection. A sheet with 600 faces paid about
2.5 s per pair for meshes that were the same every time.

A body can now keep its meshes for reuse (Body::keeping_meshes); its booleans
take the meshes from there, and the intersection takes its shell meshes
from there too (truck-shapeops: and_with_history_meshed). Clearance uses such
copies for one check. Results are unchanged to the bit. On a 3 x 2 m sheet
with bends and 300 holes, the clip took 0.23 s per pair instead of 2.6 s, and
24 rivets took 5.6 s instead of 16.5 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-19 16:14:24 -04:00
1999e763ab Tapered extrude: arcs meeting at corners meet where their moved circles cross (#2810) (#2813)
A loop made only of arcs took the all-arcs shortcut in `offset_loop`, which
moves each arc by its radius and keeps its start and sweep angles: exact for
arcs on one circle or meeting tangentially, but where arcs of different
circles meet at a corner (a circle with lobes) the moved arcs no longer
met, and the sides were built through the wrong points without an error.

`arc_corners` moves each such corner to where the two moved circles cross,
nearest the old corner (`circles_cross`, closed form), and cuts both arcs
there. Circles that no longer cross are refused ("not supported yet"), an
arc the taper turns around closes the profile. Between the sections the
corner is a straight ruling, as for every side in `taper_loop`: on the test
profile (2.5 mm at 45°) it stays within 0.015 mm of the exact corner.

Test `tapered_extrude_of_arcs_meeting_at_corners`: the volume against the
integral of the union of the shrunk circles, 18905.42 mm³ against 18907.67
(was 18975.45).

Co-authored-by: Mariusz Pacek <pacekm90@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-19 16:12:52 -04:00
fkirill 42e991069f Fix shelling revolved barrels (#2421) (#2486)
* claim: #2421

* claim: #2421

* Fix shelling circular-meridian barrels (#2421)
2026-10-19 16:09:11 -04:00
c1f9224579 Fillet fit: an outside round concentric with a wall's inside round is no longer refused (#2852)
* Fillet fit: a round counts only what lies between its edge and its arc

The fit check refused a round when any other face of the body reached into
the wedge between its edge and its two setback lines. A round changes only
the part of that wedge between the edge and its arc: a face on the ball's
side of the arc is left as it is. An outside round concentric with the
inside round of a 3 mm wall (R18 over R15) was refused as too large
(largest 14.21 mm) although it keeps the wall 3 mm thick.

The face check now tests a round's fan of prisms from its edge to its arc
(Frame::pieces, already used for blends that cross), after the wedge as a
quick filter. Chamfers keep the wedge, which is what they change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L15TLCjpXsQdp8ovC4ZacH

* Fillet fit: a curved face clears a round's thin end pieces only by a chord (#324)

The fan test shrank each piece by the curved faces' tolerance; the thin pieces at the
arc's ends vanished, so a hole wall 0.3 mm into a round beside it was missed and the
round failed to build instead of offering the radius that fits (issue_324_tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-19 16:08:37 -04:00
Colin Faulkingham e1268c6ad9 Fix fillets around tiny rounded-rectangle end edges (#2506)
Fillets around tiny rounded-rectangle end edges now build (#2498).
2026-10-19 16:08:06 -04:00