Commit Graph

1096 Commits

Author SHA1 Message Date
jyviko 2fed39c85c Continuity Analysis measures G3 and G4; the dialogs offer what each construction builds
Each seam now also reports the jump of the curvature's arc-length derivative across it (G3)
and of its second derivative (G4), both from the faces' exact partials, and its order: the
highest one every station meets within the tolerances (two new tolerances, defaults in the
kernel). The verdict keeps its meaning (smooth is G2 or better). inspect.continuity replies
with the new metrics, each seam's order and a count per order; the viewport widens G3 and G4
seams, the legend and the marker labels name every order in words. The Patch, Blend and
Boundary rows offer the orders the kernel's Construction table says they build. G4's words
in en, ar, de, he, ja, ko and zh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 21:15:20 -04:00
jyviko cb8712aa09 Continuity: one table of orders G0-G4, and one check of what each construction builds
The kernel's Continuity moves out of bridge.rs into continuity_order.rs and gains G4. One
table holds each order's command word (g0-g4), its code (G0-G4, for tips and messages) and
the message ids of its label and hover text. Construction says which orders the patch,
bridge, network, match and sketch blend curve build today; Continuity::built_by is the one
refusal every command and kernel entry point uses. A design may now save g4 (format note).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
2026-10-27 21:15:20 -04:00
jyviko f042d20736 Isocurve test: the spline face's hole is an inner loop of one face (Refs #770)
The fit-point spline's extruded side is two faces split at x = 20; the hole now
lies inside one of them, so the test clips to a real inner loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 21:15:14 -04:00
jyviko be859aa0d4 Isocurve Analysis draws each face's exact iso curves, cut to its trims (Refs #770)
The analysis sliced the display mesh with planes: faceted, approximate, and
broken at triangle seams and trims; the window drew it as a 2D overlay without
depth while the tool was active. Now one kernel routine (kernel::iso) clips a
surface's u = const or v = const line to a face's pcurve loops; the curve through
a point (sketch projection) and the analysis grid (count each way, evenly across
the face's extent) both use it. The loop polygon follows the loop's order (a
pcurve the loop runs against is walked backwards), which the inside test got
wrong before, and closes over vertex-loop poles.

ModelBody caches the lines per face, count and display quality. The session
keeps the picked faces as view state (inspect.isocurve add/clear, hostile counts
refused); the viewport, ui.render and snapshots draw them depth tested in the
accent colour. Curvature combs on faces follow the same exact lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 21:15:14 -04:00
jyviko 87e7fe472b Tapped holes: a preference keeps metric's major-less-pitch drill
New metric tapped holes drill the tap maker's drill (M8 6.8 mm). The Design preference "Metric
tapped holes drill the major diameter less the pitch" (`metric_tap_drill_major_less_pitch`, off
by default) gives them the drill every metric size took before (M8 6.75 mm): the Hole dialog
shows and sends it, and `solid.hole` given a thread and no `diameter` drills it, through the
session as `auto_project` is (kept when a design is opened or started, and across tabs). Unified,
pipe and taper sizes keep their tabulated drills; a given diameter is unchanged. Saved designs
keep the drill they stored either way.

Labels in en, ar, he, ja, zh, de and ko.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
2026-10-27 13:02:32 -04:00
jyviko 05f1c6b9ac Tapped holes: the tap maker's drill for every form, and a drill of the user's within 5%
Every tapped hole now drills the tap maker's tabulated drill. ISO metric takes Yamawa YEU23's
drill size column for cutting taps (printed p.704, M coarse; pp.704 to 706, MF fine): M8 drills
6.8 mm and M12 10.3 mm, where the major less the pitch gave 6.75 and 10.25. A custom metric size
the table lacks keeps the major less the pitch. UNC/UNF (Guhring), G, Rp, Rc and NPT (Yamawa)
were tabulated already. The Thread command's suggested hole for a metric size follows.

The Hole dialog's Tap drill diameter is now a field, the tap maker's drill by default, and the
command's `diameter` with a thread is checked the same way: a drill is accepted from the
internal thread's standard minor diameter (or the tap maker's drill where that lies below it:
Rp, Rc, NPT) to 5% over the tap maker's drill. Outside that the command names the range and
why. The field's tooltip gives the drill, the minor diameter and the range. Presets keep a
chosen drill.

