A constant-radius fillet whose setback equals the width of a neighbouring
face was refused ("the blend is larger than the neighbouring faces"), so
r = 3 on the vertical edges of a 6 mm wide box failed while 2.999 worked.
The corner edge is now consumed: the rail ends at its far vertex and a side
face used up at both ends is removed, its remaining edge becoming the rail.
A face used up at one end only, and radii past the limit, are still refused.
Refs #199
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Shelling a body through a face that a convex round meets tangentially, with
the round at least as large as the thickness, puts the cavity wall through
the rim where the round meets the open face. Both shell strategies then fail
and the first one's generic error ("a corner whose faces don't meet the same
way after the move") was reported. The shell now says why and what to change.
Successful shells are unchanged.
Found by fuzz seed 33, which reaches this case now that a disc's two edges can
be rounded to half its thickness.
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: reproduce #261 (Rectangular pattern of a hole cut scales super-linearly)
* Patterns of many hole cuts scale near-linearly (#261)
Two quadratic costs:
- Meshing a face tested every point against every segment of every boundary
loop, so a plate face with hundreds of holes meshed in quadratic time. The
vendored truck-meshalgo inside test now skips loops whose parameter box is
clear of the point (they wind zero times around it).
- A pattern cut each copy in turn, rebuilding and re-checking an ever more
complex target per copy. Copies whose boxes are clear of each other are now
one tool of many pieces (kernel::union_apart, no intersection work), cut once.
512 holes now take ~7 s instead of not finishing. The test's volume tolerance is
widened from 1e-6 relative to 0.1 % of each hole: the measure meshes the bores
and reads each hole ~0.06 % small, on main too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hole-pattern test: a budget CI can meet; coarser apart-box meshes (#261)
CI runs every test binary at once and is about ten times slower than a
machine running this test alone (15 tests passed 60 s in the last run), so the
512-hole pattern (about 6 s alone) hit the 60 s timeout there. The budget now
only has to tell linear from the quadratic hang: 300 s timeout, 120 s budget.
union_apart meshes each copy coarsely (2 % of its size, box grown by twice that).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hole-pattern test checks the scaling, not a wall-clock time (#261)
CI runs every test binary at once: the 512-hole pattern (about 6 s alone, 10 s
CPU single-threaded) took 177 s there, so no fixed time is reliable. The test
now patterns 128 and then 512 holes and requires the second to take under 8x
the first (linear is 4x, measured 3.5-4.2x here; the old quadratic cost was 16x
or more), each with the volume check and a 900 s hang guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: reproduce #268 (Panic from truck-topology lib.rs:186 when joining struts that end on a bore wall)
* Sew refuses edges whose ends weld to one vertex (#268)
The sewer welds vertices on a 1e-6 grid but only rejected edges shorter than
1e-9, so a sliver edge between the two welded its ends into one vertex and
truck-topology panicked ("Two same vertices cannot construct an edge") inside
the guard. Such an edge is now a degenerate-edge error before any truck call,
and the boolean takes its usual fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Fillets along a straight edge between two cylinders: the rolling-ball blend trims curved cap edges to its contacts
The edge where two overlapping extruded arcs meet is a straight line between two cylinders
ending on planar caps. The rolling-ball blend built its sections and surface, but only
straight seams could be shortened to the contacts, so a circular cap edge was refused with
"a curved edge where the blend ends". Curved seams now keep the part of their curve between
the new ends, found from the nearest of a few samples (a search from the far end of a long
arc can settle on the opposite side of the circle).
The ball's surface projections start from each face's last answer instead of a cold search,
falling back to one when the warm answer leaves the surface's parameters or is farther than
the last point: about 4.5x faster per fillet (370 -> 80 ms), so dragging the radius is usable.
Fixes#237
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Curve blend: refuse a closed curved seam where the blend ends
A seam that starts and ends at the same vertex (a full circle) has no
"between" two contacts, so trimming it built a broken edge. Return
"not supported yet" instead. The curved-seam trim moves into
trim_curved_seam so it can be tested directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
- Web: a failed CJK font fetch shows English for this session only; the
saved language preference is kept (CjkFontLoader, persisted on save).
- Web fetches the CJK font only when a CJK language (ja) is selected.
- fonts.rs: directory scans match CJK-specific names only (no "noto" or
"gothic"); CJK stays the last fallback after the Latin UI font.
- Revert unrelated clippy rewrites in component_clone.rs, coplanar.rs,
canvas.rs and issue_report.rs to match main.
- Tests: default English, fallback order, CJK-only matching, fetch only
for CJK, failed fetch keeps the saved language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
certify_nesting runs two planar booleans per pair of boundary loops, but
its work budget counted segments, so a mesh with many small holes could
trigger ~1M booleans. Refuse more than 50 loops (1225 pairs) with a
"too complex to convert" error and test a 400-hole plate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each of these asks the allocator for tens of gigabytes, or runs for hours, from
a file of a few kilobytes to a few megabytes. An allocation failure is an abort,
so no panic guard can report it and the user's unsaved work is gone.
- IGES composite curves (102) name their children by pointer, and a tree whose
every node names the same child twice fans out to 2^30 leaf visits under the
depth cap. Count the entities visited and the segments produced against one
budget, since a node that yields no segment trips no bound on the output, and
refuse a composite curve that contains itself.
- IGES directory entries could all name the same parameter range, each
materialising it as its own string and token list. Keep the total text
proportional to the parameter section the file carries.
- STEP knot multiplicities were expanded into one value each before the knot
count was compared with the control points: 20 million knots of multiplicity
64 built a 10 GB vector only to be rejected. Check the sum first.
- One STEP face could list any number of boundary loops, each cloning its edge
handles into a new wire that the face extent then samples. Cap the loops and
the edges they hold; 100,000 bounds went from 2.1 GB to an immediate error.
Also guard the IGES translation, which was the one importer stage outside every
panic guard, keep the vertex-sewing bucket key clear of the overflow its
neighbour lookup caused for coordinates near f64's range, and count how often a
loop passes each vertex once instead of once per edge.
Fixes#146
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit c32792b71c76d74348ad5002d8d2da870937e58e)
A Join unioned its tools into the first target only, leaving other target
bodies the tool overlapped or touched as separate, interpenetrating bodies.
Each tool now merges with every target it meets (shared material or face
contact, tested on the operands' boundaries) into one body under the first
one's name; absorbed bodies are removed and their threads follow. A tool that
meets no target still joins the first target as a separate lump. Sheet metal
and mesh bodies are never absorbed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
Record ownership transfers in the timeline so earlier Join/Cut membership,
world placement, machining data and styles survive component moves, edits,
independent copying, undo and reopening. Preserve pending positions in history
snapshots and keep constructor transfers ordered during feature redefinition.
Retain shallow sheet pockets and exact planar intersection islands through
unfold/refold, with depth-aware flat previews and DXF export. Store explicit
3D wire coordinate spaces and migrate known legacy provenance without moving
saved points or analytic fits.
Infer unique eligible cylindrical Emboss targets, align Revolve manipulators
with actual extent geometry, and discard obsolete preview errors after input
correction. Certify constructed and immutable imported face styles using exact
source metadata while preserving legacy face names. Upgrade designs to format10
with bounded validation and geometry-preserving legacy migration.
Persist typed sketch dimensions and bounded ordered configuration rules in format 8. Use exact occurrence paths and source identity for editing, movement, appearance, and mass. Preserve sheet material regions through cutting, folding, and exact DXF export; retain nested cavities and material islands.
Validation: cargo xtask ci passed, including 368 engine tests, 178 UI tests, 68 UI scenarios, 136 kernel tests, and the 10-crate wasm check. Actual preview workflows and legacy format-7 origin-corner reopening were inspected.
kernel::sweep swept segment by segment: a line translates the profile along it, an arc turns it. At a sharp corner the next line runs within the plane the profile has reached, so the profile slid sideways in its own plane and the walls folded into an inside-out, zero-volume shell. Paths whose direction changes at a joint (more than 1e-3 rad) are now swept as a polyline by sweep_path, which mitres the corners; arcs become points within 0.01 mm. Smooth paths (lines and tangent arcs) are swept as before. Pipe uses the same path.
Tests: sweep_round_a_sharp_corner_is_mitred sweeps a 2 x 2 square along an L of two 10 mm lines and checks the volume, 80 mm3 (fails on main).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loft's top lies in the pocket floor. truck's boolean cannot take coincident faces, and the exact polygon fallback (planar_boolean) only takes bodies whose faces are all planar, so the hole's cylinder ruled out the only path that handles this cut. planar_boolean now first tries a sliced form: when an axis-aligned slab holds the tool and none of the body's curved faces, the body is split on the slab's planes (planes clear of the body's own faces), the planar middle piece takes the exact boolean, and the pieces outside are glued back on along the cut faces (the middle piece is healed first so its edges match theirs). Pieces in several parts are refused rather than glued into one shell. split_by_plane's half-space code is now ops::half_space, which this uses.
Tests: loft_cut_beside_a_round_hole replays examples/regressions/loft_cut_beside_round_hole.json and checks the exact volume (slab - pocket - prismatoid - hole; fails on main). With the hole made after the loft, the body is the same (volume, area, 19 faces, 42 edges).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>