A hole drilled square to a flange's panel, not only the base's, is now a flat cut-out of the sheet: it unfolds with its panel to where the bend allowance puts it, shows in the flat pattern and DXF as a circle, and survives unfold and refold. A pocket tool spans the cut-outs already inside it (a counterbore's hole), so the order they are made in no longer matters. Fixes#560.
When an extrude cuts with profiles whose outlines stay more than 1 µm apart (the letters of a text), their tools are apart too, so they are joined and cut from the body in one boolean instead of one per profile. A ten-letter text cut drops from about 13 s to 1.5 s with the same result. Sheet metal targets and touching profiles keep cutting tool by tool. Tapered extrudes are unaffected.
surface.extrude sweeps open curve chains and profile boundaries along the sketch normal into a surface body (walls only, no caps). Saved under format 17.
Refs #757
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds `fit: taper` to Fillet: where a constant radius doesn't fit along a straight edge, it follows the room there as a smooth spline, down to zero where a face closes to a point. Taper to Fit is in the Fillet dialog, and the feature's warning says how far it tapered.
A cut or an intersection between prisms (a plate and the tools through it) is now decided in 2D before either operand is meshed. A body's size and volume are measured once, a placement reads its orientation from its matrix, the evaluation cache is bounded to 64 MiB, and STEP import copies fewer shells. Hole plates evaluate 20-30% faster with less memory, and models are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sheet metal rims and bend ends attach by name (<panel>:rim.<edge>, <flange>:rim.bend0/1), as do face profiles from sheet faces. A plain or counterbored Hole square to a panel is machined into the flat pattern. Boss, Lip, Rest and Snap Fit keep the body's history and name their own faces. Hole tools name their faces by the part they cut, and pattern copies as copies. A pattern's repeated fillets keep the body's history. Loft sides are named by their section curves. A piece Offset Face moved off its face's plane attaches by its own qualified name. Sides of a profile grown to overlap a flush body keep their sketch line's name, so walls a Join merged take a sketch (#681). Face sketch 75% -> 90%, offset plane 75% -> 90%, press-pull/extrude 64% -> 85% in the action matrix; baseline blessed.
Fixes#494, fixes#495, fixes#560, fixes#561, fixes#563, fixes#564, fixes#681.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The loading box was a full-window modal whose backdrop covered the custom title bar. It is now a light overlay under the title bar, so dragging, snapping, resizing and the window buttons keep working during open, import and recovery. The timeline rebuild shows feature N of M, and Cancel or Esc stops it at the next feature (solidcraft_doc::progress). Recovery restore now runs off the window thread too. A control or MCP doc.open or import is answered once the file is in and doesn't block frames; edits sent meanwhile wait their turn. The web build still opens in place.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fillet fit: reduce lowers the radius only where it doesn't fit: picked edges are grouped where they can limit each other (sharing a face or vertex), and each group gets the requested radius where it fits, otherwise the largest that does. Refs #293.
Co-authored-by: Iraklis Kourtis <[email protected]>
A point dimensioned from linked geometry (projected, included or intersected) keeps its side when a parameter moves the link's source past it: the solve reads sides and turns from the stored state instead of from beside the moved line, both on recompute and when editing the sketch.
Co-authored-by: NOWN-DEV <[email protected]>
A Box cut, or a Cylinder cut whose axis is the sheet's normal, machines a sheet metal body like the extrude cut of its footprint: through the sheet it's a cut-out (a cylinder an exact round one in the flat pattern and DXF), part way it's a pocket. Primitives at other angles or across a bend are still refused. Un-ignores the #541 regression test. Fixes#541.
Co-authored-by: jonaswre <[email protected]>
Faces that a sheet fold or unfold moves keep their names: points on a moved panel are taken back through the panel they lie on to where the sheet held them before the feature, and named from the faces there. Un-ignores the #535 regression test. Fixes#535.
Co-authored-by: jonaswre <[email protected]>
An extrude cut or join only extends its tool past an end that lies on a target face, and by less than half the way to the next face level, so a tool can no longer cut through a thin floor beyond an end that touches no face. Un-ignores the #545 and #544 regression tests. Fixes#545.
Co-authored-by: jonaswre <[email protected]>
Construction axes, points and planes resolve their picks against the model as their component sees it (in the root, every placement, pasted instances too), and a Hole picked on another component's body cuts that body. components-arm 355 -> 502/594 and joints-hinge 304/390 -> 371/408 in the action matrix.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A copy added by raising a body pattern's count after a rename is numbered after the highest stored copy, so it no longer takes the number the rename freed. Un-ignores the #372 regression test and adds one for undo/redo and save/reopen. Fixes#372.
Co-authored-by: jonaswre <[email protected]>
solid.move takes copy: true to keep the source bodies and transform a new copy of each, with a Create Copy box in the Move dialog. Designs without it save and load exactly as before.
Display meshes also bound the angle one facet turns, so bends and small holes on a large body are drawn round instead of as chamfers or squares. Bounded per face, and only the display mesh changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Text outlines count as drawn geometry when picking profiles, and "all profiles" leaves out the counters of letters (the holes of A, B, 8), so text extrudes as letters with their holes whether or not other curves are drawn next to it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When a body that was already cut into pieces is cut again, the new pieces are numbered past the names already taken ("Bar (2)"), instead of reusing "Bar (1)".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A hole positioned on a face now follows that face along the hole's own axis instead of sliding to the face's nearest point. When the face no longer reaches the position, the hole stays where it was placed and the feature says so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sketch on a flat face of a sheet metal body was refused ("this face has no exact planar
source"), so the only way was a plane frozen in space. When the sheet changes (a flange's
angle, the thickness), the frozen sketch stays behind: a cut from it fails on the slanted
flange or leaves a blind pocket in the thicker sheet.
`sketch.create` with a face of a sheet metal body now attaches the sketch to the panel the
face belongs to. It keeps the face's name, `sheet:<body>:<panel>:<top|bottom>` (the panel is
the base or a flange's feature tag), and takes the panel's exact plane from its flat region
and fold each time the model is evaluated, so the sketch follows the sheet through parameter
changes, unfold and refold. Its coordinates are the flat pattern's (the bottom face's y
mirrored). A face that is gone is reported as a lost attachment, never replaced by the
nearest face. The attachment follows a renamed or copied body, shows as `sheet_face` in the
command result and as its own status in Redefine Sketch Plane. `fixed: true` still freezes.
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Sketch on a cylinder primitive's caps (#420)
A solid.cylinder's caps carried no construction certificate, so a sketch
on its top or bottom was refused unless fixed:true (#420's crossed
cylinders, built from primitives).
- kernel: FaceSource::CylinderCap { producer, high, plane };
Body::with_cylinder_certificate turns a cylinder's two extrude caps into
certified caps. kernel::cylinder's output is unchanged for its other
callers.
- doc: FaceCertificate::CylinderCap { high }, named F<id>:top / F<id>:bottom
as the cylinder's face names already are; New Body and Join cylinders
are certified, shells carry it as their planar source.
- format 15: designs can store the new certificate kind; version 14
designs need no change, and older builds ask for an update instead of
meeting an unknown kind.
Tests: top and bottom attachments follow a height and base edit and a
reopen; crossed joined cylinders keep both caps attachable; a shelled
cylinder's floor keeps its cap identity; malformed certificate JSON is
refused.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb
* Format 15 also covers sketch_axis links (#448)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
A Union dropped every construction certificate, so after any join no face
could take a durable sketch: sketch.create failed with "this face has no
exact planar source" even on a face the join never touched (#420, and the
"CREATE SKETCH: operation failed" reports #344, #195).
- kernel: map planar sources through a Union from both operands, with the
same exact-support rule Cut uses. Coplanar supports of the two operands
carry distinct identities, so a face merged from both stays without one.
- doc: Join extrude and Join box tools are certified like new bodies; a
cap moved off its designed height (into the target, or past a curved To
face) is not certified. Join producers are accepted when certifying.
Tests: both operands' faces attach and follow an upstream edit and a
reopen; a merged coplanar face is refused and fixed:true works; a face a
join covers whole is lost without moving to the new roof. Tests that used
a joined face as their "uncertified" example now use a merged face; the
trimmed-face case in a_sketch_on_a_face_stays_on_that_face now stays
attached to the surviving piece.
Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix: fillet fit review follow-ups
From the review of #388 (merged before these landed):
- The Fillet dialog's Applied Radius showed nothing when editing a fillet
that was already reduced: the preview found the radius by diffing the
states before and after. The engine's preview now keeps the command
replies, and the dialog reads the radius of the feature they name.
- Pattern copies of a reduced fillet try the radius the original was
reduced to first, instead of each searching again (up to 16 fillets).
- fit: reduce with a chord or variable radius is refused instead of being
silently ignored.
- One helper appends a note to a feature's warning; too_large is imported
like unsupported in the loop and curve blends.
Refs #293
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44
* Use the solidcraft_doc crate name after the rename
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HWLPLy7ug7URSK5WybjRNa
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bherbruck <[email protected]>
object_height, curved_target and curved_target_any took the first solid body with any face
within its pick tolerance of the To Object point. That tolerance is 5 % of the body's size,
so a large body beside a small target took picks made on the target: a ridge extruded up
to a post 2 mm above a plate treated the plate as the target, nothing trimmed the tool, and
it ran past the post (to x 9.08 instead of -2.31).
They now share face_at: the nearest face over every solid body, still within that body's
tolerance. appearance gains nearest_face (the face and its distance) and pick_tolerance,
which face_index_at now uses as well.
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix(doc): extrude and revolve in a moved component from another component's sketch
A feature built from a sketch of another component read the sketch's plane as if it were in
the feature's own component. When the two components are placed differently (a moved or
jointed part), the tool landed transformed along with the feature's component: a join on a
part turned half a turn about Z, from a root sketch, came out on the far side of the origin.
Extrude and Revolve now see the sketch through both placements, as sweeps along 3D paths and
sketches on another component's face already do (sketch_in_feature_frame, using
profile_in_frame). The sheet-metal rotated-frames test built its cap witness from a sketch of
the moved component while the root was active, relying on the old reading; it now builds the
witness in the sketch's own component, which is what it measures against.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* test: To Object extrude in a moved component from a root sketch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* style: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
* feat(kernel): typed TooLarge refusal and fillet_max search
Blend refusals caused only by the size (larger than the neighbouring faces,
taller than the walls, longer than the next edge, larger than a corner
radius) now return KernelError::TooLarge, displayed as before. fillet_max
finds the largest radius below a request at which the fillet of the edges
works, to three significant digits, by building candidates; it is an explicit
call (up to 16 fillets), not run on every refusal.
A corner edge that doesn't run square to the edge is now reported as such
instead of as a blend larger than the neighbouring faces.
Refs #293
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44
* feat: fillet fit option reduces a radius too large for its edges
solid.fillet takes fit: exact (the default, as before: a radius too large
for the geometry around the edges is refused) or reduce: the radius is
reduced to the largest that works for all the picked edges (kernel
fillet_max, three significant digits, every candidate built). The feature
stores the requested radius, so it refits on every evaluation; when it
reduces, the feature's warning says so and the command returns
radius_applied. Pattern copies of the fillet use the same rule. The file
format writes fit only when it is reduce.
The Fillet dialog gets a Radius Fit row (Exact / Reduce to Fit, constant
radius) and, when the preview reduced the radius, an Applied Radius line;
editing the feature keeps the choice.
One radius for all edges for now; per-edge radii and taper follow.
Refs #293
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bherbruck <[email protected]>
One extrude cutting many circles from one sketch still cut the plate once per
circle, each boolean rebuilding and re-meshing a plate grown by every hole before
it (32 circles 2 s, 128 circles 60 s). The #261 fix joined clear tools with
kernel::union_apart only in pattern replay; apply_traced now does it for every
cut with 8 or more tools (patterns included), keeping each piece's face lineage.
A rectangular or circular pattern of a fillet or chamfer repeated it at the
points picked when the feature was made. The feature itself finds its edges
by name on every evaluation, but a parameter that moves the edges (a taller
pocket) leaves the old points on other geometry, and the copies then
blended the wrong edges or failed ("the faces at the edge ends must be
planar"). The model state now records, per fillet or chamfer, the edges it
blended in this evaluation (part of the state's digest), and the pattern
repeats those.
Fixes#219
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44
Co-authored-by: Claude Opus 5.5 <[email protected]>
* feat(sketch): constructive solving ahead of the numeric solver
Part 1 of splitting #251: the sketch crate only. A graph-constructive front
end (Owen 1991; Fudos & Hoffmann 1997) places what it can by ruler-and-compass
loci and cluster decomposition, Levenberg-Marquardt finishes and verifies the
rest, and the rank analysis stays authoritative for degrees of freedom. Solves
can follow a reference (the previous solution), so edits and drags keep their
branch instead of flipping.
New API: solve_with (hold, reference, analysis, budget) and
solve_after_value_edit; solve and solve_holding keep their signatures as
wrappers, so no caller changes here.
When the seed moves an arc's centre, the arc's unconstructed ends start carried
along (the arc translates) instead of left behind for LM to turn; found by the
reference test 20-slot-constrained.
Refs #203
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44
* feat(doc): keep a sketch's solved state across value edits
Part 2 of splitting #251 (builds on part 1, the sketch crate):
- Sketch edits solve for the document's values continuing from the stored
solved state (Document::solve_sketch), so a dimension edit keeps the
sketch's branch instead of re-solving from scratch.
- A value edit (parameters, a configuration, the units) that would make a
sketch that solved before fail or turn over is refused with the
conflicting constraints named; otherwise the solved sketches are written
back into the document (Model::adopt_solved_sketches), which changes what
saved designs store.
- Later features are keyed by a sketch's solved geometry (to a 1e-6 mm
quantum), so setting a value back reuses their earlier results.
- Solved sketches in a model state are shared (Arc) between states.
Refs #203
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44
* feat(sketch): live drag that keeps its solution, ended by sketch.drag_end
Part 3 of splitting #251 (builds on part 2):
- sketch.drag follows a long move in short held steps, so the geometry keeps
its branch, and reports how far it got (reached, at).
- live: true runs one frame of an interactive drag: the sketch is solved
once and shown, later features keep their last result (marked stale)
until sketch.drag_end or any command that reads the model evaluates it
(CommandSpec::live, reads_model, doc_only).
- sketch.drag_end adds the constraint the drag let go on and evaluates the
model once.
- Undo and redo evaluate a new document once (execute does) instead of
twice.
- The viewport drives drags through live frames and drag_end; dimension
text drags start only off sketch geometry.
Refs #203
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Pieces a cut splits a body into are named after their faces since names
follow history (lineage::order_lumps), so they keep their body names when
an upstream edit moves the cut. The kernel's shell order named them
before, and the two orders can differ: a slot along X through a 40x30x10
block swaps Body1 and Body1 (1). A design saved by an older build stores
body names meaning the old order (fillet and chamfer bodies, targets,
participants, materials, appearances, joints), so it would act on the
other piece.
Each feature now says which order names its pieces
(Feature::shell_piece_order). The v13 to v14 step sets it on every saved
feature, so an older design evaluates as it was saved; features made from
now on, in new or old designs, name their pieces after their faces.
The corpus gains split-body-pieces, saved by v0.3.2: a fillet, a chamfer
and an appearance refer to the two pieces by body name. Without the
setting the two pieces swap names (the corpus test fails); with it they
stay as saved. Migration tests that compared an upgraded design with the
original now expect the setting on its features.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
The world-placement tests checked that the source body's mesh was built
before a placement adopted its names. Once names follow history they no
longer need the mesh, so that is a side effect's timing, not behaviour.
What the tests protect, a placement sharing its source's names, both
still assert through the public API: Arc::ptr_eq(face_names(placed),
face_names(source)).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
Why: every reference a user makes to a face or edge (fillet and chamfer
edges, sketches on faces, shell/draft/offset faces, joints, appearances)
is stored as a name and found again after any upstream edit. Those names
were re-derived by sampling points of each face and voting, and pieces
of a split face were numbered by where their centres lay. After an
upstream edit that splits, merges, mirrors or shrinks a face, a fillet
or sketch could silently re-attach to the wrong face, and nothing told
the user. The kernel now reports exact history for its operations, so
names follow it and fail loudly instead of guessing.
- crates/doc/src/lineage.rs: a feature's kernel histories composed into
one lineage over nameable operands (bodies before the feature, moved
copies, shell cavities, tools named by role: sketch curve, cap, box
side, imported face). Instrumented: extrude/revolve/sweep/loft tools,
join/cut/intersect, lumps after a cut, fillet/chamfer, shell, draft,
split face/body, pattern/mirror (bodies and features), combine,
boundary fill, move/scale, offset/delete face, thread, import.
- crates/doc/src/history_names.rs: names from lineage; split pieces told
apart by the faces only they touch (F2:top#[F2:side:l4,F4:side:l4]),
merged faces answer to every name (aliases), lump bodies ordered by
their faces' names instead of shell order.
- crates/doc/src/name_lookup.rs, piece_names.rs: references resolve by
exact name, alias or bounding faces, else nearest piece with a
warning; names worked out from geometry (operations without history)
are reported when only geometry decided.
- Format 14: names stored by older formats get a legacy mark and are
found as the build that saved them found them (some geometric names
name a face history names differently: a tube's outer wall was named
after its inner circle). New corpus file saved by 9a979cf.
- kernel: Body::lumps carries each piece's history over the whole.
- Adversarial tests: split, merge, mirrored twins, shrink to a sliver,
moving cut with body pieces, history-less bodies, certified sketch.
- docs/book/src/dev/naming.md describes the real mechanism and why.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
* 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 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
---------
Co-authored-by: bherbruck <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
construct.plane.offset now takes a planar face as its base
({"face": [x,y,z], body?, index?, path?}), captured with the same exact
face attachment a sketch on that face uses. The Offset Plane dialog accepts
planar faces as well as origin and construction planes.
A construction plane on a face is resolved through its face anchor when the
timeline is evaluated, so it follows the face when upstream features change
(and after reopening), instead of keeping the plane as it was when picked.
Tests: engine (top and side faces, follows a box resize, survives reopen,
hostile picks are refused without adding a feature) and a UI scenario that
picks a face in the dialog and builds a pad on the new plane.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A construction plane built from model geometry resolves each of its sides
through the document again. That hop restarted the recursion depth at 0, so a
plane that is its own side never exceeded depth 1 and the 32-level cap never
fired: a ~600-byte design file recursed until the stack overflowed and the
process aborted, which no panic guard can intercept.
Carry the depth on the construction context so the geometry arms continue the
caller's count instead of restarting it.
Fixes#143
Co-Authored-By: Claude Opus 5 <[email protected]>
(cherry picked from commit 4bca1d49f27dfe9ef0140132746e1c7415daba8e)
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8