A saved design keeps the diameter it stored, so it builds as before, and editing such a hole
keeps its drill: no preference and no format change. Scripts that give a thread without a
diameter drill the new default; ladder 05/007, 11/003 and rig case threads/2115 follow
analytically, and their golden entries are re-measured from runs (tier 05 re-sorted by time).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
2026-10-27 13:02:32 -04:00
jyviko e7994d48cc Tapped holes: Rc and NPT take the tap maker's tabulated drill
The Rc and NPT drills were rounded to a drill series from a third-party reproduction of DIN 345
and a fractional/letter inch series cited from memory. They now come from the same tap maker's
catalogue the G and Rp drills do: Yamawa YEU23, printed p.709 (Rc, the maximum straight bored
hole with non-full threads, Rc 1/16 to 4) and p.710 (NPT, the maximum straight bored hole, 1/16
to 3). A size its tables lack (Rc 5 and 6, NPT 3 1/2 to 12) is refused as a tapped hole, naming
the range. The dialog's tooltip keeps the standard's minor diameter where the thread ends.
Rc 1/2 drills 17.6 mm (minor 17.807), 1/2 NPT 17.60 mm (minor 17.473).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
2026-10-27 13:02:32 -04:00
jyviko b6c3b32a98 Tapped holes: an Rc or NPT hole takes the stocked drill under its standard minor
An Rc or NPT hole was drilled to the exact minor diameter where the thread ends, a size no shop
stocks. It now takes the largest stocked drill not larger than that: metric drills of the DIN 345
series for Rc (to 100 mm), letter and fractional inch drills by 64ths (to 1 1/2 in, ASME B94.11M)
for NPT; past the series it is bored to the exact value. The Hole dialog's drill shows the inch
size for NPT, and its tooltip the exact standard value. Rc 1/2: 17.807 mm, drilled 17.75 mm;
1/2 NPT: 17.473 mm, drilled 11/16 in (17.4625 mm).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
2026-10-27 13:02:32 -04:00
jyviko 9717fab2f3 Tapped holes: Rc and NPT, and a modelled thread with the thread's End
The tapped-hole catalogue takes ISO 7-1's Rc and ASME B1.20.1's NPT internal taper threads,
every size of the tables a modelled thread cuts. Their drill is the internal minor diameter where
the thread ends (the reference plane's minor at the face less the 1 in 16 taper over the useful
or effective thread length), derived from the standards rather than a tap maker's table. An
external R is still no hole's.

A tapped hole's options mirror the thread's: `modeled` cuts its thread into the wall, from the
entry, by the same cut as `solid.thread` (now one function both call), and `ease_out` fades its
inner end out before the bottom (ISO 3508); without it the groove stops square at the thread
depth, clear of a blind hole's bottom. The Hole dialog's Tapped rows add Rc and NPT, Modeled
and the End choice (Square, Fade, Fade Length). The format notes in pending/thread-ends.rs and
pending/pipe-threads.rs record the new keys and sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:02:32 -04:00
jyviko 80e47130b9 Thread: the pipe forms in the dialog, ISO 7-1's Rp, a chamfered tip on a taper thread
The thread dialog offers the pipe threads: G (ISO 228-1), Rp (ISO 7-1), R and Rc (ISO 7-1) and
NPT (ASME B1.20.1). Their sizes are listed by designation without a pitch, one start and a hand
of right or left; a size picked for a hole the dialog sized Rc stays Rc.

Rp, ISO 7-1's parallel internal thread an R screws into, is a form of its own (`rp`): the
parallel profile (H = 0.960491 P) at ISO 7-1's gauge diameters, refused on a shaft. Its cut
meets the analytic material of its profile within 1e-5, as G does.

A taper thread takes a chamfered tip: each end's cone is measured from the major diameter the
taper has at that end. Pipe threads are not ISO metric, so they give chamfer_depth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:02:32 -04:00
jyviko 5fe141c0f9 Thread: the start never fades; ease_in is gone
A thread's start runs off its free end, plain or chamfered; only its inner end fades out
(ease_out) or stops square. The `ease_in` field, the command parameter (refused now, naming
ease_out), the dialog's Ease In row and their docs are removed; `pending/thread-ends.rs`
drops a saved `ease_in` as the design opens. The runout test covers the short, standard and a
sub-turn fade at the inner end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:02:32 -04:00
jyviko 361b35afee Thread: a shoulder thinner than the overrun's reach is still a shoulder
The free-end test looked for material 1 to 1.5 pitches past the face's end, beyond the first
pitch so that a chamfer the groove runs out through counts as free. A plate thinner than that
(a boss on a 1 mm plate under M10) had none there, so its top was taken for a free end and
the groove's one-pitch overrun cut into the plate (15 mm³ of a 1 mm plate).

An end is now closed when, over the overrun's whole reach, material lies outside the face's
cylinder (inside it, for a hole) at the radii the groove cuts beyond the face: a shoulder,
plate, head or blind bottom, however thin, steps out past the face, while a chamfer, neck,
countersink or counterbore steps the other way. The band test beyond the first pitch stays
for a cylinder that goes on past a split.

The regression test builds the boss as the app does (a rounded plate, a circle sketched on its
top face, extruded with Join) on 6 and 1 mm plates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:02:32 -04:00
jyviko 75deb73aa7 Thread: ISO 228-1, ISO 7-1 and NPT pipe threads, taper threads by a radial law
Three forms: `bspp` (G, ISO 228-1, 55° parallel), `bspt` (R external, Rc internal, ISO 7-1,
55° taper 1 in 16) and `npt` (ASME B1.20.1, 60° taper 1 in 16). Their sizes are tabulated
from the public sources: ISO 7-1:1978 Table 1 (its gauge-plane diameters are the G sizes)
and NBS Handbook H28 (1957) Table VII.2; tests check every row against the standards' own
relations.

A taper thread starts at its free end with its standard diameter there (external: the gauge
diameter its gauge length in, internal: at the face). Its groove is the screw sweep with the
section moved 1/32 of the axial travel off the axis (the radial law the runouts use, added to
it), its section reaching just past the crest, and the material beyond the major cone (or
inside the minor cone) is cut with it, as a die or tap cuts the crests. An external one runs
its standard length by default.

The volumes are checked against the profile's material fraction integrated over the tapered
radii: G 1/2 to 2e-6, R 1/2, Rc 1/2 and 1/2-14 NPT within 0.2 % of the cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:02:32 -04:00
jyviko 2c4212c042 Thread: the chamfered tip at the start or at both ends
`tip` was none|chamfer, chamfer cutting every free end. It is now none|start|both: `start`
chamfers the end the thread starts from, refused when that end is not free (a shoulder, or a
stop inside the face), and `both` every free end as `chamfer` did. The dialog's Tip pick offers
None, Start and Both; `tip: chamfer` in a command names its replacements.

The thread's end options (runouts, tip, chamfer angle and depth) came without a format
change file; `pending/thread-ends.rs` records them and rewrites a saved `tip: chamfer` to
`both`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:02:32 -04:00
jyviko a30a85698f Thread: the chamfered tip reaches ISO 724's external minor diameter
ISO 4753's chamfered end bounds the end's diameter by the minor diameter. The default took the
modelled root (the basic d1) for it, so the cone's small circle lay on the root cylinder and
the chamfer ring pinched to nothing wherever the groove reached the end. The default is now
the external minor diameter d3 = d - 1.226869 P of ISO 724, below d1: the cone crosses the
root, each end is a whole disk of d3 inside a whole conical ring. ISO 4753 is for ISO metric
threads; the other forms, whose root is already their design minor, give chamfer_depth.

Ladder 06/017's analytic volume follows (1945.354522563); its golden entry re-measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:02:32 -04:00
jyviko d1a8f0417f Thread: a boss thread keeps clear of the plate below
A regression test for the groove stopping at the plate's top: the boss loses the
closed-form volume and the plate keeps all of its 9600 mm³, with and without a runout
and a chamfered tip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:02:32 -04:00
jyviko b2fc5e1fbf smlib, kernel: a refused setback corner is named by its point and why
SMLib's out_failed_corner reaches Rust as Error::CornerRefused { corner }, and the
kernel names the corner by its point: a G2 fillet at a corner of mixed convexity
says where it is and that SMLib sets back only corners whose rounds are all
convex or all concave. Engine test checks the message through solid.fillet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
2026-10-27 12:24:10 -04:00
jyviko d8282b3361 doc, engine: corner: g2 sets each corner back for its G2 patch
FeatureKind::Fillet with corner G2 is kernel::fillet_g2_corners, its setbacks
the corners' distances (the rest 1.5 r); setbacks now go with either corner.
The engine's corner test builds the G2 corner, default and set back 7, one
closed solid of 10 faces within 0.1% of the ball corner's volume.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:24:10 -04:00
jyviko faffa66648 Profiler: the diagnostics.profile test holds while other tests record
The profiler is process-wide, so the engine's other tests (the schema
check's failing diagnostics.profile calls among them) record into it while
this test does. It now shows the reset by the wall time starting again,
and checks only rows that are certain to be there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
2026-10-27 11:40:49 -04:00
jyviko 9cd655259d Profiler: commands, feature evaluation, io; diagnostics.profile; CLI --profile
- Session::execute is a command span; Model::evaluate and each feature computed
  (by its type) are spans; io's import, export and design read/write too; the
  STEP reader's topology, pcurves and solid record.
- diagnostics.profile (contract + output schema): report, start/stop, reset,
  Chrome trace file.
- A STEP/IGES import's reply carries its own profile while recording.
- Every app compiles the profiler in by default (feature `profile`);
  SOLIDCRAFT_PROFILE=1 starts it.
- solidcraft-cli run/eval/bench-open --profile print the table on stderr,
  --profile-out writes the trace; run --profile works on designs and STEP files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:40:49 -04:00
jyviko 541b94a608 Smooth edges: hide only joins inside one smooth surface (G2); draw tangent joins
An edge is left out of the shaded view only where its two faces meet
with one tangent plane (normals within 0.5 degrees) and one curvature
across the edge: at each station the faces' normal curvatures square to
the edge agree within 0.5 degrees' worth, |k1 - k2| <= 0.0087 x
max(|k|, 1/L), L the edge's length, so the continued faces turn apart
by no more than that over a radius of curvature or the edge's length.
Fillet boundaries (0 to 1/r), corner-patch joins and a plane tangent to
a cylinder are drawn again, straight ones included. A modelled thread's
flank pieces (one sweep) differ by at most 0.0046 and stay hidden.

Second derivatives of a spline surface now exist at a boundary point a
pcurve carries a rounding step past the knot range, as its point and
first derivatives already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:16:05 -04:00
jyviko 970c297452 Smooth edges: a measured crease is kept apart from a tangent display join
Smoothness (normals within 0.5 degrees) and a crease are separate
verdicts. A crease is certified where the normals differ by more than
the evaluation can account for: each pcurve point's measured distance
off the edge's own curve times the faces' larger principal curvatures.
So a real crease too small to see is not drawn in the shading but stays
a pickable edge in the wireframe, overriding a coarse same-surface seam
fit, as before; exact tangent joins (fillets, seams, a thread's flank
pieces) are no crease.

The viewport's smooth-wall test expects the wireframe to draw
`edge_visible` edges: the offset ellipse's pieces meet at real creases
of 1e-4 rad that the old checker could not certify.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:16:05 -04:00
jyviko f6645e2c6a Threads: a modelled thread's tangent flank joins are not drawn
The M10 thread's 32 flank-piece joins meet with normals about 1.5e-6
degrees apart (measured independently with seam_diagnostics); each is
left out of the shaded view, stays pickable and stays in the wireframe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:16:05 -04:00
jyviko 57d48aa9f0 Clippy: the kernel's and engine's warnings on develop
A collapsible if (assemble.rs), two manual assign operations (sew.rs), a range loop indexing
the placement inverse's translation row (step_in), and two needless borrows (engine features),
which failed CI's clippy -D warnings. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:03:55 -04:00
jyviko d13300b0a1 cargo fmt: the format-40 change and the rebase's export list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:33:33 -04:00
jyviko 8aea8e5a82 Format 40: a thread's hand on tapped holes too; v0.24.0's handedness opens as hand
v0.24.0 (format 39) saved a left-hand Thread or tapped Hole as `handedness: "left"`; this
branch replaced main's handedness (#4414) with the thread's `hand`, and features don't refuse
unknown fields, so such a design opened right-handed without a word. The pending format change
thread-hand.rs renames `handedness` to `hand` on Hole and Thread features (a `right` is
dropped, anything else refused).

Tapped holes take `hand` again, right or left (never both, and only with `thread`): the Hole
feature saves it, its thread is cut and annotated that way, solid.hole takes `hand?`, the hole
dialog's tapped rows have the thread dialog's Hand choice (without Both), editing a hole keeps
it, and hole presets save it under v0.24.0's `thread_options.handedness` key, so presets saved
by v0.24.0 recall it.

Test: a format-39 design with a left-hand thread and tapped hole opens left-handed; a hole's
`hand` refuses both and needs a thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:53 -04:00
jyviko 562a5bd337 Formatting: rustfmt over the rebased branch
cargo fmt --all, nothing else: code from the branch's commits and from the rebase's
resolutions that was not yet in rustfmt's layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoZgkCdAVrscuw2kjQTWDs
2026-10-27 02:28:53 -04:00
jyviko 5fc160732e Main's new callers of the placing calls that now return their error
Code main added since the last rebase reads the placed model, a camera or a scene
as values; on this branch they return Result. Picks and the edit dialogs read the
session's placed model through open_meshing::session_world, as the others do; the
revolve and circular-pattern axes, the model axis of a command and the warming of a
computed edit pass the error on; tests unwrap. A test meshing bodies uses
Body::mesh, the one mesher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoZgkCdAVrscuw2kjQTWDs
2026-10-27 02:28:53 -04:00
jyviko 6b9f5e0e49 thread: main's screw direction (#4414) out; the thread's hand is the one direction
The reworked thread has its own direction, `hand: right|left|both`, on SMLib's
exact screw sweep. Main's #4414 added a second one, `handedness: right|left`, on
Hole and Thread, for the faceted groove this branch replaces. It is taken out: the
field, its pending format change (never released), the hole dialog's direction row,
its hole-preset option, its strings and its tests, which measured the faceted
groove. A tapped hole is right-handed, as before #4414.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoZgkCdAVrscuw2kjQTWDs
2026-10-27 02:28:53 -04:00
jyviko a57ce13e70 thread: a shaft thicker than a modelled thread is refused, never turned down
A modelled thread never changes the size of the shaft it is on (#1018, #2853): a
shaft thicker than the thread is refused with the size to make it, as
tests/kernel-cases/threads/2705 expects ("this 14.6 mm shaft is too thick for a
modelled M14 thread; make the shaft 14 mm for M14"). This takes out the turn-down
of #4256: `thread_turn`, the fit that let a thicker shaft through, and its tests;
the 9 mm shaft refusing an M8 and the preview's M6 refusal on an 8 mm shaft are
back as they were before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoZgkCdAVrscuw2kjQTWDs
2026-10-27 02:28:53 -04:00
jyviko 6e15b96769 engine, kernel-cases: tangent joins out of the corner-pick count; 3383 and 5024 recipes to main's rules
The corner pick (#5173) counts the edges meeting at a picked point; seams already
did not count, and now a tangent join between two spans of one smooth curve does
not either (4332: a chamfer on a spline rim was refused as two edges).

3383: the first occurrence is grounded by default on main, so the recipe
ungrounds it before moving it. 5024: a hole's position must lie on a face, so the
centre is on the box's top edge; the volume is half of the drilled cut, exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:53 -04:00
jyviko cfc393d68f Main's new callers of the placing calls that now return their error
The branch's placement change (placing the model returns its error) made
world_state, home_camera, bounds, open_sketch_plane, the scene builders,
to_local and auto_operation return a Result. Main added callers of them
since the branch left it; they now take the error as the branch's own
callers do:

- engine: commands pass it on (`?`); helpers answering yes/no or "found"
  (snap points, a face's normal, the DXF export check) answer no for a
  model that cannot be placed. Frames keep the session for main's lazy body
  frames (#6356) beside the placed model the picks are on; a library apply
  maps its targets on that placed model. Before an extract or a link is
  replaced, the placed model is taken or the command fails.
- ui: panels, picks, dialogs and previews read the placed model through
  open_meshing::session_world (none when it cannot be placed: the viewport
  says why); Home and Fit frame nothing then.
- cli: the usage table is main's, with kernel-cases in it (a resolution of
  the rig's commit had kept the branch's whole file).
- tests read the Results with unwrap, and main's tests of Body::tessellate
  mesh through Body::mesh. Main's test of saved boolean retries goes (the
  branch saves none), as does the thread half of its dialog rendering test
  (the branch's thread dialog has its own); the tapped hole half stays.
- What no caller uses after the merge goes: a mesh body's span, the metric
  thread refusal text, and the revolve's targets-as-shown model (main used
  it for the coplanar run-on the branch removed). Main's SolidWorks Y-up
  test reads the exporting system's name (the branch's STEP header has no
  preprocessor or description fields).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:53 -04:00
jyviko c854239d18 doc, engine: solid.fillet setbacks: [{vertex, distance}]
FeatureKind::Fillet gains setbacks (serde default: none, omitted when empty),
each a corner vertex and a length expression; a fillet with setbacks is
evaluated by kernel::fillet_setback. Constant-radius exact fillets with
rolling corners only (refused otherwise, in the command and in evaluation).
One distance per corner: SMLib keeps one setback per corner, not per edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:53 -04:00
jyviko fdede5a0ae Thread work in rustfmt's layout
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:53 -04:00
jyviko d433ce2c9f Thread runouts: the groove fades out instead of stopping square
Ladder 06/009 had no way to ask for one: "solid.thread has no runout (vanishing thread)
length". With SMLib's smlib_screw_sweep_radial (replay e7acea1):

- smlib: the FFI binding and Brep::screw_sweep_radial, the profile moved off the axis by a
  piecewise linear law of the turn; crate test: a tooth moved out 1 mm over three turns has
  A 2π (c turns + ∫ offset dt).
- kernel: sweep_helix_radial, sharing sweep_helix's checks (helix_profile).
- doc: `ease_in`, `ease_out` (a length, or ISO 3508's standard 2.5 P or short 1.25 P) fade the
  groove over that length, its section moving out of the material as it turns (outward for a
  shaft, inward for a nut). ease_in is at the end the thread starts from. The runout lies past
  the full-form length: beyond a square stop, before a shoulder up to its clearance; at a free
  end inside the face. Runouts longer than the thread are refused. No end gets one by default,
  so saved threads keep their geometry.

The cut is the integral over the turn of the clipped section's 2π∫ r dA: engine test (2.5 P
and 1.25 P between two collars) within the sweep's wall tolerance times the runouts' wall area
(0.029 against a 0.036 mm³ bound), ladder 06/009 (004's shaft, a 2 P runout before the
shoulder) graded in closed form (measured 1.7e-6 off).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:53 -04:00
jyviko 7bfc184e8e A modelled thread's chamfered tip (ISO 4753)
`tip: chamfer` cuts ISO 4753's chamfered end at each free end a modelled external thread
reaches, after the groove, so the last teeth taper into the cone instead of ending in a
ragged first turn: the material outside a cone `chamfer_angle` to the axis (default 45°)
reaching d_f = d - 2 `chamfer_depth` at the face's end (default the external minor diameter,
the largest the standard allows), a revolved ring cut by one boolean per end. Past the end the
ring keeps to the radii the groove's overrun already cuts. An internal thread is refused (its
entry takes the hole's countersink), as is a tip with no free end or on a cosmetic thread.

The cone is axisymmetric and cut after the screw-symmetric groove, so the volume is the stock's
chamfered section with the thread's material fraction: engine test (both ends 45°, and 30°
x 0.5 mm at a shaft's one free end above a collar) within 1e-4, ladder 06/017 (measured
2.4e-7 off).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:53 -04:00
jyviko 3287bcba9e Threads with several starts
Ladder 06/008 had no way to ask for one: "solid.thread has no start count". `starts: n` (1 to
8) cuts n grooves of lead n x P, the j-th a j/n turn round from the first (the same helix j
pitches along), in one boolean with their union. A designation that gives the lead
(M16xPh3P1.5, Tr40x14(P7)) gives the starts, and a `starts` that disagrees is refused. The
helix's self-clearance check and the turn limit use the lead.

Over the same length n grooves of lead nP take the volume of one start of pitch P: engine test
(2 and 3 starts, M10xPh3P1.5) within 1e-4 of the closed form, ladder 06/008 graded as 001
(measured 3.5e-7 off).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:53 -04:00
jyviko b4549d2700 Threads in the standard forms, right- or left-handed or both
Only the ISO metric 60° thread existed. Forms now (crates/doc/src/thread_forms.rs, values and
sources in docs/threads.md): ISO metric (ISO 68-1), trapezoidal (ISO 2904 design profile, ISO
2902 sizes), general purpose Acme (ASME B1.5), metric buttress 3°/30° (DIN 513), buttress inch
7°/45° (ASME B1.9 basic profile). Each has its groove as a straight-flanked polygon, its standard
sizes (none for ASME buttress), its designations (M10, M16xPh3P1.5, Tr40x14(P7), S20x4,
1/2-10 ACME, 2.5-8 BUTT) and the size that fits a face. The square thread is refused: no current
standard defines it.

`hand: right | left | both`: a left-handed groove is the screw motion about the reversed axis
rising along it; both cuts the two grooves, crossing, in one boolean. `steep_flank` turns a
buttress. The design diameters are checked against the published tables (ISO 2904/DIN 103,
DIN 513 via IS 4696-3); every form's cut against 2π∫ r w(r) dr per turn to 1e-4 in engine tests
and to 1e-4 in ladder 06/011 to 016 (they measure within 5e-7).

FeatureKind::Thread holds every option the command has (form, hand, starts, ease_in, ease_out,
tip, chamfer_angle, chamfer_depth, steep_flank, all defaulted so saved threads load unchanged);
starts, runouts and the chamfered tip are refused as not built yet. suggested_thread_in takes the
form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:52 -04:00
jyviko 366b90597f Threads start at the end meant and stop clear of a shoulder
A thread of some length ran from "the end of the face nearest the pick", a coin toss for a
pick in the middle (rig 602's 4 mm thread meant for the free bottom end ran at the head end,
cutting 1.17 mm into the hex), and every thread end at a face end got a one-pitch overrun,
which at a shoulder gouged the material past it (ladder 06/004 cut 0.9% of its volume out of
the collar).

- kernel: material_in_ring, the exact intersection of a body with a ring about a cylinder
  face's axis, by an SMLib boolean.
- doc: a face end is free when no material lies past it, over the span the groove's one-pitch
  overrun reaches beyond its first pitch, within the radii the groove cuts (a cap, a chamfer
  the groove runs out through, a counterbore). At a free end the groove runs out as before; at
  a closed one (a shoulder, a head, a blind hole's bottom) it stops with its widest part clear
  of it; inside the face it stops square.
- doc: a thread of some length starts at the end nearest `from`, else at the face's only free
  end, else at the end the pick lies within a third of the face of; a pick in the middle of a
  face whose ends are alike is refused, naming `from`. A length as long as the face runs the
  whole face. `up_to` a plane or a point's place along the axis. New fields `from`, `up_to`
  default to none, so saved threads keep their meaning where it was not a coin toss.
- engine: solid.thread takes extent (full|length|up_to), from and up_to.
- ladder 06/003 (now with `from`), 004 and 007 are graded by volume and centre of mass in
  closed form (a square stop inside material removes exactly length/P turns, the section being
  symmetric); rig 602's method says why the thread is at the bottom, tolerance 0.02%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:52 -04:00
jyviko 3f2585569b kernel: sag refinement adds no node to a face's boundary
A face's boundary nodes are its edges', shared with the neighbouring face.
On a nearly straight stretch (the base of a thread flank's corner, clipped by
a hex head's flats, on an iso-line of the helix) refinement met slivers
between consecutive boundary nodes: their centroid, or the middle of their
inner edge, lies on the boundary chord to rounding. Inserting it split the
boundary constraint, and the face was refused ("boundary crosses itself";
rig threads/602, refused while the body was meshed to pick the fillet's
edges). A sliver whose centroid lies on the boundary is now split at its
inner edges' middles, and a refinement point that lands on the boundary is
not inserted.

The 602 engine test assumed the old kernel's faceted thread (over 1000
faces); with SMLib the thread is exact, and the test now checks that the
fillet finishes and removes a little material.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:52 -04:00
jyviko 9a93f9d215 An edge flange picked inside a notch is built on the notch's edge (#5306)
A notch cut into a sheet's base is a cut-out, so the base outline that
edge_at and the edge names read has no edge along it: a flange picked on
the notch's bottom edge took the name of the nearest outline edge (the
plate's far edge) and was built on the stretch of it the notch left. A
pick nearer a notch's edge than any outline edge now names no edge, and
the flange runs the base's outline round that notch first, keeping as a
cut-out only what it takes of the flanges, then trims and bends as on any
edge of the outline. The edge of a hole inside the base is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:52 -04:00
jyviko 5a9bf47420 A converted sheet takes its rule's values at its own thickness (#4328)
Convert to Sheet Metal keeps a shell's (and a plate's) own thickness, but
the rule's values were evaluated with `Thickness` the rule's: under the
default rule (2.5 mm, bend radius `Thickness`) an L 2 mm thick with its
bend modelled R 2 was rebuilt bent R 2.5, losing 8.58 mm3 (2659.911
against 2668.496), and its gap and reliefs followed 2.5 mm too. The rule
is now evaluated for the sheet's thickness (Document::rule_values_at):
in the conversion once the shell's thickness is known, for a converted
plate, and for the flanges, hems and folds added to an existing sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:52 -04:00
jyviko 1171c4c2a5 A sheet metal body mirrored or patterned into new bodies is sheet metal (#3353)
solid.mirror and solid.pattern of bodies pushed each copy as a plain body:
no sheet model went with it, so its flat pattern, DXF and bend table were
refused ("`Body1 (2)` is not a sheet metal body"). Each copy now gets its
source's sheet model moved with it (SheetBody::moved). A reflection turns
the frame's handedness over, so a mirrored copy's frame sits on the moved
top face, thickness growing back toward the moved bottom: the same blank,
each bend turning the other way, machining depths taken from the other
face. A sheet feature on the copy rebuilds it in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:52 -04:00
jyviko 3fb263d004 hole: keep a countersink's tool within its rim above the face
The countersink's cone started 1% of the drilled body's size above the
entry face, widening on to rc + top tan(half) there, so above the face
it cut beyond the countersink's rim: a wall 0.15 mm from the rim was
notched (#5041, 12 faces instead of 10). The tool is now one revolved
profile: the cone from its rim on the face to just inside the bore, and
above the face the rim's own cylinder, as the bore's cylinder runs on
above it. The cone stays the profile's first segment (csink:side:0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:52 -04:00
jyviko 610173e1fe combine: move each tool into its target's frame
A Combine took its tools in their own component's coordinates and joined
them to a target kept in another component's: a root body joined into a
body of a component turned a quarter turn landed where the turn carries
its root coordinates (#3383, 2 shells and 17000 mm3 instead of one solid
of 16750). Each tool is now moved through its own placement and the
inverse of the target's (component placement, then body offset), as
state_in_frame places bodies, and a combine's cache key follows the
placements so moving a component recomputes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:52 -04:00
jyviko 1df330c79b engine: a shell that opens no face makes a closed hollow body
solid.shell refused an empty face list ("select 1…1000 faces"), and the
shell feature found its body from the first face picked. SMLib shells with no
face opened (the void sealed inside, its own shell): the command now takes
`faces: []`, the feature shells the one body there is or the one named
(`body`), and an outside or both-ways shell pushes no open face out of the
cavity. Rig shell/4009 declares the void's shell (`shells: 2`), as its method
says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:52 -04:00
jyviko 810f0d66d9 doc: a join groups pieces that touch nothing; only edge and point contacts are kept apart
With touching: separate (the default) a join groups the joined body's
pieces by face contact. The groups that hold the material of the body
before the feature stay in it (each unless it touches one already kept;
the first group where the lineage doesn't say), and so do the groups that
touch nothing: a virtual grouping later operations take as one body. Every
other group that touches another along an edge or at a point becomes a
body of its own, and the feature's notice says so. The joined body keeps
its name and never holds an edge or point contact. A join that only
grouped pieces apart gives no notice, so KeptSeparate::Apart is gone.
one_body still keeps every piece in one body.

SMLib's export copies an edge or vertex that touching solids share once
per solid, so the record never listed those contacts:
Brep::region_contacts now takes such copies (the same curve and interval
between vertices at the same points, a vertex at the same point) as the
edge or vertex they share. A contact SMLib leaves out of the topology (the
3612 spoke tangent to its hub) is found on the exact geometry, the least
distance within the record tolerance, and reported as KeptSeparate::
Touching ("along a line or at a point").

The pending format change join-touching-bodies stays: older designs kept
pieces touching along an edge or at a point in one body too. Its notes say
so, and the reopened legacy emboss still expects touching: one_body.

Tests restored to their form before cc49081df and 733d3bd19: kernel-cases
2896-join-arm-end-near-ring-wall-gap (one body of two shells), 3379, 3455
and 3526 (no touching: one_body); the engine tests of posts joined while
apart (twice), two cubes for interference, a ring with a cylinder, the
buried tab and a_join_adds_only_its_sketch's 0.1 mm gap.
join_meeting_nothing_keeps_the_first_body expects one body and no notice.
The #3378 spoke stays 1 mm inside the hub.

New tests: blocks touching along an edge are split with the along-an-edge
notice; blocks apart stay one body without one; a ring with an arm on its
corner edge and a hub apart gives ring and hub in one body and the arm on
its own (doc); the engine's notice for an edge contact; the record's
contacts of cubes sharing an edge or a vertex (kernel).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:28:52 -04:00
jyviko 3e2b109cc7 engine: feature commands take touching; a join's result notice
Every feature command reads touching (separate | one_body) as it reads
participants; the box and extrude contracts declare it. A feature result
carries notice {message, contact, action} when its join kept solids
apart ("These bodies only touch along an edge, so they were kept as
separate bodies."), the action a timeline.edit that sets touching to
one_body; programmatic calls open nothing.

Tests adjusted: join_meeting_nothing_keeps_the_first_body now expects the
separate body and its notice by default and the old single body of two
lumps with touching one_body; the schema key lists and the
FeatureResult literal gain the new fields.

Engine tests whose subject is a body joined from pieces that share no
volume make it with touching: one_body (posts joined while apart, a
cube joined clear of another, a cylinder joined clear of a ring, a tab
joined in a component); a legacy design reopens with touching: one_body
on its emboss; the 0.1 mm gap of a_join_adds_only_its_sketch leaves the
block a body of its own; and the #3378 spoke starts 1 mm inside the hub,
so the join merges its targets instead of touching the hub along a line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
(cherry picked from commit 7a2915fe32e6549cbf3170d69e0f5b6518e19da3)
2026-10-27 02:28:52 -04:00
jyviko 9a79588575 doc, engine, ui: placing the model returns its error; placements are checked where they are set
doc: world_state, world_state_reusing, state_in_frame_at, state_in_frame, face_point_state,
face_point_names and construction_state return Result. A body that could not be placed, a
frame that could not be inverted, a sketch plane or thread axis that did not survive the
placement, or a failed placing worker used to leave things where they were without a word;
now the call fails with what could not be placed. is_placement is the one test of an
occurrence's placement (finite, a rotation that does not mirror, a translation within
1e9 mm). Opening a design names every occurrence that fails it; add_occurrence refuses
one; a joint solution that would place an occurrence outside one is not applied and the
joints placing it report it.

engine: Session::world_state returns Result and keeps a failure in its cache as it keeps a
placed model. bounds, home_camera, open_sketch_plane, the scene builders, the world
previews, frames::to_local and auto_operation pass the error on; helpers answering "not
found" take the placed model from their caller. Commands setting a placement check it
(valid_position, exploded view, fastener at a point, STEP insert matrix).

ui: panels, picks and hovers read the session's placed model through
open_meshing::session_world; a model that cannot be placed shows no placed bodies, and the
viewport says why across its bottom. Pictures and the control channel report the error.

cli, mcp: snapshots, renders, benches and the oracle report the error; the kernel-case rig
records it as the case's failure. Tests read the new Results with unwrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
2026-10-27 02:28:51 -04:00
jyviko d0ff1dcfa4 doc: an extrude's taper without a corner rule picks one per profile
Extent.corner is optional: unset, each profile preserves curvature where
every curve is a line or an arc and takes the tangent rule where another
curve has a corner, with a note in the feature's warning; a rule the user
gave is kept. draft_corners reports not_exact as the kernel builds (any
free curve in a cornered profile). Builds (release CLI); cases not yet run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
2026-10-27 02:28:50 -04:00