The set-back corner's hole is filled with one four-sided patch per corner of
the hole meeting at a centre point (solidcraft_geom::fill_quad), each holding
its two half-sides exactly (rational rows for the rounds' sections, standard
form, rational-line pcurves), G2 to its neighbours and to the next patch
across its spoke; the height-field fill (fill_matched) is removed. The kernel
grades a corner's seams as the ladder does (corner_continuity), and the ladder
uses it. Ladder 07/015 and 07/019: 15 and 18 faces.
Known: the eight-corner box test is ignored: at some corners SMLib gives one
half of a section as a polynomial spline 1.7e-5 mm off its round, tangent out
of its plane, which the patches cannot meet within tolerance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
Works: crates/geom fill_quad (N quads about a centre, exact rational/polynomial
boundary rows, G1/G2 by Gauss-Newton least squares, banded RCM solve), kernel
quad_patch/quad_body, corner_continuity report; box corner 07/015, concave
07/019, set back 8/11/13/15 pass the ladder-style grading (clear 7e-4 per mm).
Not yet: eight-corner box (mirrored corners: an SMLib section half that is a
polynomial approximation, tangent 0.02 deg out of its round's tangent plane at
the side's middle), ladder expected files, fillet diagnostics, images; debug
QDBG prints and probe tests still in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
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
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
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
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
A preset's thread options save `modeled` and `ease_out` (a named ISO 3508 runout or the fade's
length, kept portable as the other lengths are) and recall them into the dialog. A runout
without a modelled thread is no preset's. Recalling checked the thread with the metric parser
only, so a UNC, G, Rp, Rc or NPT preset was refused; it checks the tapped-thread catalogue now.
An older preset without these options recalls as before: listed, square.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
Ladder 06/017's golden entry was edited by hand when the chamfered tip moved to ISO 724's d3.
Re-measured by running the case (solidcraft-cli kernel-cases on SMLib): its counts, area and
volume are the ones written by hand; its time is the run's. The tier's order by time holds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
The thread's start is its free end: it runs off plain or chamfered (ISO 4753). Its inner end
stops square in its own profile or fades over a length; an empty Fade Length is ISO 3508's
runout (2.5 P, or 1.25 P for a thread saved short). A thread saved chamfered at both ends keeps
that. "Square" is its own message (thread.end_square), apart from the geometric square. The
new labels are in en, ar, he, ja, zh, de and ko; Extent, Ends, the chamfer labels and the ISO
4753 hint come from the catalog entries already on develop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
`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>
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>
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>
Curvature continuous is the Type entry's name; Corners (Ball, Smooth patch) and Setback are
described with the command words they write. The codes (G1, G2) stay in the text, where they
explain the words.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
Patch, Boundary and Blend Surface list Position, Tangent, Curvature and Curvature change; each
order's code (G0 to G3) and what it means is in the row's hover text. The Continuity Analysis
legend, its seam markers and its status line use the same words (Crease for a G0 seam). The
segments now draw their labels translated. The new labels carry over each catalog's existing
translation with the code taken off, so no language loses its wording; a word another label
already translates keeps that translation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
The Continuity G1|G2 control repeated Type (both set `type`) and G1 silently cleared the
corner; it is gone, and Type names the section in words: "Curvature continuous", its code (G2)
in the row's hover text. The vertex choice moves out of Advanced as Corners: Ball | Smooth
patch, shown only while the edges have a corner where three or more rounded edges meet (or the
fillet already has a smooth patch), enabled for Constant radius with Exact fit and saying why
when not. Smooth patch shows Setback: one distance for every corner, sent as `setbacks` at each
corner's vertex; left empty, the command's default (1.5 x the radius) applies. A fillet saved
with setbacks reopens with them: one distance at exactly the dialog's corners shows in Setback,
any other list is kept as saved. "Smooth" in Variable Radius is "Smooth radius change". The
stale "G2 corner construction is not supported yet" tooltip and the "Fit and G2 corners" note
are replaced by new messages (en, ar, de, he, ja, ko, zh).
No command parameter changes; saved designs open unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
A face whose boundary crosses itself in (u, v) (a cusp, a sliver whose sides
sag across each other) was an error, and the body had no display plan. It is
now planned once more with a lenient constrained Delaunay in (u, v): sides
crossing one already kept are left out, the rest is seeded and refined as any
face, and its points lie on the surface, where the GPU evaluates them. The
display has no hole, but the face is still a defect upstream, in its pcurves
or seams: PlanStats::lenient names it with the strict plan's refusal, and the
display plan logs a warning for each such face, so it is never silent.
The face boundaries keep coming from the exact pcurves (d61efb306); cdt::Fill
gets its `lenient` field back (dropped in 3f2585569). The old flat fill (#4021,
tess/fallback.rs) stays gone: develop removed it in 9138e8704. A face the
lenient fill refuses too (loops that do not close in (u, v), or wind round a
seam or pole) stays an error naming the face. Closes#4453's intent on develop.
(cherry picked from commit 84de5aa071)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
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
The inner corner of a 20 x 20 x 10 notch in the 60 x 40 x 20 box, three
concave r5 rounds with corner: g2: one valid body of 13 faces within the box,
the patch's twelve seams jumping at most 1e-3 per mm clear of the hole's six
corners (2.7e-4 measured; the ball corner jumps 0.2). The continuity check
looks within 11 mm of the corner: the vertical round, 2.5 mm long past its
setback, meets its faces 11.2 mm out with every round's G1 jump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The inner corner of a notch, three concave r5 rounds, takes the set-back G2
patch as a convex corner does: one valid solid with the ball corner's 13
faces, seam jump 6.3e-4 per mm clear of the hole's corners against the
ball's 0.2. A corner of two convex rounds and a concave one is refused as not
supported: SMLib sets back only corners of one convexity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
expected.json `continuity`: over the tangent seams wholly within `within_mm`
of `near`, the curvature jump across them (exact surfaces, 65 stations a
seam) at most `curvature_jump_per_mm` clear (`clear_mm` along the seam) of the
vertices where two of them turn, where a neighbour's own G1 seam ends.
07/015, the box corner's three r5 rounds with corner: g2: one valid body of 10
faces within the box, its patch's twelve seams jumping at most 1e-3 per mm
clear of the hole's six corners by 1.2 mm (2.7e-4 measured; the ball corner
of 009 jumps 0.2). Its golden entry is the passing run's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
A corner where three or more rounded edges meet, every edge there rounded, is
set back (1.5 r unless a setback names it) and left open by SMLib
(blend_smlib::fillet_setback_open: the sheet and each corner's hole, in the
sheet's orders); each hole is filled by fill::matched_patch, the height field
over its plane fitted to the position, tangent plane and normal curvature
across the boundary of every face beside it (exact second derivatives), rounds
and planes alike, each edge's pcurve its exact projection; SMLib sews the
sheet and the patches into one solid, which must close. The history is the
sewing's carried through the sheet's, each patch face made from the rounded
edges its neighbours come from.
corner_g2 is that construction now. The polar patch over three sections it
held (never sewn in) could not be: its boundary is a closed cubic through
stations, which rounds off a corner of the loop, and the setback hole has six.
The 60 x 40 x 20 box corner of r5 rounds (07/015): 10 faces, a valid closed
solid (record and SMLib's check), curvature jump across the patch's seams
6.3e-4 per mm clear of the hole's corners against the ball's 0.2; set back 8
between the hull and prism bounds; set back 11 to 15 inside the box but by
under 7e-4; all eight corners of a box filled; refusals named.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SeamReport.stations: each station's edge fraction, gap, normal angle and
curvature jump, alongside its point, so a seam's profile (where a jump holds,
where it fades) can be read and plotted, not only its worst value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
smlib_blend_setback_open's binding: the sheet, and per corner a Hole, its
edges in order (each starting where the one before ends), each with the face
beside it and whether that face runs it forwards. The holes' buffer starts at
64 edges a corner and grows once to the length a first call names. Tested on
the r5 box corner set back 8: 9/30/22, a sheet SMLib's check passes, one hole
of twelve edges chained end to start; an unrounded edge at the corner is
Unsupported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fill_loop's bicubic height field over the loop's plane, fitted to the
neighbours' derivatives as well as the loop: every condition of a graph is
linear in its heights (G1: the gradient from the face's normal; G2: the second
derivative along the face's leave direction from its normal curvature), so
one banded least-squares solve with the thin-plate energy decides the surface.
Its parameters stay plane coordinates, so an edge's pcurve is still the edge
projected onto the plane, exactly.
Where a round's section meets a plane at a corner of the loop, the section has
the round's curvature and the plane is flat across its own curve: no surface
is G2 to both there. The G2 rows fade in over 15% of the run from each corner;
asked over a few cells instead, the plane's flatness to the second order along
its curve pulls the surface off the whole side. Grids go to 128 spans, which
G2 to a plane along a curved line needs. The thin-plate weight is a thousand
times fill_loop's: with the slope and curvature rows acting a little inside
the loop, the weaker one let the surface rise past a plane it is G2 to (0.06
mm at a box corner of r5 rounds set back 8).
loop_frame is fit_once's plane, shared.
Tests: the hole a setback leaves at a box corner of r5 rounds set back 8,
within 2.2e-5 of the loop, 0.027 degrees and 4.7e-4 per mm of its neighbours
clear of the corners; set back 7.5 to 15, under its planes but by under 7.2e-4;
a hole in a cylinder filled with the cylinder's own second order; refusals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What each category records, self and total time, the table and the Chrome
trace; the CLI's --profile and --profile-out on scripts, designs and STEP
files; diagnostics.profile over the control channel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
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
- Graphics page: "Profile operations" (off by default) starts and stops the
profiler when it changes; an agent's diagnostics.profile in between stands,
and SOLIDCRAFT_PROFILE=1 is not undone by the default at start-up.
- The desktop app reads SOLIDCRAFT_PROFILE at start-up.
- Labels in en, ar, de, he, ja, ko, zh. The pending-messages test compares
right-to-left pending text in display order, as the catalogs give it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
Booleans, blends, sweeps, lofts, shell/draft/offset, healing and sewing,
SMLib conversions, meshing and measuring; the import as parse, index,
per-solid read, placement and mesh check, convert batches and finish.
The existing per-step boolean/tessellate capture names its spans too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The C API logs each call with its steady_clock time; the log sink records
that as an smlib span (child of whatever runs on the calling thread).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
solidcraft-profile (L0, standalone): spans with self and total time, SMLib-timed
records, a bounded trace (200k events) and an aggregate that stays; compiled in
with the `enabled` feature, off at run time until switched on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
An edge is left out of the shaded view when its two faces' outward
normals, evaluated on their exact surfaces at each use's own pcurve,
agree within half a degree at eight stations along it (a seam one face
meets itself across is the same test with its two pcurves); it is a
protected crease when they differ by more. Replaces the special cases
for ruled clamped Bezier patches, planes, cylinders and cones. Fillet
tangent joins and corner-patch joins are hidden too; they stay
pickable and in the wireframe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The truck vendor/ crates went with the kernel's move to SMLib, so their
mapping and the test that selected the kernel for vendor/truck-shapeops
are gone; egui-wgpu is the one vendored crate left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
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>
Seven labels of the thread dialog had no English catalog entry, so they stayed English in every
language (every_ui_label_literal_reaches_an_english_catalog_entry): Extent, Ends, Chamfer Angle,
Chamfer Depth, the two ISO 4753 hints and the designation hint. They are in
assets/locales/pending/thread-dialog-labels.json with Arabic, Hebrew, Japanese and Chinese, the
catalogs that must be complete. The ISO 4753 hints are looked up by id, and the designation hint
takes its example as {example}, so a right-to-left language places it where it reads.
pending_messages_are_in_the_catalogs compared a pending message with the catalog's text, which
for Arabic and Hebrew is in display order since #3768; it compares with that order now, as the
catalog stores it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's Arabic and Hebrew catalogs (#3768) arrived with thread.direction, thread.left_hand and
thread.right_hand, the labels of #4414's screw direction, which this branch replaced with the
thread's hand and took out of every catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each job fetched the prebuilt SMLib into target/ before Swatinem/rust-cache restored target/,
which replaced it, so every build that links SMLib stopped with "SMLib ... is not fetched"
(the Docker image of #7403). The fetch now follows the cache restore in every job.
develop (the SMLib kernel's trunk until it merges into main) gets main's push runs: CI runs on
pushes to it, a push compares against the last passing push to its own branch, and its runs save
the Rust caches the PRs into it restore.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move/Copy's pick from the browser (#4774) read Session::world_state as the placed model; it now
returns why a model cannot be placed, so the browser reads it as the other panels do, through
open_meshing::session_world (no placed bodies when it cannot be placed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Every job that builds the kernel with SMLib runs scripts/fetch-smlib.sh after the toolchain:
the host's prebuilt archive, the matrix target's for cross builds, wasm32's for the web jobs.
RISC-V has no archive, so its check compiles the sources with g++-riscv64-linux-gnu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoZgkCdAVrscuw2kjQTWDs
tangency-labs/smlib's prebuilt workflow builds the C API once per commit for every Rust target
SolidCraft builds (Linux x86_64 and aarch64, macOS arm64 and x86_64, Windows x64, x86 and ARM64,
wasm32 with wasi-sdk's C++ runtime) and releases it as prebuilt-<commit>. SMLIB_PREBUILT pins each
archive's SHA-256. scripts/fetch-smlib.sh now downloads the host's archive (or the named
targets'), checks its hash and the commit it was built from, and falls back to the sources for a
target with no archive; --source fetches the sources, --pin writes SMLIB_PREBUILT from the
release. build.rs links a fetched archive whose hash matches the pin, else compiles as before,
now with -ffp-contract=off like the archives: every platform does the same arithmetic. No
network access in build.rs.
SMLIB_REV moves to 8b57e7e: the workflow, and fixes for MSVC (no directive inside a macro
argument, SM_PI for M_PI), the no-fused-multiply-add build and a test that needed a POSIX shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoZgkCdAVrscuw2kjQTWDs
On the web target the smlib build compiles SMLib for wasm32-wasip1 with wasi-sdk's clang
(WASI_SDK_PATH), with wasm exceptions in the exnref encoding that wasi-sdk's libc++ and Rust
use, and links wasi-sdk's libc++, libc++abi, libunwind and libc. The WASI calls libc makes are
defined in ffi.rs: stdout and stderr go to the log, the clocks are the page's (the C API times
every call), and there is no file system or environment. solidcraft-web gets an smlib
feature, off until CI has wasi-sdk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoZgkCdAVrscuw2kjQTWDs
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
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
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
Main's reader (#1609, #3845, #3928) and the branch's met in the rebase, and the
tree kept main's gap closing: it moved a trim piece's end vertex onto the mean
of the gap's ends without changing its curve, and widened the STEP file's
stated accuracy to cover the move. SMLib checks each edge against its vertex at
the body's tolerance and refused the box (SmTrimmingTools Edge/Edge Gap 0.005 >
0.00425, rig 3845); the first piece of a trim curve with no start point was
also turned against the second (rig 3913, "edge loop is not connected").
The reader keeps main's structure (groups and subfigure instances as products,
names, placements, separate lumps as separate solids, the originating system)
and sews each group's surfaces the branch's way: each meeting point of two
pieces is one vertex, where most piece ends there agree, and a line or clamped
B-spline that misses it is rebuilt to end there exactly. Main's limits on which
gaps are one corner written loosely (0.1 mm, 5% of the shorter piece) decide
which meeting points are joined; the stated accuracy stays the file's. A trim
curve's first piece starts at its end away from the second when no start point
is given.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The IGES reader (#1609) writes each group and subfigure instance's placement as a
CARTESIAN_TRANSFORMATION_OPERATOR_3D_NON_UNIFORM. The branch's STEP reader took
only the uniform operator, and only its origin: every IGES assembly placement
was refused ("placement location: expected a reference") and put at the origin.
An operator, uniform or not, is now read as main reads it: its axes (the global
ones where omitted), its scales (1, and the first for the second and third) and
its origin, refused when singular. A placement may then scale, so the inverse
that maps one placement onto another inverts the linear part by its adjugate
instead of transposing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
Main gave the Thread feature two things the branch's thread rework (forms,
hands, starts, extents, runouts, tips on ThreadAsk) was written without:
- Tapped holes take catalogue sizes (ISO metric and the catalogued UNC/UNF,
G and Rp sizes): `ThreadAsk::tapped` reads the designation from the tapped
catalogue, names the thread as the catalogue does, and fits a size that is
not metric to the hole drilled for it (its tap drill). The hole feature
asks for its thread this way.
- A modelled external thread on a shaft thicker than it turns the shaft down
to its major diameter first (#4256): main's `thread_turn` makes the ring,
the shaft is cut by it, and the grooves and a chamfered tip are cut at the
turned radius. Such a shaft fits the thread.
ThreadInfo keeps the radius of the face it is on, which main's cosmetic
thread display draws at (0 in a state saved without it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The branch rebased onto main keeps each of its commits; where both sides
changed the same code, these are the pieces neither side's commit had.
- Cargo: one [features] table in the CLI's manifest (two met in the merge).
- geom: a NURBS surface's `closed` set once in a placed copy (both sides
added it).
- kernel: the whole-face extrude tools (#2040) work around truck's boolean
on truck types, which left the workspace with the SMLib kernel; the
module, its tests and the doc crate's tool and tests go. The display
planner's flat fill (tess/fallback.rs) has no caller with the schema 4
mesher, and the tessellator's seam and uv modules are the branch's. A
body's boundary tolerance from a STEP file belongs to the truck body the
schema 4 record replaced: `with_boundary_tol` goes. `unmeshed_faces`
counts the coarse mesh as the branch does. A loft's section re-split
passes the lead's perimeter shares to main's `resample` (a circle breaks
there when the matched corners do not go round in order). Main's tests of
the mesh-based measure (the SMLib kernel measures exactly) and of the
IGES preprocessor name (no such field on the branch's import record) go.
- doc: a feature result carries no boolean diagnostics on the branch, so
the model cache neither writes nor reads them, and a pattern copy's miss
test runs the intersect as it is. A placed body's worker result is a
Result: a body taken from the previous placement is Ok. A thread's
clipboard remap takes main's arguments, the tapped catalogue names a
metric size by ThreadForm::Metric, and main's tests unwrap the placing
calls that return their error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The r7 on the vertical edge runs on (tangent chain) up the side's arc on the
r5 round, a circle of radius 5: its contact line on that face would be the arc
offset 7 towards its centre, so no such round exists and the right answer is
a clean refusal. 018 picks a radius that fits (r2: 11 faces, inside the box).
cases.py now writes 013 (the corner setback, graded by its convexity bounds)
instead of marking it missing; its tolerance is the interval's exact half
width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
003 and 004 passed on one valid body alone. 003 is now the flange less
its M30 groove (stopped clear of the root round, run out through the
chamfer) and three M8 tap drills; 004 is 10/008 less the M16 groove on
the shelled boss, which cuts the wall only. With bboxes. Both hold on
SMLib (worst 1.4e-7 relative).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It passed on one valid body alone. Each d3 vent goes through the shelled
top skin into the cavity and meets nothing else, so the case is 09/011
less 4 pi 1.5^2 1.5; with the bbox. Holds on SMLib (1e-6 relative).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
010, 011 and 014 passed on one valid body alone. 010 (top rounds in
mitres, shelled open below) and 014 (drafted box with tilted rounds:
every level section loses the same elliptic corner) now check closed-form
volumes, exact counts and bbox (014 also its centre of mass). 011, the
running part shelled 1.5, checks the part less its cavity built feature
by feature, the offset faces meeting sharp at concave edges as in the
rest of the tier (a distance-based offset would differ by 0.3%). All hold
on SMLib (worst 1e-6 relative).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It passed on one valid body alone. It now checks the chamfer's prism, the
two r2 rounds below it and the columns they carry on up into the chamfer
face (the section's first moment), with exact counts and bbox. Holds on
SMLib (8e-9 relative).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
010, 011 and 012 passed on one valid body alone. 010 (r7 then an r3 chain
round its arc) and 011 (two r5 rounds in a mitre) now check closed-form
volumes, counts and bbox; 012's variable round takes only its edge and
runs on into the vertical round's column, so its volume is graded as the
interval between that end carried on as a constant r5 tube and as its
growing radius. All hold on SMLib (worst 4e-8 relative; 012 at the
growing-radius end).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
010 passed on one valid body alone: it now checks the tier-05 part less
the M16 groove (stopped w above the boss's shoulder, run out at its free
top) and the bbox. Two new cases with closed forms: 018, an M10 cut both
hands, whose crossing grooves leave the material fraction squared
(2 pi L int r m^2 dr, exact for L a whole number of pitches); 019, a
two-start Tr40x14(P7), six pitches of the Tr40x7 section. All hold on
SMLib (worst 2e-6 relative).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
07, 08, 09, 11 and 12 passed on one valid body alone. Each now checks
its volume, bbox and (where derived) counts, area and centre of mass:
the three-section loft is the degree-2 interpolant r = 10 + (z-20)^2/40;
the square-to-circle loft a ruled prismatoid, its mixed area from the
sides ruled to rational quarter arcs; the periodic fit spline the cyclic
C2 chord-length cubic; the pleated skins star polygons on fixed rays,
their crossing integrated exactly per wedge; the cap's union integrated
as y-intervals over x and z. All hold on SMLib (worst 9e-8 relative).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generator had fallen behind the tree: it no longer wrote 02/021-025,
03/010, 04/011-012 and 09/012-015, and its 07/016 lacked the second round's
tangent_chain. Each is now built in cases.py; the two reporters' scripts
(04/011, 04/012) are kept verbatim in reported.py. Regenerating reproduces
the tree exactly; the only file changes are the generator's own format
(volumes to 9 decimals, one-space indent, a final newline), all under 1e-8
relative.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three r5 rounds at (60,40,20) set back 8: the volume between the convex
hull of the body on the corner cube's faces and the rounds' prisms (both
bounds closed form or a sampled hull below the true one), the box's bbox,
and SMLib's setback-patch topology (15 faces, 36 edges, 23 vertices).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Each (point, distance) sets back the corner at the body's nearest vertex: its
patch starts that far along every edge there. Tested on the 60 x 40 x 20 box
corner with r5 rounds set back 8 (volume between the convex hull and the
rounds' prisms bounds, 15/36/23 topology, the box's bbox) and on refusals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At a pole or apex `EvalSurface::eval` takes the normal from a step beside the
point, trying (+h, +h) first. At the far end of an interval (a sphere's north
pole at u = 1) that step lands past the end, where the extrapolated surface
folds back over the pole, and the normal came out turned over: the pole vertex
shaded dark with a light ring around it (ladder 01/005, every sphere and dome).
Steps now stay inside the active interval of a spline or a revolution's
profile. The pole test checked only that the normal was radial, either way;
it now checks the side too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recorded on SMLib replay 2c5baae (screw sweep walls exact and within their
tolerance, the radial sweep, the point solve on long rational surfaces) with
the thread rework: 06/008 (two starts) and 06/009 (runout) now graded and
passing, 06/011-017 new (Tr, Acme, S, ASME buttress, an internal Tr nut,
left hand, the chamfered tip), and 06/003, 004 and 007 grading the extent and
the collar. 132/135 pass; left: 07/013 corner setback, 07/015 G2 corners
(unsupported) and 07/016 three sequential corner rounds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sizes, designations and the face's size come from ThreadForm (sizes, parse,
format) and suggested_thread_in, per form; the dialog's own trapezoidal and
buttress lists are gone. Acme sizes are listed by designation. The square
thread, which the command refuses (no standard defines it), is not offered.
Editing a thread reopens its form, hand, starts, extent (length or up to, with
its pick), start end, runouts, tip and steep flank, and OK gives them back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
`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>
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>
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>
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>
Form (ISO metric, trapezoidal, Acme, ISO and ASME buttress, square), size and
pitch from the form's list, hand, starts, extent (full, length or up to, with
the end to start from), ease in and out (standard, short or a length), the
chamfered tip and the buttress steep flank. Only the fields that apply are
shown; a form change keeps the options and a manual size's diameter where
the new form lists it. Defaults stay out of the command, so a plain thread
means what it did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ISO metric table, the thread on a cylindrical face, the groove shape and the thread
feature's evaluation move from eval.rs to eval_thread.rs, unchanged, before the extents,
forms and starts work changes them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Three new cases in closed form: two through holes touching at a point
(02/021), the stock shelled with no face open (09/012, a sealed void, two
shells) and a dome shelled from its flat base (09/013, its rim the circle
r18). Nine rig cases copied into their tiers: a cylinder grazing a hole's
edge (1279), lips 1e-4 off a base's walls (2928), wall halves merged into a
flange (494), flush chord layers (5197), the horn torus revolve (4007),
crossing pleated loft skins (4623), loft ribs joined to a rounded pad (4092),
a drafted box with rounds shelled (3576) and a shelled dome with its ring
joined back (4847). Golden recorded on SMLib replay 8017df9: 123/128 pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
The case's base is a stadium ring (r 3..5 arcs, centres 20 apart, z 0..2),
as its description and expected volume (2 (80 + 16 pi) for the base) say,
but its extrude picked no profile, and "all profiles" is every drawn region:
the ring and the stadium island inside it. The base came out filled and the
join 1066 mm3. SMLib's union was exact for those operands. The extrude now
picks the ring (the region at (4, 0)).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The record's torus label is a ring torus (major > minor; the validator
refuses any other). SMLib keeps horn and spindle tori: a half disc revolved
about a line through its rim's end is a horn torus, its major radius a
rounding below its minor, and the export wrote it as a torus label the
record then refused (rig sweeps/4007, "ring torus radii"). Such a face is
now its NURBS form, unlabelled, which is its geometry in any case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Where a face's loops pass a vertex more than once (two holes touching at a
point, a loop through another hole's corner), each pass took its parameters
from the pcurve of the edge leaving it, and the two pcurves round them 1e-14
apart. The triangulation got two nodes 1e-14 apart with constraints crossing
between them and refused the face ("boundary crosses itself"): rig 307, 3092
and 3887. A pass whose parameters agree with the vertex's first pass to that
rounding (1e-9 relative) now takes the first's; farther apart they are two
places of the surface (a seam's two sides, a closed spline's ends).
Rig 494 now fails: its revolved face has a dead-end edge walked down and back
up in one loop, which met the triangulation as a needle 1e-14 wide and now as
the slit it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fillet_max bisected the radius down on any error of a candidate fillet,
not only KernelError::TooLarge, and fillet_max_each fell back to one
radius for all on any error of a group or of the combined fillet. A
kernel failure at the radius asked for (or at any candidate) came back
as a smaller radius reported as "the largest that fits".
fillet_max and fillet_max_each now return Result<Option<..>>: only
TooLarge narrows the search or falls back to one radius, and any other
error is passed on. Ok(None) is still "no radius fits". The doc's
FilletFit::Reduce passes the error on. The test of a point with no edge
near it now expects the fillet's own error, not "no radius".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pieces of an IGES trim curve run on from one another, and the sewer made
each meeting point one vertex, but left a piece that missed it where it was,
the gap on the vertex's tolerance: an edge 0.01 mm short of its corner, 2000
times the file's resolution, which SMLib refuses (SmTrimmingTools #0, SmBrep
line or clamped B-spline piece that misses it is made to end there exactly;
any other piece that misses keeps its ends, and the gap is reported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gDVmStT5tCGKW9Fxgc5K
A cone face bounded by its base circle and its seam out to the apex and back
turns once round the axis and closes at the apex. add_seams counted the turn
as a loop without a seam and added another, out and back from the same
vertex: the drill point of a turned pin read back from STEP had a sliver
whose boundary crossed itself, and the body would not tessellate (#5193). A
winding loop with a vertex on a pole is left as it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gDVmStT5tCGKW9Fxgc5K
form::extent took the box of 32 points per edge. A face's NURBS form is built
over that box, so where the boundary peaks between two samples the form
stopped short of its edge: the re-imported rod's bore ended at v = 9.99977
while its intersection with the rod reaches 10, and SMLib found the edge
1.9e-4 off the bore (SmBrep #4, SmEdge #13). Each sample higher or lower
than its neighbours now brackets the edge's extreme there, found by golden
section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gDVmStT5tCGKW9Fxgc5K
form::extent unwrapped each loop of a face from its own first point, so a
hole beside the seam (a cross bore through a rod at the rod's seam angle)
came out a period away from the outer loop. The box over both was more than a
turn, cut to one turn from the hole: the face's NURBS form ran over quarters
3..7 while its seam uses lay at 4 and 8, and SMLib found the re-imported
rod's edge ends outside its face domain (SmBrep #70-#72). Each loop after the
first now goes the whole periods along that keep the face's box smallest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gDVmStT5tCGKW9Fxgc5K
tangent_chain followed tangent edges without looking at their convexity: a boss's concave base
arc that runs on tangentially as the convex rim of the overhang above it (rig fillets/4174,
4369) and a convex outline running into the concave edges under an overhang (4365) were sent to
SMLib as one chain. No round can pass from one side of the faces to the other, the cases'
answers stop at that vertex, and SMLib rightly refused the request (1001 in its Nx2 corner).
A chain no longer runs on into an edge of the other convexity: convex(e, v) tells, from the
second face's natural normal, the loop's direction and the first face's normal, whether the
faces meet at less than 180 degrees inside the body. With SMLib's corner and end fixes this
makes rig 4174, 4365 and 4369 pass with their analytic answers; nothing else changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gDVmStT5tCGKW9Fxgc5K
Rig case shell/4711 (a 60 x 40 x 10 block, an r3 through hole at (22.5, 7), a 10 x 10 notch on
the front edge, shelled 2 open at the bottom) failed with "SMLib shell: operation: SMLib failed
(status 1027)". The hole is 4 = 2 x 2 from the front face: the hole wall's offset (r5) is tangent
to the front's at (22.5, 2), the walls meet along a line and the cavity would pinch to zero
thickness there, which is no solid. Refusing it is right; the refusal did not say why.
SMLib now refuses an offset whose walls would meet with its own status, SMLIB_ERR_WALLS_TOUCH
(-1110). The smlib crate reads it as Error::WallsTouch; the kernel's offset backend turns it into
a sentence with the wall, as blends turn NoRoom into one: "shell: the 2 mm walls would meet along
a line (between a hole and the side, for example), which leaves no solid there; use a thinner or
thicker wall, or move the feature". Thicker builds too (SMLib shells 4711 at 2.5, the walls
crossing and merging between the hole and the front), so the sentence offers both.
4711 expects that refusal and its message ("outcome": "refused", "refusal"), its "what" and
"method" saying why: the wall is 4 = 2 x 2, the walls meet along a line. Tests: the smlib crate
shells 4711 built from primitives (2 refused as WallsTouch, 1.5 and 2.5 valid), the kernel a
plate whose hole is 3 from its side at 1.5 (refused, with "the 1.5 mm walls would meet along a
line").
Results (run-sc.sh against SMLib a759622's code, which differs from the tree run only in
comments and docs; baseline 858f381fc-5448b9a): rig 410/447 (409
before; 4711 fail -> pass as the refusal, nothing else changed), ladder 109/116 (unchanged),
kernel lib 535 passed / 202 failed (one new test; the same failing set), smlib crate 53 / 2
(one new test; the same failing set).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gDVmStT5tCGKW9Fxgc5K
SMLib now builds a round or chamfer whose rail runs off its face across a cliff (an opening's
rim, the body's outline): the blend is made as if its faces continued and what it removes is
subtracted, so it never bridges an opening (owner decision for rig 5158/5181; fork commit
"SMLib, capi: a convex blend overflowing a cliff is subtracted from the body"). The tests that
expected a refusal for such blends now expect the body, each answer from the geometry:
- variable_radius_on_a_box_edge: 2 -> 50 on the 10x20x30 box's top edge builds at
3546.18767939 (circular sections in y = const, the r x r square less the quarter disc,
clipped to the box; rel 1e-4).
- variable_radius_from_a_spline: the "big" law builds at 6000 - 726.5334566720 (de Boor law,
split at the knot and where r crosses 10).
- a_chamfer_meets_sloped_end_planes: chamfer 25 removes 11500 - 4625 * slope
(integral of 40 - 2 * slope * y over z >= y - 5).
- g2_fillet_refuses_invalid_geometry: G2 at 25 builds at 6138.1606889 (the quintic section
with control points at thirds, clipped to 20x20, along 20).
- fillet_max_each_reduces_only_where_the_radius_does_not_fit: both edges take 4, 24438.7317761.
- hostile_inputs_are_errors: chamfer 30 on the cube builds at 1000 (y + z <= 10 along 20);
R50 still fails.
- kernel-cases fillets/1080: the 9 mm chamfer into the openings builds at 3350
(method in expected.json).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gDVmStT5tCGKW9Fxgc5K
Cases from the issues filed after #4160, each a synthetic recipe with its own answer
(analysis or invariants); every other issue in the range is listed with why it was skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cases from the issues filed after #4160, each a synthetic recipe with its own answer
(analysis or invariants); every other issue in the range is listed with why it was skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cases from the issues filed after #4160, each a synthetic recipe with its own answer
(analysis or invariants); every other issue in the range is listed with why it was skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cases from the issues filed after #4160, each a synthetic recipe with its own answer
(analysis or invariants); every other issue in the range is listed with why it was skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where a cut-out's corner touches the outline (a triangle on the sheet's edge in a bend), four
walls meet along the corner's line. The touching sew paired them round the empty wedges and
gave each fan at the corner its own vertex, so a cap kept its outline and the cut-out as two
loops with a vertex each at one point: SMLib refuses that face (coincident vertices, loops
touching: its face tests 32, 38 and 39), and the bend face's two corners, a hair apart in
(u, v), left a boundary no triangulation follows.
The walls are paired round the wedges of material, as SMLib keeps a solid touching itself along
a line: an edge per wedge, the two sharing their end vertices. A face's loops that meet at a
vertex are joined there into one loop through it twice, SMLib's form of such a face.
With SMLib's face builder taking the same profile (an extrusion of the plate and its
cut-out), fixes rig sheet-metal/1095.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0bf765aa815a5552e5b090fd8854b1dc7a981e3f)
A loft cut from a block's whole outline (flush with its sides) down to a pad cuts the slab
above it loose: the slab and the rest touch along the outline's four edges, round the pocket
the loft leaves. SMLib keeps the pocket as a void region of its own (its check finds the body
valid): its one runtime shell is the slab's underside and the loft's faces, so the slab's
material shell faces two empty runtime shells, the infinite region's and the pocket's, and so
does the rest's. The export made one record shell per pair of empty and material runtime
shells, so each solid's closed boundary came out as two open shells ("an open, wire or vertex
shell lies in one region").
A record shell is now one material runtime shell, whatever empty shells its faces face. Empty
regions one solid's boundary faces together are one record region: the infinite region where
it is among them, else the void whose outer shell bounds it alone. The pocket, which no record
shell bounds once the solids' shared edges are split, is part of the empty space round them.
Fixes rig sweeps/96-loft-cut-box-sections-pad-first and -top-first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 796920968265c57c59f11dd908ec022d53f5c647)
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>
Each edge is sampled to the chord tolerance on its own, so where two curves of one face run
closer together than their chords sag (the two arcs of a thin lune, the sliver caps of a
joined ring of cylinder segments) the chords crossed although the curves do not, and no
triangulation follows such a boundary ("boundary crosses itself").
After the edges are sampled, every face's boundary is checked for chords that cross another
of the face, in its loop or another, and on a closed surface across whole periods (chords
ending on a pole are left out). The edge segments that cross are halved at their curve
parameter's middle, on the edge, so every face of the edge gets the same new nodes, and the
faces of the edges split are checked again until no chords cross. A boundary still crossing
after 16 rounds, or an edge past its node cap, is an error naming the face and the edges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
(cherry picked from commit d2b2bdb8e2720690d79123a16bc18c768b1598e8)
Every edge segment now spans at most a quarter turn of its curve, whatever the chord
tolerance: a hole far below the tolerance (a 1 mm hole through a 990 mm plate at a 2 mm
chord) stays a polygon with area, where each half arc had been one chord and the two
chords lay on one line, a hole of no area.
The boundary check takes boundary nodes at the very same (u, v) as one vertex, as the
triangulation does: two loops touching at a vertex they share (a ring standing on a
perforated plate, tangent to the holes beside it) bound a face the triangles follow. A
seam's two uses, one point in space a period apart in (u, v), stay two.
Fixes rig booleans/3665 and sheet-metal/1040.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
(cherry picked from commit 53f4f9a2a991fcd6d1374ca2d647e3a1be36554f)
The r3 on the top edge at x = 60 takes tangent_chain: false, so it
rounds that edge alone and not the r5 round's tangent chain with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
(cherry picked from commit cf46e2a713c45ae0427e831cae1dc1d3b858febc)
seam_flags matched only an edge between two faces on one curved surface.
A whole turn of a cylinder is one face whose seam it uses twice, and
history::edge_faces lists that face once, so the seam came out as [x]
and was never flagged: it was drawn and picked, and a blend pick at the
vertex where it meets a rim (ladder 07-fillets/017's (23, 15, 20) on a
boss's base circle) resolved to the seam, which SMLib then rejected. An
edge both of whose uses lie on one curved face is now a seam too, for
every consumer of the flag (display, picking, blend resolve, projection).
The blend's own edge selection took the plain nearest edge: the tangent
chain seeded from it and grew into seams (a pick at the top of a boss's
seam ran down it into the base round's seam, ladder 08-chamfers/008), and
the variable fillet's edge and the fillet's grouping of picks could
differ from the edge the blend resolved. They now take the edges the
blend's resolve picks, and a chain never grows into a seam or a smooth
join.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
(cherry picked from commit 709c19c7fd60db522d35e147f21821360bf34668)
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)
A feature's touching parameter says what a join makes of solids that
share no volume and no area: separate (the default) splits the joined
body's pieces that share no face into bodies of their own, named, with
history and lineage, as a cut's pieces are; one_body keeps one body of
several pieces. The state records how the kept-apart pieces met
(KeptSeparate: along an edge, at a point, apart) for the feature's
notice.
Older builds kept every join's pieces in one body: the pending format
change join-touching-bodies gives each joining feature of an older design
touching: one_body, so existing designs keep their bodies.
kernel-cases 2896-join-arm-end-near-ring-wall-gap: the arm meets nothing,
so it now stays a body of its own (body_count 2, one shell each).
kernel-cases that join solids apart to make the body they test ask for
it with touching: one_body: 3379 (the hub joined to the ring before the
spokes reach it), 3455 (64 pegs in one body) and 3526 (a two-lump target
for an intersect).
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 2c2aaed86f4be60f7f88e40bc24b60de60085148)
Brep::region_contacts lists each pair of material regions that shares a
face, an edge (used by faces of both) or a vertex, with the most it
shares; pairs not listed are apart. Body::region_contacts reports it for
a boolean's result (lump positions as in Body::lumps), and Body::parts
cuts a body into groups of lumps (lumps is parts of one each). Exact
facts from the record only: no geometry, no decision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
(cherry picked from commit 64a1d688e68e5f0c5b1f9e4fe0d2facfaa4744be)
Every edge of the hand-written cube was a LINE along one filler direction,
trimmed by vertices up to 1 mm off it; the reader refuses such an edge (a
degenerate edge on a line). Each edge now has the line through its two
vertices, so the assembly, instancing and unit tests read a valid cube.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
of_faces gave every edge it was not told about the meeting of its two faces.
A builder (extrude, revolve, loft, a primitive) has no operands: every face it
makes is new, so its edges are new too (EdgeProv: empty when it is new and lies
between faces new to the operation), not where profile segments meet. With no
operands an edge's origins are only what the builder states.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
The display's smooth joins were certified only between ruled B-spline patches
along a Line record. SMLib gives an extruded profile's rulings as degree-1
B-splines of two control points, and sweeps a profile curve within tolerance of
an arc as a cylinder (a ring of 256 cubics is 512 cylinder faces), so no join
was certified. A straight edge is a line or such a B-spline; a plane, cylinder or
cone gives its outward normal along the edge in closed form at both ends (their
difference is the uncertainty, so an edge that is no ruling certifies nothing).
The closed-edge fixture cuts its hole with one circle: SMLib trims a loop of
eight cubics as eight edges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
The kernel blends exactly the edges given; the Fillet and Chamfer features
expand each pick to its tangent chain (kernel::tangent_chain) before the call
(4593fd07). Six tests written before that passed one pick and expected the
chain: they now ask for it as the features do, with their analytic expectations
unchanged (10 faces along the rounded corner, the sphere corners of #1557, the
boss's half circle, the rounded rectangle's tiny end, the rounded box's loops).
blend_smlib's notes no longer say SMLib follows the chains.
fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole is left as it
is: it fillets every exact subset of the outer edges, and SMLib still fails some.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
A degree-1 patch of four control points on a parallelogram (a plane as an IGES
or STEP B-spline writes it) is that plane at every parameter, so its pcurves
take the plane's closed forms instead of going to SMLib (which drew the rim
circle of 3806's base as a 385-point spline).
SMLib may run a one-face sheet's loop against ours. A use's pcurve is the same
either way, so the loop's way round is read from an edge the face uses once and
every use is matched with it (3806 failed with 'a use SMLib did not keep').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
A use with no closed-form pcurve (an approximated intersection curve on a cone,
a spline edge on a spline surface) no longer refuses its solid. Its face goes to
SMLib alone, an open sheet with the trims it has and without the others, and
SMLib makes those as it makes every trim it needs (GetOrCreateUVTrimCurve on
export); they come back on the record's parameters and the face is closed and
checked as before. The STEP reader takes the trim maker from the kernel
(read_solids_with, read_solid_with); read_solids stays exact-only.
Kernel lib tests: 490 pass, 239 fail (were 468 / 261; pushed head 475 / 254),
none newly failing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
exact_pcurves gives each use's pcurve where it has a closed form (or is given)
and None where it has none, without closing the loops; pcurves_within closes
them once every use has its pcurve. TrimFace, TrimEdge, TrimUse and the Trims
trait carry a face to a kernel that makes the pcurves of the uses with none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
along took u or v the edge's own parameter at slope ±1. A rim SMLib makes (an
extruded or revolved circle, four quarters with scaled knots) runs the face's
quarter grid at another steady pace, and a closed rim's two ends are one point,
so neither its ends nor slope ±1 give its line. The pace is read from the edge's
points a quarter and three quarters along, unwrapped from the middle; the line
is taken only where it lifts onto the edge within the tolerance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
Validation refuses a circle or ellipse edge, or a cylinder, cone, sphere,
torus or revolution face, without its NURBS form. Test fixtures are built on
forms; seam and domain defects use the form's parameter (one turn = 4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
`Assembly::finish` (lip, emboss, delete face, and the other operations that take a body apart
and rebuild it) puts on a NURBS form every cylinder, cone, sphere, torus or revolution face that
none of its uses brings a pcurve to (over its boundary's extent on its label), and every edge
whose uses bring none and whose faces are planes, splines or on forms (a circle about such a
face's axis labelled on its frame). Their pcurves are then exact on the forms. A face or edge
with a pcurve of its own keeps the parameters that pcurve is on.
Rig and ladder unchanged (333/372, 101/116; 529 and 3368 flip between pass and a 40 s timeout
with the machine's load). The kernel tests of lip, emboss, delete face and assemble fail as
before (none new).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
sew and the STEP reader build a revolved face's NURBS form over `form::extent` (its boundary on
its label) instead of a pass of pcurves on the label, and compute its pcurves within the form's
box. The STEP reader gives revolutions their form too, and a trimmed swept profile its file's
trim range exactly (points on the axis fit either side of it). The STEP seam tests read edge
angles through `Edge::curve_interval`, intervals and domains being in the forms' parameters.
step 37/37.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
- A meridian of a sphere or a tube circle of a torus (a circle in a plane through the axis) has
its exact pcurve on the label: u fixed at the arc's azimuth, v its angle from the outward
radial, alpha + s t.
- A placed revolution has a NURBS form: its profile's control points turned about the axis on
the quarter grid from angle zero, weights multiplied, then its matrix; the record takes forms on
revolutions.
- `form::extent`: a face's parameter box from its boundary projected on its label (the collapsed
parameter at a pole bounds nothing; a boundary that never goes round covers the turn), what its
form is built over with no pcurve involved.
- A face that goes all the way round gets a form of exactly one turn, closed and so periodic.
- `along` projects inside the face's box (a spline extended past its knots, or a profile turned
past the face, has other preimages), and takes a closed form within the record's tolerance, as
the closed forms on analytic surfaces are.
Tests: fits are gone, so curves with no closed form on their face (a tilted circle on a sphere,
an ellipse on a cylinder, an intersection record, a curve weaving across a thread flank) are
asserted refused; the closed-spline fixtures are built as forms. geom 166/166.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
`On::fit` and everything it used (samples, least squares, breaks, filled poles) are deleted, and
the STEP reader no longer retries a face at ten times its tolerance. A pcurve is exact (closed
form, an affine image, or a straight line along a form's parameters) or the face is refused:
"not supported yet: no exact pcurve of <curve> on <surface>".
The rig and ladder are as with the fit (333/372, 101/116; none lost; 3806, already failing, now
refused on import). 16 unit tests that build sphere meridians, torus seams, closed spline
bands, intersection curves and surfaces of revolution on their labels now refuse: those paths
need meridians labelled on their face's meridian plane and revolutions on the quarter grid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
A surface of linear extrusion or of revolution over a circle or ellipse built its profile with the
reader's own conic spline, on a parameter of its own; the profile is now `form::curve`, the form
its edges carry, so the surface's parameter along the profile is theirs and their pcurves are
straight. The reader's conic spline is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
SMLib holds every ellipse as an exact rational NURBS; the fork now sends ellipse edges with that
form and takes them back with it. The export reads an ellipse with its form as it reads a circle
with one (its frame turned so its angle grows along the form, its trims carried over as they are);
the import writes an ellipse on its form as its conic entry plus the NURBS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
`form::edge` is the one rule sew and the STEP reader share: a circle about a revolved face's
axis is labelled on that face's frame, and a circle or ellipse edge gets its form. The STEP
reader now gives every imported edge its label and form before any pcurve, and every
cylinder, cone, sphere and torus face its form over the extent its label's pcurves give; the
pcurves, the outer-loop choice and the turn-over test then run on the forms, and the record
carries them.
With the pcurve fit turned off, the STEP and IGES circle cases pass (8 of the 9 import cases
that needed it; 3208's ellipse on an extruded spline surface remains).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
A flat-pattern edge folded across a bend is a helix, which no NURBS is: sheet metal already
gives its 3D curve as degree-7 Hermite pieces of the fold, within 1e-7 mm. Its pcurve on the
bend is now built the same way, on the edge's own parameter: degree-7 Hermite pieces matching
the point and first three derivatives of its angle about the axis (through the form's quarter
parameter, by the chain rule) and of its height along it, broken where the angle crosses a
quarter (the form's parameter is only once differentiable there), halved until they lift onto
the edge within 1e-7 of its size. An approximation by construction from exact derivatives, like
the edge; nothing is fitted. B-spline and NURBS edges alike (a NURBS's derivatives from its
homogeneous form). Breaks within rounding of each other are one.
With the pcurve fit turned off, all 44 sew cases that needed it pass; with it on, all 44 pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
The bend's rim arcs carried frames of their own (x toward the arc's start), a fraction of a
quarter from the bend cylinder's, so their forms were off the cylinder's quarter grid and no
straight pcurve lay along them. `form::on_frame` relabels such a circle on the face's frame (the
same circle, its interval shifted); its form is then on the face's grid, its pcurve u = ±s + c.
`along` takes u or v as the edge's parameter either way along, offset. The volume test steps to
a pcurve's ends exactly (t0(1-f) + t1 f), as an interval's end may now carry rounding.
With the fit off, 38 of the 44 sew cases pass (from 29). With it on, the rig is unchanged
(333/372, ladder 101/116, no case changed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
Every circle and ellipse edge sew builds now carries its NURBS form, its interval in the form's
parameter; every cylinder, cone, sphere and torus face its form over the extent its label's
pcurves give; and every pcurve is computed on the forms, so a bend's rim circle is a straight
line on the bend and an affine image on the flange, exactly. The volume test evaluates the faces
on their forms. A form interval's end within rounding past its last quarter stays on it.
With the pcurve fit turned off, 29 of the 44 sew cases that needed it pass (from none); with it
on, all 44 pass as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
On a plane, a B-spline or NURBS edge's control points go through the plane's affine parameters
and the result is cut exactly (bspline::segment) to the edge's interval, so a circle's form gives
its exact pcurve on any plane. On a spline face (a NURBS form), an edge that runs along the face's
parameters at its own pace gets the straight (u, v) line it is: between its ends' parameters, or
with u or v its own parameter (a rim on the same quarter grid). Each is taken only where it lifts
onto the edge to within 1e-9 of the geometry's size; nothing is fitted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
A form's parameter is now the angle's quarter on its frame plus the arc's parameter in it, and a
form spans the quarters its interval needs (up to five for a turn). Every form on one frame has
the same parameter at the same angle, so a circle edge and the cylinder it bounds agree along it,
and an arc across the frame's x axis, or starting anywhere, has a form. Traces its label to 1e-14
on such arcs, negative angles and a turn from 30 degrees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
`brep::form` builds each analytic kind's exact B-spline the way SMLib holds its own: a circle
or ellipse as four rational quadratic quarter arcs from its frame's x axis on [0, 4] (angle and
parameter related in closed form), a cylinder or cone as that circle times a line along the axis,
a sphere as the circle times a half circle of latitude, a torus as the circle times its tube's
circle, a line as the degree-1 B-spline on its own parameter. A circle and every face it bounds
then share one parameterisation. Each traces its label to 1e-14.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
The assembly kept a face's domain and pcurves, which are in its NURBS form's parameters, but
dropped the form and computed new pcurves and the domain on the analytic surface. The form now
travels with the face, and its pcurves and domain are computed on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shell's `forward` is about the surface the face's (u, v) are on, its NURBS form when it has
one, which may run against its cylinder, cone, sphere or torus. The kernel read it against the
analytic surface, so a fillet's bore whose form runs the other way was a shaft and an M10 thread
was refused in its 8.5 mm hole. `Face::form_turns` says when the form turns the normal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No floor of ours on tolerances (1e-6 of the body's size, 30 times SMLib's 1e-5 default): the
record's tolerances go back into SMLib, which approximates to them, so the floor coarsened every
later operation. No recognition of NURBS edges as lines or circles: an edge is a circle where
SMLib holds an SmCircle (with its NURBS form), and a NURBS edge keeps SMLib's trims on its own
parameter. crashes/2992 and sweeps/1853 pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edge::curve_interval (and brep::curve_interval for assemble's edges) gives the interval on the
edge's curve itself: a form circle's angles of its ends, its frame turned by the SMLib conversion
so the angle grows along the NURBS. lip and the ruled surfaces read arcs by it; the hull, the
blend tangents, the patch pcurves, point paths, IGES output and the prism runs evaluate on
Edge::parameters. assemble carries an edge's form through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SMLib conversion reads and writes EDGE_NURBS_FORM: a circle edge's record keeps the circle
as what it is and the NURBS SMLib holds it as as its parameter, so the trims on it carry over
exactly (TrimMap identity).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The edge mirror of a face's NURBS form: Edge::nurbs keeps the NURBS SMLib holds a conic as, and
the edge's interval and its pcurves' parameter are its. Edge::parameters is the curve every
evaluation at a parameter uses; curve says what the edge is. Nothing writes the form yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prism.rs's sheet regions import Surface and Vec3 only where they are built (the smlib feature),
so the kernel without it has no unused imports; the cli's tests that measure meshes at their own
tolerance depend on solidcraft-kernel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
An edge whose two faces meet tangentially has no corner to round, and SMLib refuses a blend along
it. The chain now runs on sharp edges only, as a tangent chain does (fillets/3369: a rim edge that
ends on the tangent seam of a stadium).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A whole circle is one closed edge now, so the outer wall and the hole wall each have a seam; the
rig counts seam edges (4005, 4006), and 1c490ddd's count without them broke those cases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
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
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
The kernel blends exactly the edges given; the feature's tangent_chain (default true, saved
only when false: pending format note blend-tangent-chain) expands each pick to its tangent
chain (kernel::tangent_chain) before the kernel call. A variable or spline round takes its
edge alone. Scripts and MCP passing one pick get the chain again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Every SMLib analytic surface is a B-spline surface, and its trims are in that NURBS's
parameters. Face::nurbs keeps that NURBS: the face's domain and pcurves are its (u, v), and
`surface` says what the face is. Face::parameters is the surface every (u, v) evaluation uses.
The SMLib conversion reads and writes it (fork FACE_NURBS_FORM, SMLIB_REV c4483f6); the STEP
writer writes the analytic surface and the face's edges without pcurves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
SMLib (fork 68e3086) refuses a blend whose rails leave the faces they roll on with
SMLIB_ERR_NO_ROOM, which the kernel reports as KernelError::TooLarge. The kernel's own
pre-checks go: blend_fit's fit check and the 256-station band check of the variable fillet.
The shim no longer follows tangent chains; the Fillet/Chamfer dialog already expands a pick
to its chain (its Tangent Chain option) and stores every edge in the feature.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Exporters write a plane as a CONICAL_SURFACE whose semi-angle falls a whisker
short of 90 degrees (#4006: pi/2 - 7e-12): its height over the face spans
4e-11, a parameterisation ill-conditioned for everything downstream. The
reader now measures the cone's deviation from its limit plane (the plane
through the placement across the axis) over the face's boundary points and,
should the face hold the axis, the apex; within the file's distance
uncertainty the face's surface is that plane, oriented as STEP's normal.
One criterion, the deviation over the extent against the tolerance; no
semi-angle threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Pinned at the fork's draft-corners head: a drafted rectangle (or any
line-line corner) has its walls meet at a corner edge under the tangent
rule as well, 6 faces rather than 14. A curvature refusal for a curve
that is neither a line nor an arc names that curve's start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Extent.corner (preserve_curvature by default, tangent or round), saved
only when not the default (one pending format change), and the
solid.extrude `corner` parameter. draft_corner_options reports what each
rule gives at each corner of a feature's profiles, for a dialog to offer
the others where the chosen one is a poor fit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
extrude_tapered(_between) take a DraftCorner: PreserveCurvature (each wall
continues as itself, a line's plane or an arc's cone, until it meets its
neighbour; SMLib's SM_OC_CURVE_EXTENSION), Tangent (the offsets continued
along their end tangents) or Round (an arc about the corner). A corner
the rule cannot close is refused with its point and why
(solidcraft_smlib::Error::Corner). draft_corners reports, per sharp
corner of a profile, what each rule gives: how far each offset curve runs
on or is trimmed, whether the corner cuts inside the exact offset, or why
the rule refuses it. SMLib pinned at the fork's draft-corners head.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The angles a cylindrical wall spans came from its arc's two ends about its middle; a wall in
one piece has a whole circle for an edge, whose ends are one point. The circle's parameter is
its angle, so the span is the edge's interval about the angle of its middle: half the turn
either side for a whole wall, as for any arc.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A rim's arc was its two ends and the point halfway, its circle found through the three: a
whole circle, one closed edge now, has one end and no circle through it. Each arc piece carries
its circle's centre and the angle it turns about the face's normal, read off the record circle
(its parameter is the angle), and offsets, profiles, edges and walls are made from those; the
loops' turning is read through points a third and two thirds along each arc. assemble's
unused edge_point goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Loop2::circle and circle_from gave two half arcs, so everything built from a circle came out in
two: an extruded wall in two faces, a rim in two edges, and so every round, chamfer or sweep on
it in two B-spline halves that SMLib cannot join (two separately parameterised arcs meet at
different curve parameters, SmVertex::IsTopologicalVertex; different free-form surfaces are not
topological, SmEdge::IsTopological). A circle is now one arc of a whole turn from its start
angle: one closed edge, one wall with a seam, one round. Sketch circles, the cylinder builder
(circle_from at its seam angle) and every other builder take it through Loop2; SMLib's arc
curves take a whole turn (smlib_sweeps clamps to one) and ArcSweepCylinder makes the whole
cylinder.
Cases, singly on 9f5ad20c: fillets/1191, 2797, 3753 and sweeps/1269, 2295 fail before and pass
now; eight hole and six thread cases give what the base gives (12 pass, booleans/1279,
booleans/307 and threads/2038 fail as on the base).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Seven sheet-metal cases failed in smlib_planar_faces (status 1001/1006). Every refused
curve list, in the rig's dumps, has curves of which more than two meet at one end point:
flange regions of one flat pattern touching at their corners (405, 349 x2, 436, 1481, 725,
723) and an outline passing a point twice where a cut-out's corner lies on the sheet's edge
(1095). SmBrep::CreatePlanarFacesWith3DCurves, behind SmApiCreatePlanarFaces, documents
that it fails for such "spine points (where more than two curves share a common
end-point)" (SmBrep.cpp); the sheet handed it all of an operand's loops at once.
A sheet is now built one simple loop at a time: each loop (outer counter-clockwise, holes
clockwise) split where it passes a point twice, each simple loop its own planar face, the
counter-clockwise ones united and the clockwise ones taken away with SMLib's 2D boolean,
which takes faces touching at a point. extrude_planar_regions uses the same sheet.
The read-back keeps the contract the regions had: a face's loop that passes a point twice
(a cut-out touching the outline) comes back as simple loops, counter-clockwise pieces
material and clockwise ones holes of the piece that holds them.
Release CLI, SOLIDCRAFT_KERNEL=smlib: 405, 349 (72 and 90 degrees), 436, 1481, 723 and 725
pass. 1095 now gets past the planar regions; its extrusion of a plate with a cut-out
touching its edge at one point fails in SMLib's export ("a closed shell bounds two regions
and encloses one"). Every other sheet-metal case fails or passes as in rig-1, and 529
(a hang in rig-1) passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The five "boundary winds around the surface" refusals of the first SMLib rig run (solid.hole
1355, solid.revolve 1853, 1866 at 180°, 3191, 466) are one mechanism: a revolved face whose
profile ends on the axis (a drill point, a cone tip, a half slot about its centreline) has a
pole, and its two meridian edges meet there with pcurves half or a whole period apart in the
angle, the parameter the pole leaves free. The boundary unwrapped every step to the nearest
copy, so at the pole it moved the next meridian a period along (1866's quarter sphere: from π
to 2π instead of 0) or, where the pole closed the loop (3191: v from 1 back to 0), the closing
step did; the loop then drifted a whole period and the face was refused as winding.
The pcurves are exact and run on continuously, so nothing within an edge is unwrapped now.
Where one edge meets the next, a pcurve starting a whole period from where the previous one
ended is moved back by it, except in a direction the meeting point is a pole of: there the
two pcurves' own values are the ends of the boundary's run along the pole, which the split
poles then carry. Each loop starts at a node off every pole whose predecessor is off them too,
so seam::drift's nearest-copy closing step never crosses a pole. All five cases pass;
tessellation/ and sweeps/ otherwise as in the rig run (1856 and 3006 pass, slow under a
machine load of 190; 2552 hangs as before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
SMLib's mass properties no longer refuse bodies that are not one manifold
solid: several lumps and lumps touching along edges or points are the sum
over their material regions, and a sheet is its faces' area and centre with
volume 0. Docs of Brep::mass_properties and kernel::measure say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The ruled surface's far-curve solve kept its own copy of NURBS Book A2.3. The
geom crate now exposes bspline::basis_derivatives (the basis functions alive at
a parameter and their derivatives up to an order, each from its local knots via
local_basis, on the span tess::span_of picks); tess's second-derivative surface
evaluation and the ruled surface both use it, and the copy is gone. smlib's
mod list back in order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Owner's decision for strips whose rulings turn freely along their edge (a face's
normal along a free-form face, which was refused): the far edge is not the
offset curve but a B-spline the kernel defines, and the strip is exactly the
ruled surface between the edge and it.
The far curve has the edge's own degree, knots and weights (so the ruled NURBS
needs no knot merging); it runs from the strip's start corner to its end corner
(where the rulings at the edge's ends reach); its interior control points are
given (handles) or the least-squares solution of weighted terms, the
construction's parameters: closeness to the rulings' ends at the edge's
Greville abscissae (W_CLOSE), fairness by arc length (W_FAIR), each end leaving
along the chord to the other end (W_TANGENT, one small function), and at a joint
where the chain runs on smoothly with shared sides the first and second
derivatives by arc length matched to the neighbour's (W_JOIN), in one solve
along the chain, an exact neighbour's far edge matched as it is. Exact strips
are unchanged.
ruled_far_handles gives each free strip's default handles; ruled_surface_with
takes moved ones. The Ruled Surface feature saves them as `far` (per chain
edge, null for the default; pending format note ruled-far-handles), and
surface.ruled takes them as `far`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
No caller is left for: Assembly::sheet and assemble::outward_normal, Body::shell_nesting,
regularize_generated_planar_faces, with_shell_planar_sources, boolean_record's Traced and
of_pair, offset's normal, plane_fit's plane_through, EdgeSampler, planar_face and
plane_frame (fill has its own on the record), polyhedron's solve3, prism's wall_along,
record::regions/region, splitface's side_of_point, Analytic, analytic_face, face_sides and
canonical, surfaces::picked_loop, sweep_path's closed_path, turn and stations,
shell_groups' groups and parents with their limits, and the whole of spline_conv. Their
tests go with them: prism's curved_prism_wall_requires_positive_separable_rational_weights,
body_lump's shell_grouping_preserves_large_disconnected_patterns_and_bounds_hostile_work,
and regularization protection's check of the generated regularizer. Three names follow
clippy's conventions: into_face -> direction_into_face, from_to -> ends_along, and Params::
to_eval takes its Copy self by value; fill's clash loop walks its slots.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Machine-applied by cargo clippy --fix: borrows the compiler dereferences at once, an
unused import, a manual += , an entry() for contains_key+insert, a needless Ok(?),
contains() for iter().any(). No behaviour changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
mesh memo (one remembered mesh per tolerance), history (a bare body from a record), tests.rs
(no coarse-missing field, lumps for touching shells), outline (the foot found by the planar
faces in its plane), chamfer sides (the rim from the record's edges), seam faces, coplanar
and issue 679 and pinch tests (the boolean is SMLib's: the diagnostics assertions go, every
volume, shell and validity check stays), boolean history (a cut of two pieces through the
boolean), integration tests naming solidcraft_kernel::MeshTolerance.
Deleted, testing internals that are gone: boolean_history_tests' glue_merges_across_the_seam
and faces_pushed_apart (coplanar::glue, boolean_apart), planar_fallback (polybool's
planar_boolean) and bore_union_of_a_bore_in_two_arcs_has_no_history (coplanar::bore_union);
issue_851's the_sphere_at_a_box_corner_is_the_planar_corner_builders (blend_vertex); pinch's
prism_boolean shell check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The truck reads become record reads: an intersection edge of a moved cut, the fillet
mitre edges sampled with CurveEval, the cache estimate from the record's faces and edge
uses, holed faces by their loops. The drain pads and the coincident-faces booleans keep
their volumes and faces without the boolean diagnostics and the polygon boolean.
Deleted, testing internals that are gone: a_tapered_s_curve_is_cut_into_pieces_that_follow_its_offset
(build::offset_error/offset_splits), planar_fallback_names_the_curved_faces (the polygon
fallback), mixed_loop_blend_guards_selection_and_construction and
loop_chamfer_mixed_arc_and_line_uses_offset_intersections (loopblend),
an_attempt_budget_stops_the_boolean_and_is_shared_by_nesting (with_attempt_budget),
placing_a_body_does_not_turn_its_revolved_faces_again (REPHASED, truck's revolution phase),
and the truck debug runs debug_coplanar_union_rate, debug_truck_sphere, debug_sphere_patches,
debug_sphere_cases, debug_sphere_rotations, debug_cube_sphere_measure, debug_heal_sphere_cut,
debug_coplanar_cylinder_union, debug_drilled_hole, debug_face_param_areas and
debug_revolved_tube_cut (truck_shapeops booleans, build::sphere_patches).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
native.rs's test module keeps its encode/decode, wire-record, hostile-input and appearance
tests. Deleted with the adapter they tested: pcurves_on_planes_are_the_curves_mapped_exactly,
pcurves_fit_curved_faces_within_tolerance, pcurves_of_seams_lie_a_period_apart and
pcurve_pictures, with their helpers (pcurves_of, check_pcurves, check_faces, curved_bodies,
seam_faces, draw). smlib_import_tests count shells on the record; their notes no longer
speak of truck.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
- thin faces: the face pinched against its own edge is a record sheet (sew::assemble) meshed
by the body's mesher; its triangles still cover its boundary polygon.
- trimmed loft faces, loft rails: distances to faces through the record's SurfaceEval.
- untouched faces: edges and in-line stations from the record's edges and vertices.
- intersection curves: the #1252 regression on a record sheet whose edge is a traced
intersection of parallel planes: meshing neither panics nor goes non-finite.
- lumps: shells counted on the record; the certified witness is Brep::compound of its three
bodies, faces known by surface and box.
- Deleted, testing internals that are gone: intersection_curve_tests'
degenerate_intersection_polylines_return_none_without_panicking,
closed_intersection_retains_the_closure_and_spherical_supports and
open_intersection_keeps_endpoints_on_both_planes (truck's IntersectionCurveWithParameters)
and a_result_with_an_edge_that_does_not_evaluate_is_refused (boolean_record::edges_evaluate);
body_lump_tests' unowned_overlapping_and_touching_cavities_are_errors_not_dropped_geometry
and shortcut_clearance_refuses_curved_contact_hidden_between_mesh_vertices (truck shells
joined without regions, separated_lumps) — kept as spheres_apart_join_as_two_lumps — and the
shell-order permutation of nested_cavities (the record names each cavity's region).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
- placement: a rigid placement moves the record and nothing else (regions, shells, loops
with their pcurves, edges and their uses equal; every vertex moved; the identity's
history), on a cylinder whose wall's loop uses its seam twice.
- issue 2005, 616, 674, 1038, 1510, join between caps, regularization protection, history,
mesh memo, display mesh: the truck reads become record reads (shells closed by edge
uses, faces by the record's surfaces, vertices by the record's points, the mesher's own
boundary budget); the nearly-in-plane slab of 1510 is an extruded record body.
- Deleted, testing internals that are gone: issue 616's glue_joins_every_face_the_pieces_share
(coplanar::glue) and the_retry_guard_refuses_changes_away_from_the_tool
(boolean_guard::check_retry); issue 674's partial_contact_does_not_join_tangent_or_separated_faces
(coplanar::imprint_union); issue 1038's hinted_division_matches_the_search_from_nothing
(truck's vendored parameter_division); issue 1510's a_face_close_to_the_plane_but_off_it_is_not_moved
(ops::off_plane). The boolean-diagnostics assertions (no retry, no tool nudge) went with
capture_boolean_diagnostics; the rest of each test is kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
solidcraft-smlib is a dependency of every build (it answers NotBuilt without
SMLib), so the SMLib sides of surfaces.rs and blend_smlib.rs no longer sit
behind cfg(feature = "smlib"), and their stub modules go; blend_smlib's tests
run in every build like the rest. This also uses offset_backend::thicken and
the smlib_sweeps curve conversions in builds without the feature, which left
them unused there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The three refusals of the cornered controlled sweep become the owner's rules:
- an orientation direction turns the section about the corner's binormal like
any sweep, then turns it back about its normal to the direction's frame
evenly along the next run, as a twist (direction_offset: the turn between
the carried section and the direction law's section there);
- guide rails follow a path with corners when each rail has a corner of its
own at every corner of the path, in the plane halving it and by the same
turn about the same axis; the rails are cut there into each run's
(run_rails), anything else is refused with the corner named;
- round a closed path with corners the first run starts from the closing
mitre and the last ends on it; a twist is allowed where the profile maps
onto itself (closing_offset), an end scale is refused.
Each run's station count now follows its own turn, twist included.
solidcraft-smlib is a dependency in every build (it answers NotBuilt without
SMLib), so smlib_sweeps' imp is no longer behind the smlib feature and its
stubs and not_built go; polyhedron calls offset_backend in every build.
Deleted as unused: sweep_path::{closed_path, turn, stations}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A sweep that twists or scales refused any path whose direction jumps by more
than 1e-3 rad, a branch on the path's shape. It now takes the corner model of
the plain sweep: each smooth run is swept through stations by the laws a smooth
path has (twist and scale running with the length along the whole path, each
run going on from the section the last ended with), and the runs meet at each
corner in a mitre. Turning with the path, each run goes on past its corners as
a prism of its section there (the laws hold still across the mitre: the two
runs' sections meet on the plane halving the corner only when both are the
corner's section) and is cut back to that plane, then turned through the
corner onto the next run; only moving along the path, the runs meet at the
corner's section. The mitre plane is one function for both (mitre_of).
Refused, with the reason, where a control has no meaning at a sharp corner: an
orientation direction (it turns the section by its own turn, not the corner's,
so the two runs' sections don't meet), guide rails (their sections jump where
the path turns), and a closed path with corners (a twist or scale keeps the
last section off the first).
(not built: base in progress; no error in these files)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Intersection::point started its Newton solve from the last point found (a Cell holding both
surfaces' parameters) when that landed near the leader, as truck took a hinted point. The
answer then depended on what was evaluated before, so CurveEval was not Sync, and the
clearance check, which shares a body's DistanceShape (its edges' CurveEvals) across worker
threads, did not compile; with threads it would also have made the result depend on their
interleaving. Each point is now solved from the leader's own point, the path the hint fell
back to, and the unused trace tolerance goes with the hint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
sheet_hole_cut chose by failure: a hole's drills were tried all at once and, if that was
refused, one by one; each drill was tried as a flat cut-out and, if that was refused (or
machined nothing), as a bore. Now the drill's geometry decides, one path each:
- SheetBody::drilled_panels: the panels a straight drill meets and how square it is to each
(|panel normal · axis|); a drill into a bend is refused, as before. add_bore uses it.
- sheet_drill: square across every panel it meets is Drill::Across (sheet_primitive_cut, a
cut-out or pocket of the flat pattern), along every one into a rim is Drill::Along
(sheet_bore_cut, a bore), meeting no sheet is None, and anything else (square to one panel
and along another, or at an angle) is refused with the angle message.
- The all-at-once pass (sheet_drill_cuts) passes its errors on; its None (the drills not all
square along one axis) still puts the model back and drills them one by one.
- sheet_bore_cut passes add_bore's refusal on instead of answering None.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The boss's post and ribs, the boss's hole, the snap fit's arm and the rest started a few
hundredths of their height inside the material (and the hole and the groove ran on as far
past the face), and the groove's cutter reached past the rim edge into the free side, so no
face of a tool lay on a face of the part. SMLib joins and cuts touching faces exactly: each
tool is now the feature's own shape, from the face (a drafted post or rest sized at the
face, the groove's band the rim's own, free reach 0). The material each adds or removes is
the same.
kernel::rim_band's `free` is now always 0 from the doc.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The kernel has no boolean retries now, nor their diagnostics
(capture_boolean_diagnostics, BooleanDiagnostics, BooleanStrategy), nor the near-coaxial
crescent (coaxial_overlap, coaxial_candidate, with_attempt_budget). What read them goes:
- doc: FeatureResult and the evaluation cache carry no boolean diagnostics, and no "Boolean
retry" warning; the saved model no longer writes them (an older file's are ignored);
- engine: timeline.inspect / feature inspection drop `boolean_diagnostics`; fuzz runs and
the CLI fuzz summary drop their retry counts;
- inspect.interference: each pair is the bodies' Intersect, measured; the near-coaxial
crescent, the by-piece intersection of bodies in several pieces and the unverified notes
go (the command's doc no longer lists near_coaxial);
- inspect.clearance: an overlap is the bodies' Intersect, measured near its region; the pin
tries, the cut-down copies of large bodies (clipped, narrowed), the union cross-check
(union_disagrees, contradicted) and the malformed-part check (plausible) go, and the
measure near the origin passes its move's error on.
Tests: the retry test and the unit tests of the deleted checks are removed; the barrel join
test (tests/boolean_diagnostics.rs, now tests/barrel_join.rs) keeps its geometry checks;
the pin-in-bushing test expects the filleted case measured; assertions on empty diagnostics
and on near_coaxial are dropped; the corpus fixtures lose the diagnostics key and the one
retry warning.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Every lib warning under --features smlib outside the files agents are editing
(assemble.rs, sweep_path.rs) pointed at code nothing calls any more:
- body: shell_nesting;
- boolean_record: of_pair and its Traced alias;
- planar_regularize: the generated-wall regularization (and its branches in the
certified one, which was its only other path); planar_sources:
with_shell_planar_sources;
- plane_fit: plane_through, EdgeSampler, planar_face, plane_frame (the patch is
fill's); only loop_area is left;
- polyhedron: solve3; prism: wall_along; offset: Surf::normal;
- record: regions, region; shell_groups: groups, parents and their limits;
- splitface: side_of_point, Analytic, analytic_face, face_sides, canonical;
- spline_conv.rs, all of it (surfaces builds NURBS records); an unused import in
tess/uv.rs.
The tests of deleted items go with them: prism's wall_along test, the shell
grouping test in body_lump_tests, and the generated-regularization assertion in
regularization_protection_tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
split_band_edges_are_seams tested split.rs's splitting of a face that winds around its
surface into two (deleted with the truck reader; the record keeps the face with a seam):
deleted. ImportedBody::file_faces says each body face is a file face.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Each body's record is written directly: a manifold solid B-rep (186) per material region
with its voids, open shells as 514 form 2, faces on the record's exact surfaces and edges
on its exact curves over their intervals, in the STEP form solidcraft_step::write::topology
gives them (one geometry model for both exports). The detour through STEP text and its
re-parse is gone; the maximum coordinate is the largest coordinate written.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
write::topology::layout says which shells make each solid (a material region's outer shell
and its voids) and which are sheets, and outer_loop which loop of a face is its outer one;
the STEP writer uses both, and the IGES writer can too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Import: every solid is read by solidcraft_step::topology into its record and taken into
SMLib as given (step_in/parallel.rs smlib_part), the one path; the truck readers
(step_in/brep.rs, split.rs, geom.rs) are deleted. Placements are column-major 4x4 matrices
from solidcraft_step::read placements; an instance is its part's record moved there
(ops::transform_matrix), or the very part at the identity. Assemblies, names, colours,
shared displays and the public API are unchanged. step_orientation_errors lists the faces
the STEP reader turns over (their loops disagree with their same_sense).
Export: each body's record is written by solidcraft_step::write::topology as the exact
B-rep (solids per material region, open shells as a surface model, the record's pcurves);
step_out writes products, units, colours and assemblies around it with Writer. The truck
writer and every post-processing pass (split-face merging, sheet trimming, analytic
recognition, short-edge collapse, the re-read fallback) are deleted. The context's
uncertainty is the loosest record tolerance exported.
Tests: ported to the record API with their assertions. Deleted, as tests of deleted
internals: conic_tests extruded_ellipse_seam_uses_profile_angles_for_oblique_sweeps and
conic_arcs_are_exact (truck geometry reader), tests offset_surface_analytic_geometry_and_
orientation, offset_surfaces_nested_and_hostile and periodic_knot_bsplines_are_read_on_
their_domain (truck geometry reader), corpus_truck_and_schema_4_counts (truck comparison);
the orientation counter asserts (crate::body::ORIENTED, gone with orienting by mesh).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A placed revolution is written exactly whenever its placement maps revolutions about the
axis onto revolutions (one scale across the axis, another along it, then a rotation or
reflection), its profile placed with it; a conic profile stays a conic under a similarity.
A void's CLOSED_SHELL keeps the record's sides (normals out of the material, into the void)
through an ORIENTED_CLOSED_SHELL .T., as SolidCraft has written voids. Reader::frame reads an
assembly placement (AXIS2_PLACEMENT_3D, or _2D in the XY plane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
write::topology writes each material region as a MANIFOLD_SOLID_BREP (BREP_WITH_VOIDS
with its voids as ORIENTED_CLOSED_SHELLs), open shells as a SHELL_BASED_SURFACE_MODEL,
faces as ADVANCED_FACEs on their exact surface with FACE_OUTER_BOUND/FACE_BOUND of
EDGE_LOOPs and VERTEX_LOOPs, edges as EDGE_CURVEs on their exact curve over the record's
interval, and the record's exact pcurves as PCURVEs of SURFACE_CURVE/SEAM_CURVE. A record
surface's (u, v) map linearly onto STEP's (planes on their orthonormal frame, narrowing
cones turned over, revolutions angle first), and pcurves go through the same map.
Writer::add_distinct writes topology entities unshared. topology::Solid lists the faces
the reader turned over (their loops disagree with their same_sense).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
doc's FeatureResult has no boolean_diagnostics any more (truck's Boolean
retry reports, gone with truck; SMLib's boolean has no retries). The
timeline.inspect and doc.inspect replies drop the key.
Tests of the removed reports: tests/boolean_diagnostics.rs is deleted (it
checked truck's retry events and their survival through the cache, undo and
reopen); the assertions that a feature's report had no events go from
features_more_tests, patterns_tests, hole_face_tests and tests/rim_cut (the
rest of each test stays). The CLI corpus's expected replies
(move-bodies, issue-489-stacked-join, issue-616-lid-ribs) drop the key; the
issue-616 one listed a planar_fallback event. docs/robustness.md: its
section on retries becomes one on SMLib's booleans, which have none.
Left to the blends work: engine fuzz.rs (its retry-event counts) and
clearance/interference (capture_boolean_diagnostics, coaxial_overlap,
with_attempt_budget).
Not built: base in progress (kernel-smlib/no-truck 66ab6b7f does not build).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The kernel's BooleanDiagnostics and capture_boolean_diagnostics are gone
with truck: they reported the retries and fallbacks truck's booleans took
(shifted operands, refined tolerances, a planar fallback), and SMLib's
boolean has none. Model::evaluate ran every feature inside
capture_boolean_diagnostics, warned with its summary and kept the report
in FeatureResult, its cache entry and the saved model ("diagnostics").
The feature runs as it is, FeatureResult and CachedEval lose the field, and
the saved model neither writes nor reads "diagnostics" (a model saved with
it still opens: the key is not looked at).
Callers outside doc that read FeatureResult::boolean_diagnostics change
with it: engine's timeline.inspect reply here; engine's fuzz report and
clearance/interference belong to the blends work.
Not built: base in progress (kernel-smlib/no-truck 66ab6b7f does not build).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The vendored truck-geometry they pinned is gone; the same finite-difference checks, degrees
2 to 5 and a rational surface, now pin SurfaceEval::second, which every curvature the kernel
reads comes from.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The helpers read every face of the fill as a record surface over its box, the loops'
(u, v) polygons from their exact pcurves, the rim from the free edges and curvature from
SurfaceEval::second; check_face takes the fill's faces (one, or the patches about a centre)
whose free edges lie on the body's edge curves. Expectations changed by the coordinator's
decision on loops of five or more edges: the pentagon fills as five patches (each rim edge
in halves, five spokes: at least 15 edges), the hexagon through a point as six patches with
18 edges. Every other assertion is kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
finish lays out the faces left and added (each stored relative to its surface's natural
normal with its pcurves and box, now one function, stored()), the source's shells and
regions (a region whose outer shell lost every face gone) and the edges and vertices as
they are, then calls Brep::canonical; its own ordering walk is deleted. check_geometry
and the face and edge maps are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
sweeps_along_3d_paths asserted at most 8 faces for a 41-point path: one wall
per profile segment fitted through the points, which the exact construction
no longer makes (no curve is fitted through samples). A point path is the
polyline its points make, swept line by line and mitred at each point, so the
circle's two arcs give two walls along each of the 40 lines, and two caps:
82 faces. The volume check stands (a mitred sweep of a profile centred on its
path keeps area × length).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
An Iso link along an explicit direction on a solid now takes
kernel::face_section_along, the face cut by the plane holding its exact normal
and the direction, instead of the triangles' section. With the projection, the
u/v iso curves and the base's exact intersection edges, every linked 3D curve
on a solid keeps its exact curves (Wire::exact); curves that meet end to end
become one wire (joined_wires, shared by the projection and the iso curves).
Mesh bodies, which have only their triangles, keep the triangle-based curves.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
face_section_along cuts a face through a point by the plane that holds the
face's exact normal there and a direction (Brep::face_section): the iso curve
"along a direction" of a link, as record curves on their intervals, the
B-splines SMLib made. The projection and the section share the picking reach,
the curve tolerance, the error words and the conversion to record curves.
(Intersection links take the base's meeting_edges, SMLib's imprint.)
lib.rs (coordinator): export point_path::face_section_along.
(not built: base in progress; no error in these files)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Brep::face_section (smlib_face_section: one face cut by a plane) hands back
the NURBS SMLib made with their intervals, through one reader of smlib_curves
(take_curves) shared with project_curves. The face-pick errors map to
NoMatch/Ambiguous in one place (picked). SMLIB_REV at fork eedfa51.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
LinkSource::Iso with `u` or `v` on a solid now takes kernel::iso_curves: the
face's isoparametric curve through the point, exactly, cut by the face's
loops, each piece a wire with its exact curve. An explicit direction (the face
cut by a plane: an intersection curve, waiting for SMLib's exact SSI curves)
and a mesh body still use the triangles' section.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
iso_curves gives the isoparametric curve of a body's face through a point, along
its first or second parameter, as record curves: on a B-spline surface the
surface's own row at the point's other parameter (each control point its
column evaluated there, homogeneous, so exact for rational nets), on an
analytic surface its line or circle. It is cut where it crosses the face's
loops: each crossing solved on the loop's pcurve itself (bracketed on samples,
then bisected on the pcurve), the pieces between kept where their middle lies
inside the face (even-odd over the loops), neighbouring inside pieces joined.
A revolved surface record is not supported yet.
lib.rs (coordinator): export point_path::iso_curves.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A sketch curve linked onto a face (LinkSource::OnSurface) was projected onto
the face's display triangles, so the wire was a polyline on the mesh. On a
solid it is now kernel::project_onto_face of the curve's exact segments, along
the sketch normal turned toward the face: the wire's exact curves are the
B-splines SMLib made, pieces meeting end to end one wire, the points sampled
from them for display. A mesh body, which has only its triangles, is still
projected onto them.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
project_onto_face projects sketch segments along a direction onto the face of a
body at a point through Brep::project_curves (smlib_project_curves), each
projected piece the B-spline SMLib made, returned as a record curve
(RationalSpline) on its interval, within a millionth of the body's size.
lib.rs (coordinator): export point_path::project_onto_face.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The base no longer links truck, so build.rs and sweep_path.rs drop what was
kept only for other modules: chain and profile wires, the profile face,
loft wires and their ruled and smooth faces, across/section_parameters, the
cubic interpolations and NetFit, the uniform wires, the six sphere patch
faces, and sweep_path's ruled_sheet and smooth_sheet with the station helpers
only they used. Their callers switch to planar_face_record,
smlib_sweeps::{chain_curves, body_from_record, sphere}, sketch_record_curve,
the SMLib sweeps with open ends, and bspline::CurveN::interpolate.
offset_backend and polyhedron pick faces through provenance::closest_on_tri,
boolean_guard being gone.
Callers left to switch (other ports): surfaces.rs, loft_rails.rs,
surface_offset.rs, surface_ruled.rs, step_in/brep.rs, trimmed_face_mesh_tests.rs.
(not built: base in progress; no error in these files)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The FFI for smlib_project_curves and the smlib_curves result (fork 5f9af50),
and the safe call: a curve list projected along a direction onto the picked
face, back as Curve::Nurbs with each curve's interval. SMLIB_REV at 5f9af50
(fork integration head 1934b57 + the untrim status fix + the projection shim).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Rulings that are constant, or keep their angle to an arc's axis, move the edge by
an affine map: the far edge is the edge's exact image and the strip exact. Any
other ruling field (a normal turning along a free-form face, a tangent carried
round a free-form curve) ends on an offset curve, which has no exact NURBS form
in general: SMLib holds one only implicitly (SmOffsetCurve) or approximates it
(NL_CrvOffset.h, N_CrvOffsetApprox), and its ruled surface (SmApiCreateRuledSurface)
takes two B-spline curves it is given. Such a strip was a B-spline fitted within
1e-3; it is now refused by name. The fitted-strip test checks the refusal.
The round-corner cases count one face per corner: SMLib's revolve makes a quarter
turn one face, where truck's cone builder made two (12 faces and joins, now 8).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
SMLib is the only kernel now. cargo xtask kernel-cases ran every case once
per kernel asked for (--kernel default|smlib|both), setting
SOLIDCRAFT_KERNEL per run, and wrote a column per kernel; solidcraft-cli no
longer reads that variable.
The rig and the ladder run each case once, through solidcraft-cli built with
--features smlib (cargo run passes it), and report one result per case:
--kernel and the env var are gone. Each run has an output folder (--out,
default target/kernel-cases, target/kernel-ladder for the ladder) holding
results.json (every case's full result, which --results used to write on
request) and dumps/<area>-<case>/: every case runs with
solidcraft-cli kernel-cases --dumps=<out>/dumps, so a failed SMLib call
leaves its inputs for replay.
The ladder's golden holds one entry per case: golden.json is the last
recording's smlib entries, which predate SMLib running every case (its
reason says so); record it again after the first SMLib-only run.
docs/kernel-cases.md (until the next run regenerates it),
tests/kernel-cases/README.md, docs/kernel-smlib-sweeps-shell.md (its truck
column and comparisons dropped), xtask's and solidcraft-cli's usage text:
one kernel. Nothing was run.
clippy -p xtask --all-targets: clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The helpers that read a face's surface, its edges and its seam shape read the record
(SurfaceEval, CurveEval, matchsurf's Patch) instead of truck faces; every assertion is
kept.
(not compiled: the kernel's test target waits on the base's step_*, build, sweep_path and
surface_ruled ports)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Before sewing, each edge of one input is split exactly (Brep::split_edges) at every vertex
of another input that lies on it within the stitch tolerance away from its own ends, so a
patch whose corner meets a body's edge part way (the five-plus-sided fill's midpoints)
sews with no T-junction. The pieces carry their edge's history. The search is the nearest
of 64 samples refined by golden section, over the vertices within a chord's reach of the
edge's samples. Nothing else in stitch changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Each piece keeps the edge's curve on a sub-interval, a new vertex sits on the curve at each
split, every use becomes uses of the pieces with its pcurve cut at the same parameters
(bspline::segment), wire edges likewise; the result is in canonical order with each
edge's source edge. What a sheet needs so that another's vertex on one of its edges is a
vertex of both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
sew::assemble and surfaces::Parts::canonical each lay out their record as its parts stand
(regions, shells, faces, edges with no uses, vertices) and end in Brep::canonical; their own
ordering walks are deleted. spline_conv::tensor_face (no callers left) is deleted in favour
of surfaces::tensor_sheet, and fill's copy of the spline surface record in favour of
surfaces::spline_surface.
(not type-checked: the kernel stops at name resolution in step_*, build, sweep_path and
surface_ruled)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
part()'s walk becomes renumbered(): the record of given source regions in canonical order.
part() hands it the regions it cuts; canonical() hands it every region as it is, open,
wire and vertex shells included, for records builders put together in their own order.
Each edge's uses come from the walk; a source list naming exactly those uses keeps its
radial order (turned to start at the primary use), as part() did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The strips keep their model (exact where the rulings move the edge by an affine
map, a ruled B-spline fitted within 1e-3 otherwise; mitred or rounded corners;
an inside corner cut back). Each strip is built on its own on the record: the
chain edge as its exact NURBS (a circle or ellipse arc rational quadratic), the
far edge its image (its control points mapped) or the fit's, the surface a plane
or the ruled NURBS between them with straight pcurves along its parameter lines.
A mitre's triangles are flat polygons; a round corner's sector is SMLib's revolve
of the incoming side about the corner's axis. The faces are stitched by SMLib
(surfaces::sheet_of, which now takes the operation's operands), each strip made
in place of its edge, each corner face in place of the two edges there. Tests
read the record; the check that the strips stand on the box's own edges now
looks for edges lying along them, as the result is a body of its own.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A surface sweep along a path with corners was refused. Each smooth run is now
swept on its own with open ends by the solid sweep's corner model: carried on
straight past each corner, imprinted with the planes halving the corners and the
pieces between them kept (a point inside each face, smlib_face_points), the
profile turned through each corner onto the next run; the runs are stitched.
Only moving along the path, the runs meet as they are. Each wall keeps the
segments its uncut face came from through SMLib's tags. Sweep options along a
path with corners are refused as the solid sweep refuses them. The sheet
builders use smlib_sweeps' curve and path_curve; their own copies are gone.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
smlib_sweeps' loft_rails kept its own copy of the rail law with the anchoring
4056 was refused by (a rail lying in the last section's plane met at its first
crossing there). It now builds the sections with loft_rails::Section and takes
the sections and stations loft_rails::railed gives, so the law and its fix live
in one place. path_curve is exported beside curve for the sheet builders.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The doc puts exact tools into SMLib's boolean as drawn, so nothing asks whether a touching tool
can be joined, batches extrusions, or probes for room to grow a profile.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
machine_bores cut with the drill clipped to the panel and, refused (coincident faces at the
panel's rims), cut again with a longer cylinder a hair past the panel. SMLib's boolean takes
the flush ends exactly, so the clipped drill is the one cut; its refusal is passed on.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A lip tried kernel::raise_band and, refused, joined an extruded band in its place (whose
walls coincide with the walls below); a groove tried kernel::rim_band and, refused, cut a
band offset from the rim face's display-mesh loop (a sampled outline). Both second paths are
gone: a refusal of the exact band is the feature's refusal. offset_left, which only the
sampled band used, goes too. The rim and opening checks are unchanged.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
SMLib's boolean takes touching and coplanar faces exactly, so the tools no longer bend
around them, and no tool is tried a second way after a refusal:
- extend_for_coplanar / grow_profile_for_coplanar (a cut or join tool run on past faces it
lies on, its profile grown out of walls it touches), Surroundings and its clearance probe,
fill_void_holes (holes over empty space taken into the profile, #1360),
trim_embedded_join_ends (a join's buried ends cut off, #2059), the touching-face checks
(kernel::can_join_touching_faces / can_join_all_touching_faces) that decided when the
exact tool could go in, the profile that "grows wherever the samples find room" when the
build failed, region_samples, and the revolve's grown profiles;
- a tapered tool's run-on into the face it stands on (taper_run_on_fits, face_level_at) and
its straight TAPER_FOOT: the tapered tool is the profile drafted over its length;
- apply_traced's batched boolean that, when refused, put the model back and ran the tools
one by one (and common_join_target, which only it used): the tools run as the operation
always runs them;
- a pattern's copy of a reduced fillet tried again as a new fit when the copied radii were
refused: the copy uses the radii the original was built with;
- union_apart / join_apart errors are passed on instead of leaving the tools apart.
The command behaviour stays: the same tools, the same lineage, the same at-once boolean of
tools apart. The tests of the deleted growth and trimming (coplanar_growth_tests) go with
them.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
thread_groove built the faceted mesh groove truck cut; nothing calls it since the thread
cut is SMLib's screw sweep (kernel::sweep_helix) and a B-rep boolean. With it go
GrooveShape's facet count and the refusal it fed ("the modelled thread is too fine for this
diameter"): an exact groove has no chord error, so a fine thread on a large diameter is cut
as drawn. Dimensions that are not finite are still refused.
kernel::sweep_helix_mesh is now called only by crates/engine/src/thread_tests.rs.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
loft_blend hands its net to surfaces::tensor_sheet (the bridge's own one-face sheet);
section_frames picks the face that bounds the whole chain and faces the run best from the
record's edge uses and the face's SurfaceEval normal, and reads its frames with
bridge::frames. Behaviour and messages unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
match_surface and seam_continuity read faces as record surfaces (SurfaceEval over the
face's domain), pick the face and its kept side's edges from the record, chain and frame
the target through bridge::chain/frames, keep the crown whose bridge strays least from the
original, and turn the bridge face to the target's sense by building it again turned
(surfaces::Parts). The history is over [body, target]: the face modified from the body's,
its edges new. Behaviour, constants and messages unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
general_fill takes loops of three to sixteen edges. quilt_body assembles fill_iso's
patches into one sheet: each loop edge whole, or for five or more its two halves at the
middle of its interval, both on the edge's exact curve record and sharing a midpoint
vertex; each spoke a new edge from that vertex to the centre; every pcurve the straight
line in (u, v) of its side. The history marks every face generated, each edge or half a
piece of the body's own edge, each spoke new. A reversed loop edge's knots keep the
interval's ends exactly, so pieces name the edge's own vertices.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A loop of N >= 5 edges is N four-sided patches about a centre point: each edge split at
the middle of its parameter interval, a spoke from each midpoint to the centre, patch k
on the second half of edge k-1, the first half of edge k and the two spokes beside them.
All patches share one compatible space, so each spoke is the same control points in both
patches it divides and the centre one point; the least-squares solve takes them once
and adds rows opposing the two patches' derivatives across each spoke (tangent there).
The one- and three-/four-sided fill is the one-patch case of the same model. The result
lists each patch's sides (edge pieces with their parameter ranges, or spokes) and the
spoke curves; every pcurve is a straight line in (u, v).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
sketch.intersection_curve (LinkSource::Intersection, crates/doc/src/
project.rs) intersected the two bodies' display meshes triangle by triangle
and chained the segments into polylines: a curve known only as points, off
the true intersection by the meshes' chord.
kernel::meeting_edges(a, a_face, b, b_face, chord) takes each side whole or
as one face (that face alone as a surface body, surfaces::taken), tags both
and imprints the first by the second in SMLib (smlib_imprint: "each of its
faces that `tool` meets cut along the intersection", capi/include/smlib.h).
SMLib intersects the faces' surfaces and trims each curve to both faces, so
the edges the imprint makes are exactly where they meet; its own history
says which they are (an edge no edge of either operand became). Each comes
back as its record curve on its interval, from SMLib's export of the
imprinted body, with its points from that body's mesh at `chord`. Sides that
do not meet give none.
The intersection link now stores each edge as a wire with its exact curve
(Wire::exact, the existing WireCurve; no new type or format change), its
points at 1/1000 of the larger body. project3d::mesh_intersection, its
triangle/triangle test and segment chaining go; project3d::mesh_of becomes
part_of (the body and, for a face reference, the face index).
Needs in crates/kernel/src/lib.rs (the coordinator's): `mod intersection;`
and `pub use intersection::{MeetingEdge, meeting_edges};`.
Not built: base in progress (kernel-smlib/no-truck 513ebe47 does not build).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A rig case whose SMLib call fails left nothing to replay it from: nothing called
solidcraft_smlib::set_dump_dir. `solidcraft-cli kernel-cases --dumps=DIR` now points SMLib's
dump folder at DIR/<area>-<case> before each case runs, so a failing call leaves its
arguments and B-reps there (smlib_brep_read reads them back into a scratch C program), and
turns it off after the run. Off without the flag. The kernel exposes it as
smlib_dump_dir, beside smlib_built (the CLI reaches SMLib only through the kernel).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
truck-modeling, -topology, -shapeops, -stepio and the vendored truck-geometry, -meshalgo,
-shapeops, -stepio and matext4cgmath (with their attribution rows and vendor notes) are removed.
The kernel-cases runner has one kernel, SMLib, and no build-mismatch check; a modelled thread is
cut by SMLib's exact screw-swept groove (the faceted groove path is gone).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Edge chains are the body's record edges, walked along their curves the way the
chain runs; G1/G2 conditions read the open edge's face on the record with its
exact first and second derivatives (SurfaceEval::derivs, ::second). The face is
built on the record: each side the network surface's boundary row on its knots,
split at the chains' inner vertices into edges over parts of the side's interval
(no knot insertion), each with its straight pcurve; a collapsed side is left out.
boundary_join counts the record's edges. Tests ported.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
ssi::intersect took a sample count and gave each intersection curve as
samples: its 3D points, its UV points on each surface and the surfaces at
them. The fork's 947eedc hands out the curves SMLib's intersectors return,
exactly (smlib_ssi_curve_nurbs: degree, knots, control points, weights), and
drops the samples; SMLIB_ABI_VERSION 4.
SsiCurve now holds the 3D curve as a schema 4 Curve::Spline or
Curve::RationalSpline (rational where a weight is not 1) and each UV curve
the method returned as a Curve2, with SMLib's type, deviation estimate and
the 3D curve's natural interval; ssi::intersect loses its sample count. A
curve SMLib does not hold as a B-spline is Error::Unsupported (it is never
approximated); a B-spline whose knot count is not points + degree + 1 is an
argument error.
The SSI corpus scorer (tests/ssi_corpus.rs) samples the exact curves itself
where the corpus runner samples them: 4001 parameters spread evenly over the
3D curve's natural interval and over each UV curve's interval, the surfaces
evaluated at the UV samples, samples that cannot be evaluated dropped. The
unit test plane_through_sphere_is_a_circle evaluates the exact curve.
Nothing else used the samples. SMLIB_REV: the fork at 947eedc.
clippy -p solidcraft-smlib --all-targets, with and without native: clean.
The corpus gate (smlib_global_intersect_at_1e_6) is not run here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
bridge::{chain, frames, frames_across, chain_closes} and surfaces take
body.record() instead of encoding one; a surface body is Body::from_record of the
canonical record, a stitched one Body::from_smlib_record of SMLib's export, its
history matched to the input record when SMLib's import numbers it alike.
record::attach takes the new form.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The bridge core was already its own geometry; its readings went through truck.
Chains are edges of the body's record (index, direction), measured along each
edge's record curve the way the chain runs; a support face is a record face whose
surface is evaluated with its exact first derivatives (SurfaceEval::derivs) where
truck's were finite differences; the station's parameters come from SurfaceEval's
projection. The bridge face is surfaces::tensor_sheet, and blend_between records
it as new over [a, b]. frames, frames_across and chain_closes take the body and
face index. Boundary patches go through the same bridge. Tests ported.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
surfaces.rs gains the record pieces NURBS faces are built from: a NURBS surface
and curve as record geometry, a pcurve along a parameter line, a whole tensor face
bounded by its net's boundary rows and columns (exact for a clamped surface), and
all_new for a face new to an operation. tspline_sheet makes a regular T-spline
that tensor face and an irregular one its Bézier elements, each side split at the
T-points, each edge an exact Bézier piece of its side with a straight pcurve on
every patch it bounds, shared between neighbours. Tests ported to the record.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Copy, unstitch, reverse and remove faces, Patch (a sketch region, or a planar loop
of a body's edges) and Extend work on the body's record: Parts puts any faces,
edges and vertices into canonical order, and each face taken is the body's face
(Kept), its edges the body's. Sheets are SMLib's sweeps with no caps (extrude,
revolve, curve sweep, stations for twist/scale/orientation/rails), a chain at a
time; a loft lofts each column of matched segments and stitches the columns.
History: a wall comes from the segments its edges in the start plane lie along.
Stitch joins the bodies' records and sews them with smlib_stitch, faces and edges
carried back to the bodies through SMLib's tags. Offset Surface offsets each face
on its own with smlib_offset_sheet and stitches the offsets within the tolerance:
smooth neighbours stay joined, sharp ones come apart, as before; the fitted
offset surfaces and their fold check are gone. Thicken is SMLib's thicken.
Not yet: a surface sweep along a path with corners (refused). Tests ported;
the fold-radius message test now checks only the refusal.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
loft_rails.rs placed stations between truck wires. Its law now works on the
sections' exact NURBS curves (Section, from the sketch segments' record curves)
and returns the sections and stations, each a curve list a loft is skinned
through; nothing of truck is left.
Case 4056 (a keel rail lying in the bow's sketch plane): rail() anchored every
section at the rail's first crossing of the section's plane after the section
before. A rail lying in the last section's plane crosses it all along, first at
the far section, 100 mm from the bow's curve, and the loft was refused ("a guide
rail crosses section 2's plane"). A rail runs from the first section to the
last, so the last section is met where the rail ends; the sections between are
met at their first crossing, as before.
smlib_sweeps.rs's solid loft_rails keeps its own copy of the anchoring and has
the same cause; it should call loft_rails::railed.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The safe wrappers of the fork's smlib_stitch (a body's faces sewn at a tolerance,
oriented, with whether it closed) and smlib_offset_sheet (a sheet's faces onto
their offsets), for the kernel's Stitch and Offset Surface.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
freeform.rs turned each free-form sketch segment into a truck edge. It now gives
every segment (lines, arcs, Béziers, conics) as a record curve and its pcurve on
the sketch plane's own surface, on one parameter with the same knots and weights,
so the pcurve lifts onto the edge exactly.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
trim_keep told each piece's side of the tool from the tool's field at the
centres of its mesh triangles. Each piece now takes one point on its own
surface inside its trims (SMLib's CalculateAnInternalPoint, through
smlib_face_points), mapped to the record's face order through SMLib's
numbering of the split body; the tool's field there says the side. A piece
whose side can't be told is an error, never guessed.
(not built: base in progress; no errors in this file)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Each face's point from SmFace::CalculateAnInternalPoint, in SMLib's face
order; the fork at 89a1933 (also: smlib_extend_sheet extends every surface
as SMLib's B-spline, its analytic kind given back).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
planar_regularize now calls the fork's smlib_merge_faces as it is
(SmBrep::RemoveTopologicalEdgesAndVertices with the edges under the kept points
staying): every edge the lineage does not choose is kept by a point at its
middle, so SMLib joins the faces across the chosen ones only. A body with more
kept edges than one call takes stays as it is, as the other budgets leave it.
splitface, planar_sources and planar_regularize read the body's record in place
(Body::record) and a face's use from Body::face_forward; the two-lump test body
is Body::from_record of the two records side by side.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
planar_sources reads each face's oriented plane from the body's record
(actual_planes: the surface's plane, turned with the face's use) instead of
truck faces; the lineage rules are unchanged. planar_regularize picks the edges
to remove as before (two faces of one shell on one certified plane, from one
source unless generated walls, off every split line) from the record's edge
uses, and SMLib joins the faces across them (Brep::merge_faces: each edge
removed where SmEdge::IsTopological finds the faces' surfaces coincide), so the
merged face is SMLib's and the history its own tags (Merged), composed through
the body's. SmBrep::Simplify is not this operation: it refits curves and
surfaces by knot removal and sampling.
The planar_sources tests read face points, edges and planes from the record;
the two-lump body is built from the two records side by side, each face given
the source of its own operand's face on that plane. Deleted:
planar_regularize's alternate_adjacency_path_cannot_remove_protected_internal_edge,
a test of the truck face builder merged_face (only listed edges are removed now,
so no other path can remove a protected one).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
coplanar.rs (coincident faces pushed apart, faces glued back to back, bores and
insets joined, imprint unions), seam_faces.rs (round faces halved before a
retried boolean) and planar_faces.rs (flat B-spline faces made planes so the
coplanar strategies see them) existed to get round the mesh-based boolean's
coincident faces and seam loops. SMLib's boolean classifies coincident faces
itself (SmMerge::ManifoldBoolean, step 2), so these are retries and second
paths around the operation, not operations: deleted, not ported.
Their tests of what the boolean gives keep their volume, lump and validity
assertions and now run against the boolean itself (coplanar_tests.rs and
seam_faces_tests.rs need their own mod lines in lib.rs); the assertions on the
strategies used, and the tests of push_faces, grow_faces_exact, trim_exact,
planarized, coincident_planes and halved, go with them.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A tool is now a set of schema 4 record surfaces, each over a parameter box,
unbounded where the tool is: a plane; a face's surface over its own box; a
sketch curve as one patch per segment swept along the direction (a line a plane
strip, an arc swept square to its sketch a cylinder strip, any other segment its
exact rational Bezier extruded), an open curve continued along its end tangents
past the body. SMLib cuts the faces where the patches meet them
(Brep::split_faces: its intersection curves trimmed to each face by
MergeCurvesOnSurface) and slices a body with one patch (Brep::split_body); the
history is SMLib's own tags. Nothing is traced on a mesh, sampled or fitted.
Which side of the tool a point is on is its offset along the normal of the
nearest patch, each patch's surface prepared once with its projection grid
(ToolSide), never per point. ToolSide::of and side_of_point take the body the
tool must reach past; face_side becomes face_sides (every face of a body, one
mesh); analytic_field becomes analytic_face (a face of a body, on its record);
split_any returns the face map instead of the truck imprint. split_edges_at and
fit (a Catmull-Rom fit through samples) are deleted with the truck code.
ssi::Surface::from_record turns a record surface over a box into SMLib's.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
split_faces cuts faces (SMLib face order) where ssi::Surface tools meet them and
says per face how many faces it became; split_body cuts a solid in two by a
surface, the part behind it and the part in front. SMLIB_REV moves to the fork
commit that adds smlib_split_faces and smlib_split_body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
face_params and outline are gone (no callers on the record). The fork head adds the 2D planar
boolean (smlib_planar_faces, smlib_boolean2d), the grazing-curve crossing rule (solidcore#480) and
untrim's parameter-box status.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
It imprinted two bodies touching back to back on each other along the outline of their
contact and glued them, for the old boolean's coplanar unions that could not join the pieces
its own imprint left. SMLib's boolean is the one path, and nothing calls it any more. Its tests
of the union itself (a lip swept on a round rim joins it, the lip's foot is a plane) stay as
boolean tests, without the old retry diagnostics.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Its sampled parameter loops (points projected back onto the surface, the outer loop guessed by
area) and its iso-curves of truck surfaces served Untrim and Extend, which no longer use them;
on the schema 4 record a face's loops in (u, v) are its exact pcurves, the outer loop the one
running counter-clockwise about the natural normal. The only use left is a truck test in
native.rs's seam fixtures.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
geom's NURBS curves and surfaces become the record's rational splines, weights kept.
tensor_face builds a whole tensor surface as a one-face sheet (assemble's new Assembly::sheet:
one open shell in the infinite region): the net's first and last rows and columns as edges on
the surface's own knots, sharing the corners, each with its exact pcurve (an iso-line, the
edge's parameter being the surface's along it), and returns the body.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
plane_through gives a plane record (its natural normal turning with the loop); EdgeSampler
samples a record edge through CurveEval in the direction a loop runs along it (an intersection
curve traced as the record holds it, with the evaluator's own hint); planar_face certifies a
loop of record edges from their records (lines by their ends, circles and ellipses by their
frames, polylines and splines by their points with positive weights, intersection curves by a
plane among their surfaces) and returns the plane, the caller building the face on the record.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A face's reach into a region comes from its record surface: planes (as offset::surf_of reads
them) lie in their triangles, a spline face's control net bounds it (its hull, read off the
record's points and positive weights), walls along the direction are prism::wall_along's, and
round faces keep their span and side. Truck's surfaces and the guard around them are gone.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The exact step of the distance between bodies works on the record: analytic faces in closed
form as before, other faces through SurfaceEval::project, edges by a bracketed golden-section
search of the record curve's own points from the nearest of evenly spaced ones (one method for
every curve kind, traced intersections included), the direction there from the curve's
derivative. Truck's point inversion is gone with it.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
face_surface is the record face's analytic surface (offset::surf_of); edge_circle takes a
circle record's centre, axis and angle exactly (its parameter is the angle about the frame's
z), and for any other curve keeps the test that its points all lie on one circle (SMLib holds
its primitives' circles as rational splines). The spindle torus's signed major radius is gone
with the old kernel: the record's tori are ring tori.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Seams and merged topology tell faces on one surface apart by the record's own surfaces
(offset::surf_of: plane, cylinder, sphere, cone, torus) and, for free-form faces, by the very
same surface record, never by fitting circles, spheres and cones to mesh vertices or by
comparing mesh normals along an edge. Edges are welded by their record vertices and middle
point, split lines and pinches read the record edge's points, a cone's apex is checked against
its faces' record vertices. The mesh still gives faces of no area and a cylinder face's extent
along its axis; a cylinder's side (hole or boss) is the record's.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
planar_regions_boolean replaced each free-form loop (a font's curves, an ellipse) with arcs
fitted within PLANAR_FIT_TOL before its own line/arc arrangement, and a loop the operation
cut kept those arcs: a cut free-form outline was extruded inexactly. Now each operand is a
sheet of planar faces built from its loops' own curves (Brep::planar_faces; lines, arcs and
the Béziers and conics as NURBS, exactly) and SM_API's 2D boolean combines them
(Brep::boolean2d): SMLib's topology intersector, the one the solid booleans use, splits the
operands' edges where they cross and the result's edges are copies of those curves. The
faces read back exactly (prism_curve::projected: lines, arcs, each Bézier span as the
segment it is). The Rust arrangement (crossings, ray parity, chaining, the fit and the
"exact again" restoration) is deleted: one source, no fitting.
extrude_planar_regions unites its regions the same way (balanced unions of one-region
sheets) and refuses pieces of the union that share a vertex, read off the result's
topology instead of a line/arc crossing test on fitted arcs.
Signatures stay (doc/sheet.rs and mesh_convert call them as before); `tolerance` is the gap
a loop may close within and the least tolerance edges are read back at. PLANAR_FIT_TOL
stays for doc/engine's flat patterns, which fit their own loops. smlib_sweeps::curve (the
Seg2 to SMLib curve conversion) is made pub(crate) for the sheets.
Tests: those of the deleted helpers (crossings, ray parity, closest_param, segment boxes)
are gone; the free-form test now asserts what the exact boolean gives (the half-ellipse
notch is the ellipse's own conics, area exact to 1e-6; the joined ellipses are conics), and
the fitted-arc budget assertion is dropped. Without the smlib feature the booleans are
refused ("not supported yet").
(not built: base in progress; cargo check shows no errors in prism.rs or smlib_sweeps.rs
with and without the smlib feature)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Wrappers of the fork's smlib_planar_faces (a sheet of planar faces from closed curve loops in
one plane, nested by SMLib) and smlib_boolean2d (two such sheets combined as regions of the
plane by SM_API's 2D boolean, the inputs' own curves split where they cross; an empty result
is None). SMLIB_REV moves to the fork commit that adds them (78617dd).
clippy -p solidcraft-smlib --features native --all-targets -D warnings: clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The general fill (G1/G2 patches, patches through points, loops that bound no height
field) is solidcraft_geom::fill_iso's patch on the loop's own edges: each line or spline
edge, on its interval and run the loop's way, is a side, so its pcurve is a straight line
in (u, v), exact. The frames across a G1/G2 edge are read off the one face beside it in
the record: its outward normal, the way it leaves the edge, and for G2 its normal
curvature from the surface's exact second derivatives. Corners whose neighbours' tangent
planes disagree are exempt from the angle tolerance. Loops of five or more edges, and
conic, rational or traced edges, are refused by name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The loop's edges become the sides of the unit square (three sides: the fourth collapses
to the first corner, a pole), each mapped affinely onto [0, 1]. Opposite sides are made
compatible exactly (split into Bezier pieces at every break, raised to the higher degree,
rejoined), the bilinearly blended Coons patch is the start, and with the boundary rows
held the interior control points solve the G1/G2 rows, the points to pass through and a
vanishing multiple of the thin-plate energy by least squares, refined by halving spans.
Every edge lies on the patch exactly and its pcurve is a straight line in (u, v), affine
in the edge's own parameter: no pcurve is fitted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A solid body's volume, area and centre are SMLib's mass properties at its documented 1e-4 and its
box SMLib's exact one; a body SMLib cannot measure is an error. The three-mesh measure and the
mesh cross-check beside SMLib's numbers are gone, and with them the kernel switch
(SOLIDCRAFT_KERNEL, Backend): SMLib is the kernel. The smlib feature gates in the crate root, the
validity check and the offset backend go, since SMLib's crate is always linked (answering not
built without the feature).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Encoding writes the body's own record and decoding makes the body from it: the adapter to and
from the old kernel is gone. A record goes into SMLib as given (SMLib's own form); one SMLib
refuses is an error that names why, never sent through SMLib's healer. A body made by SMLib keeps
the SMLib body. The bounded preview of a decoded body is a coarse mesh from the planner, charged
to its triangle budget.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A boolean is SMLib's boolean, its history SMLib's tags: the jittered retries, tool nudges, prism
and coaxial routes, the retry result guard and their diagnostics are gone. A rigid placement moves
the record; any other matrix goes through SMLib's transform (new Brep::transformed over
smlib_brep_transform). Bodies apart join as their records side by side (brep part::compound),
with each piece's faces, sources and history; apart means their exact boxes do not meet. A split
is the intersection with a half-space box, with no faces pushed off the plane.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The fork head carries untrim and extend shims with their history tags, the local solvers that
take no value from a failed evaluation (solidcore#481), the B-spline curve radius check
(#475), the torus minor-interval check (#482), the blend rail along its face's rim (#485) and the
intersection path's statuses.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
What stays is the composition of a multi-step operation's histories (composed, placed, kept,
through, operand, of_pair, Traced). healed(body) is crate::heal::heal with the body's split
lines kept, its history over [body] SMLib's, each face keeping the construction source its
faces shared and the body its regularization protection (as the prism result's healing did).
Deleted with the old kernel: heal_build, truck_result, heal_body and heal_prism_result (heal
and record::after replace them), Shapeops and shapeops (truck's boolean history), edges_evaluate
(sampling truck's intersection curves), copied and void (truck copies and the void boolean,
no callers left), join (records apart join through brep::part::compound) and parts_of (a lump's
history is history::of_part). Tests: the truck-retry tests (shifted attempts, the nudged tool)
deleted; shell counts read the record.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Body::from_record takes the finished record as the body, so its faces and edges are the
record's and the history needs no round trip through SMLib's numbering. The edge origins
(kept edges, pieces) go to record::attach, which gives every other edge its faces' meeting.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
An operation that builds its result says, per result face, what it is made from (an operand
face it is the whole of or a piece of, profile segments, caps, operand edges a blend replaces),
and per result edge the operand edges it is or is a piece of (pieces_of); of_faces and attach
make the history of the result's record from that, every other edge the meeting of its two
faces, operand faces under several result faces marked Split. That is the one model in place
of the identity-based Operand, Rebuild/Was, traced/build/build_placed, edge_index/face_index/
edge_origins/edge_provs/edges_by_identity, from_faces, solid_counts and rebuilt/rebuilt_body,
and of the truck sweep followers (ruled, follow, columns, Column, swept_face), all deleted.
The face constructors, regions, counts, after and meeting stay. Tests read edge_faces of the
body's record.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The SMLib point sweep refused a polyline whose points turn gently (a smooth run
of more than two points), a branch on the shape of the input: a polyline is
the path its points make, and SMLib's cornered sweep mitres it at each point.
Curves with an exact form no longer reach the kernel as points (previous
commit); what still does (curves projected onto a face, intersection and iso
curves, sweep_path's point lists) is followed as those points.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A sweep path through 3D sketch curves sampled every planar sketch curve in it
into a 0.01 mm polyline, and took every linked 3D curve as the points its link
stored, so SMLib got a many-cornered polyline (and refused a smooth one). Now:
- a planar sketch curve in such a path is its exact segments
(PathPiece::Segs of PathSeg::of_sketch over exact_segs);
- a linked curve whose source has an exact curve keeps it: Wire::exact holds
the record curves (WireCurve: schema 4 curve + interval) end to end along the
wire, set with the wire when the link is resolved: an included body edge its
record edge (kernel::brep_record), an included sketch curve leaving the plane
its segments (kernel::sketch_record_curve). Wire::map carries them with a
placement. The sweep follows them through kernel::curve_path (line, arc,
ellipse as rational quadratic spans, polyline, B-spline as Bézier spans).
- curves known only as points (projected onto a face, intersection and iso
curves: the link resolves them on the mesh) stay points.
Kernel, for the sheet and blend ports: planar_face_record (a profile face as a
schema 4 sheet record with exact pcurves), smlib_sweeps::chain_curves (a
segment chain as SMLib curves), body_from_record (any record into SMLib,
checked), sketch_record_curve; record curves from design files are checked in
curve_path. Format: pending/wire-exact-curves.rs.
lib.rs (coordinator): export SpaceBezier, curve_path, edge_path,
sketch_record_curve and planar_face_record from the kernel.
(not built: base in progress; kernel and sketch check clean, doc not reachable)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
PathSeg::Bezier(SpaceBezier) replaces PathSeg::Cubic: a rational Bézier span in
space of degree 1 to 9 (homogeneous control points), so a drawn spline's cubic
spans and an edge's own curve are one kind of segment, swept by SMLib as the
NURBS it is. SpaceBezier evaluates, differentiates (quotient rule on the
hodograph), measures and walks by length (Gauss-Legendre), reverses and maps.
edge_path gives a body edge's record curve as path segments exactly: a line,
an arc, an ellipse as rational quadratic spans, a polyline's lines, a B-spline
as its Bézier spans between knots (bspline::segment); an intersection curve
(no exact curve in the record) is refused. PathPiece::Segs carries such exact
curves in a point path; PathSeg gains reversed, mapped (a placement) and
of_sketch (a sketch segment on its plane). The cubic_* helpers are deleted.
surfaces.rs: its one PathSeg::Cubic arm follows the rename.
lib.rs (coordinator): export SpaceBezier and edge_path from point_path.
(not built: base in progress; cargo check shows no error in these files)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
extend_surface keeps its signature, options and error messages. One model:
SMLib extends the surface past a side of the face's parameter box
(smlib_extend_sheet, CreateExtendedSurface: natural continues the end spans,
tangent G1, curvature G2) and the face stays one face on it, tagged so the
face is Modified and its sides keep their edges. Several picked sides grow
the box on each. Up to a target, the face is extended 50 body sizes past
its edge and trimmed back with trim_keep (SMLib's exact intersection),
keeping the piece the face lies in. Back to the bounds is untrim of the
picked edge's face, in place, with its holes; one already there is
refused. A plane's straight edges with neighbours lengthened still go
through surfaces::extend. Edges are picked on the schema 4 record (samples
of each edge's curve), an edge two faces share or of a hole refused as before.
Not ported (every far edge there was a cubic fitted through marched
samples, which the exact-B-splines rule forbids): an edge that is not a
whole side of its surface's box (trimmed edges, curved planar edges such as
a disc's, a notch), mitred corners on such faces, sides run square to the
edge on a non-box face, and bodies of more than one face. These now answer
"not supported yet: ...". Tangent and curvature no longer add a separate
run-off face: SMLib's G1/G2 extension is one face (the engine's
surface.extend description says so).
Tests ported keeping their assertions: the planar extension, the cylinder
strip along its axis, around and both (now an extruded arc, not a truck
revolution), the extruded strip, the parabola extended naturally (now a
conic extruded), back to bounds (the saddle from loft_sheet and trim),
hostile requests. Deleted, as what they test no longer exists:
neighbours_lengthened_or_square, self_intersecting_extensions_are_refused,
a_disc_grows_by_its_curved_edges, neighbouring_faces_extend_together, the
tangent/curvature run-off half of the B-spline test, and
a_continued_bspline_is_unchanged_on_its_range (the deleted continue_span).
SMLIB_REV: fork 9531966 (tags carried through untrim and extend).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
untrim keeps its signature and messages. The face is picked in SMLib by a
point inside it (the centre of its largest mesh triangle), holes by their
points; SMLib deletes the holes or rebuilds the face over its surface's
parameter box with the holes kept (fork c6c7fb4). The body goes tagged as
operand 0, so faces and edges come back with their origins; the untrimmed
face is Modified. A face whose span goes all the way round its surface now
closes on the seam instead of being refused.
solidcraft-smlib: Brep::untrim_face (ffi/trim.rs, trim.rs) and
Error::NotExact for SMLIB_ERR_NOT_EXACT.
Tests: ported to the record (loops counted on brep_record; the saddle made by
loft_sheet and trim instead of truck types; the torus face copied out by a
point). Deleted: the_outer_loop_is_found_whatever_order_the_loops_are_in: it
built a truck face listing its hole first; SMLib's import always puts the
outer loop first, so the case no longer exists.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
trim / trim_keep / mutual_trim keep their signatures and behaviour; the
truck walk over the split solid is gone. The body is split where the tool
meets it (splitface::split_any), each piece's side is the tool's field at
the centre of its mesh triangle where the field is largest (the record
mesher), regions come from splitface::regions, and the pieces not kept go
through remove_faces, whose history record::after carries back to the
body. A mutual trim's nearest boundary edge is found on the schema 4
record: edges one face uses once, their middles through CurveEval.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A solid body holds its record (canonical faces, edges and vertices are the body's everywhere) and
builds its SMLib body from it on first use, or keeps the one the operation made. Queries read the
record: planar faces, outline and exact bounds, counts, closed shells, lumps from its material
regions (record parts, with their history), shell nesting from its regions. The orientation mesh,
the revolved-face rephasing, kept tolerances and the old kernel's mesh caches are gone. History's
edge faces come from the record's edge uses. The kernel no longer depends on the old kernel's
crates; SMLib's crate is a plain dependency (without the smlib feature it answers not built).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
compound concatenates the records and takes the part of all their material, so the result is
in canonical order with each face, edge and vertex numbered from its input. part now keeps an
island inside a void when both its region and the void's material are taken.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
patch_loop picks the closed loop among the record's edges, certifies a plane (lines by
their ends, splines by their control points, conics by centre and tilt, traced curves by
a planar support) or fits the height field, and assembles one face on the body's own edge
records; the height field's parameters are plane coordinates, so each line or spline
edge's pcurve is its exact projection (segmented to the edge's interval). The history
marks the face generated and each edge the body's own. A fit on circular, elliptic or
traced edges, whose pcurves on the spline have no exact form, is refused by name, as is
the general fill (G1/G2, points, loops bounding no height field) until it is ported.
Tests ported to the record; the general-fill ones stay and fail until then.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
corner_g2_patch builds the same tensor patch and returns CornerPatch: the B-spline
surface record, its v = 0 boundary line (the net's first column on the same u knots)
and each side's interval of it, so the seams lie on the patch exactly without cutting
the curve. The fold check and seam_error read the record surface through SurfaceEval.
Tests ported (the rolling ball is a sphere record).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Edges, the faces each bounds and each face's outward side come from the schema 4
record; stations on the edge's curve (CurveEval::derivs for its tangent) are projected
onto each face's SurfaceEval, and the normal curvature across the seam is read from the
surface's exact second derivatives (SurfaceEval::second). Behaviour, budgets and
messages unchanged.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Closed forms on cylinders, cones, spheres and tori; on a spline the basis functions' own
second derivatives (local_basis) through the quotient rule for the weights; zero on a
plane. A revolution has none (its profile evaluator carries first derivatives only).
The seam continuity measure reads normal curvatures from them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
fair_bspline_curve/_surface and fair_nurbs_curve/_surface take and give the schema 4
record's Spline and RationalSpline kinds (the weights kept, the knots unchanged).
fair_faces fairs the picked faces' surfaces in the body's record, keeps its loops,
edges, vertices and pcurves, and has SMLib build the body; its history marks the
faired faces modified and every edge kept when SMLib keeps the record's order, and is
left off otherwise. Tests ported to the record types with their assertions.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The faces' descriptions become a record with exactly the topology they give: vertices
and edges shared by key (lines, circles or flat arcs as their exact rational quadratic,
Bezier chains as B-splines), planes about the boundary's mean, revolved lines as
cylinders, cones or planes, extruded arcs as cylinders, ruled walls as B-splines with
exact iso-line pcurves. assemble() groups faces into shells, puts closed shells in
regions (a cavity where the outward normals enclose negative volume, in the solid whose
winding number about it is one), orders everything canonically and validates it.
SMLib imports the record as trusted; the body and the face each body face came from
follow SMLib's order. The edge-sheet and vertex-fan logic for touching bodies is kept.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
assemble (records rebuilt after removing and adding faces) joins; the boolean guards that re-ran
a failed boolean with a changed tool (bore_guard, candidate_reach, sphere_lens, tangent_cylinders,
touching_walls) are gone; the lens-cut result stays as a boolean test. SMLIB_REV at the fork's
integration head with smlib_merge_faces.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
heal: a body's faces on one surface merged and its edges on one curve joined by SMLib
(Brep::merge_faces, SmBrep::RemoveTopologicalEdgesAndVertices), the edges along its split lines
kept, with SMLib's history over the body. Its booleans already end the same way
(SmMerge::BooleanPostProcess). The truck healing is deleted with what only it did: coplanar and
collinear merges by hand, flat surfaces rebuilt as planes, and approximate edges refitted as
lines or arcs from samples. on_split_line stays; edge_points reads a record edge.
Deleted, with no port: the retries around the old boolean's failures (touching_walls,
bore_guard, tangent_cylinders, sphere_lens, tool_reseam, candidate_reach). Each re-ran a failed
boolean with a changed tool or split faces; SMLib's boolean is the one path, and a case it
fails is its defect to fix there. Their own tests go with them (the helpers' unit tests in
issue_788_tests, bore_union_tests, candidate_reach_tests, and sphere_lens' relaxed-tool test);
the #2750 lens cut's result test stays, as a boolean test, without the retry diagnostics.
Heal's tests built truck prisms and a truck spline edge: deleted.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The wrapper of the fork's smlib_merge_faces (SmBrep::RemoveTopologicalEdgesAndVertices): faces
on one surface meeting along an edge become one, edges on one curve meeting at a vertex nothing
else uses become one, and the edges under the given points stay. SMLIB_REV moves to the fork
commit that adds it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The neighbours of the deleted faces are rebuilt from the body's record: each keeps its own
plane (every moved corner lies on it) and its edges away from the gap; corners on the gap move
to where the neighbouring planes meet, and the gap closes with straight edges shared between
the rebuilt faces. Moved and restored edges are recorded as pieces of the edges they replace
(assemble.rs: Assembly::piece), so the history is as before: untouched faces kept, neighbours
modified, edges kept, pieces of their edges, or where their faces meet.
Test deleted: delete_pocket_keeps_an_imported_face_around_it bowed the edges of a truck body in
place to show that a face losing only whole loops keeps its surface; a record body has no such
in-place edit, and every rebuilt face now keeps its own surface.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The band is built on the body's record: the face keeps its other loop and the band's edge, the
walls rise along the face's loop (continuing the walls below) and along the band's edge
(planes along lines, cylinders along arcs), and the band's top is the face's plane moved up.
Each face is given with its outside, so no orientation is tried: the eight flips the old
construction tried in turn are gone. rim_band reads the loop's lines and arcs from the record.
History as before (the trimmed face, each wall from its loop edge, the top an offset).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The emboss is built from the body's record: the wall's face loses the profiles' footprints,
each profile adds planar walls and its cap on the wall's cylinder at the new radius, and SMLib
builds the body from the finished record (assemble.rs: a record taken apart and finished again
in canonical order, pcurves from the record's routine for new uses, the record's own for kept
ones). surf_of and with_same_surface read the record's analytic surfaces; the topological
offset and draft that only the old kernel used are deleted (SMLib's offset faces and draft in
offset_backend replace them).
#3782: the saved pick had to lie on the half of the wall the sketch is tangent to (its angle
inside that face's span). The wall is every exterior cylindrical face of the body (a stepped or
interrupted cylinder is refused before), so the pick is checked on the cylinder between the
endcaps. The sleeve test's far-side pick now succeeds; a pick on the bore is still refused.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A lump of a body is the part of one of its material regions: its outer shell, its open and wire
shells and a void for each closed inner shell, renumbered in the record's canonical order, with
the source index of each face, edge and vertex for history. Islands inside those voids are parts
of their own. Read from the regions the record already holds, with no mesh or ray test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The fork's shims now hold an operation SMLib reports done to having done
something (SMLIB_ERR_NO_EFFECT, -1107: a blend that made no face, a face
move or draft that moved no face). It maps to Error::NoEffect, and the
kernel says "SMLib made no change: ..." for blends and for offset, shell
and draft. Case 1080 (chamfer over openings) returned an unchanged body
as success.
The fork pin moves to 6bb2938: the chamfer side fix of the piecewise
merge, the no-effect contract, and SmStatus marked must-use with the
dropped statuses of the shims and of the fillet, boolean, offset and sweep code passed on.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
faceted_boolean (a body with a closed triangle mesh) faceted the body's curved faces and ran
its own BSP polygon boolean, then rebuilt a truck solid. It now builds the mesh into a
solid: each run of coplanar triangles sharing edges (corners within `tol` of each other's
planes) one planar face, its boundary chained into loops (split where a loop passes a vertex
twice, holes given to the innermost outer loop), sewn with sew_touching; SMLib's boolean
takes it with the body, whose faces stay exact. The history over [a, mesh] is the boolean's
over [a, tool] with the tool's own over the mesh substituted: each tool face from its
triangles, each tool edge the meeting of the runs either side.
Deleted with the BSP and the rebuild: planar_boolean and sliced_planar_boolean (the truck
boolean's exact fallback for touching planar faces: on SMLib the boolean is one path),
reassembled and split_beside (prism_local's), components_in_order (truck shell order) and
the tests of what no longer exists (face_planes diagnostics, the planar fallback beside a
plate's small rounds).
lib.rs (coordinator): `pub use polybool::faceted_boolean;` (planar_boolean is gone).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The prism module kept two things: the 2D arrangement of planar regions
(planar_regions_boolean, and extrude_planar_regions, which normalises the material with it,
proves the pieces apart and extrudes them with crate::extrude, SMLib's extrusion with its
face sources), and a second route to the solid boolean: prism operands read section by
section, the result sewn and its history rebuilt from section tags (prism_boolean,
prism_union_between_caps, the stacked and free-form variants, prism_lineage.rs) and the
column-local variant (prism_local.rs). On SMLib a boolean is smlib_boolean, with SMLib's
own history: the second route is deleted, with its tests (prism_spline_tests.rs, the
stack/prism tests in planar_tests) of what no longer exists.
The section tags shrink to what the arrangement uses: the free-form segment a fitted arc
stands for. wall_along reads a record surface (cylinder, revolved line, spline net) for
clearance.rs. The 2D tests and the wall test keep their assertions.
lib.rs (coordinator): drop `mod prism_local;`.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
certified_prism_equivalent proves two bodies the same uniform prism from their records
(crate::native::brep_record): caps are the planar faces square to a candidate direction,
their outward side from the shell's oriented faces, their loops run about the outward
normal and read by prism_curve; walls are planes, cylinders (their axial extent from the
face's domain), revolutions of a line or ruled spline nets, each bounded as before. The
interval arithmetic, loop matching and separation proof are unchanged. Tests build their
altered bodies by editing the record (a wall's surface swapped or moved, a cap plane's
origin moved far away) instead of truck faces, with the same assertions.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
prism_curve::segment and ::projected take a Run (a record edge's curve over its
interval, run one way, with the vertex points it must reach) instead of a truck edge.
Lines and circles are read exactly as the record holds them; a polyline as its lines; a
B-spline or NURBS curve span by span (clamped to the interval with Boehm insertion, then
split into Bezier spans), certified by the existing whole-span Bernstein proofs; an
ellipse as its exact rational quadratic spans of at most a quarter turn. segment_fitted
(arcs fitted to a spline, only the prism boolean's free-form route used it) is deleted.
The existing tests run on record curves; the part of the aliasing test that asked the
deleted prism boolean to decline is dropped.
Callers in body.rs, bore_guard.rs, sphere_lens.rs (other owners) build a Run with
prism_curve::Run::of(record, edge, forward) or Run { curve, interval, forward, ends }.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
measure_backend asked SMLib for mass properties at 1e-8, the finest its
integrator allows and below the range SmApiBrepComputeMassProperties
documents ("Relative accuracy, clamped to [1e-4, 1e-1]", SmApiQueries.h,
whose cost is "governed by dRelativeAccuracy"). The tight request was there
because the documented range did not hold on faces bounded by circles
(KNOWN_ISSUES.md); the fork's dcbb07d integrates each trim-curve knot span
on its own, and such faces now meet it (a cylinder's volume 1.8e-6 off at
1e-4). At 1e-8 SMLib's measure was most of the regression rig's time on a
modelled thread (threads/602).
The measure now asks 1e-4: ten times inside the 1e-3 it is held to (AGREE
against the mesh measure, and the rig's volume and area tolerance). NURBS
spheres and tori can still miss 1e-4 by a small factor (tangency-labs/
solidcore#384; a torus's volume 2.5e-4 at 1e-4), inside that 1e-3.
Brep::mass_properties keeps MEASURE_ACCURACY (1e-8) as its tests' default.
SMLIB_REV: the fork at dcbb07d.
Not built: base in progress (kernel-smlib/no-truck 52f25ca3 does not build).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A rotation and a translation move every point, axis and control point and leave parameters,
pcurves, tolerances, orientation and the canonical order as they are: the same B-rep in other
coordinates. Placing a body (assembly instances, patterns) no longer needs a kernel round trip.
Reflections and scalings change parameters or geometry kinds and go to the kernel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
SMLib's blend builds rolling-ball rounds, corners, loops and vertex spheres in one call; the
modules that built them on the old kernel were deleted by the blends port.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A join is hidden when both faces are clamped Bezier B-spline surfaces swept by one
linear translation (read from the record's spline net, the face's side from its shell)
and the edge is a straight line on a ruling of both, their normals agreeing within the
certified angle, as before. The edges are the record's, in the order the mesh has them.
Tests take the record (a closed edge's equal end vertices, the ruled face by its spline
net, ruled() on record control nets); the case that bowed a truck edge in place is
removed (a record edge cannot be changed under a body).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
curveblend.rs (rolling-ball rounds and chamfers of curved edges, their corners, the law
and chord variants), curvecorner.rs, loopblend.rs (blends round whole face loops) and
blend_vertex.rs (a sphere at three rounded edges) built blends on truck; SMLib's blend
makes all of these in its one call, with its own corners. curvecorner_tests.rs tests
fillet and chamfer through the public functions only, so it stays, mounted from blend.rs.
curveblend_tests.rs and curveblend_law_tests.rs go with the builder: they test its
internals (fit_sections, curve_blend_with_limit, ROLLS, its NURBS surfaces and contacts).
lib.rs needs `mod curveblend;`, `mod loopblend;` and `mod blend_vertex;` removed.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
fillet_variable, fillet_variable_points and fillet_variable_spline round the edge with
SMLib's variable-radius solver (smlib_fillet_variable) on any edge; fillet_chord is
SMLib's constant-chord round (smlib_blend, SMLIB_BLEND_CHORD). On a straight edge
between planar faces a radius law whose band runs past the faces at any of 256 stations
is refused as too large before SMLib runs, as before. The edge setup, the band check and
the taper's room read the body's schema 4 record: planar faces' outward normals, the
directions into them, side edges' exact tangents at the ends; a curved boundary is
divided to within the clearance for the containment test. RadiusSpline evaluates with
the geometry crate's B-spline basis.
The truck variable-radius surface builder and the rolling-ball law and chord blends are
gone. taper_mesh_tests keeps its mesh-accuracy test; its three tests of truck's mesher
on a surface with an arbitrary pole are removed (that mesher no longer exists).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The corner builder (one, two or three rounds at a vertex of planar faces, taking earlier
rounds off and blending them again) is gone: SMLib's blend makes those corners itself,
with each edge's radius. The tests check through the record (curved faces' normals along
their shared edges, vertex points, planar faces' vertices on their planes). Removed: the
assertion that a concave edge meeting a convex one is refused, a limit of the deleted
builder.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
fillet, fillet_each, fillet_g2, chamfer and chamfer_sides blend every picked edge in one
SMLib call (smlib_blend / smlib_chamfer_sides), which builds the rounds, their tangent
chains and the corners where they meet. The #1080 fit check (blend_fit) reads the body's
schema 4 record: planar faces' outward normals, the direction into a face across a
straight edge, edge tangents at the ends. It now runs before SMLib, so a blend that runs
past its faces is TooLarge with the largest size that fits (fillet_max_each, the taper
and the doc's reduce path rely on that), and the volume and wrong-way checks after it.
fillet_max_each groups the edges by their faces and ends in the record. fillet_g2_corners
is refused: SMLib's corners are a sphere or a Coons patch, not G2.
The local truck blends, their reorder search and every truck helper they used are gone.
Tests: blend.rs's tests call fillet/chamfer instead of the deleted internal `blend`; the
assertions on the deleted quintic construction's surface and curve parameters and on
extension_fits (deleted) are removed, the analytic volumes kept.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Every builder in build.rs keeps its signature and is SMLib's: extrude and
extrude_whole_sides (smlib_extrude), revolve and revolve_whole (smlib_revolve;
SMLib makes one face per profile segment and cap, so the two agree), the
tapered extrude (smlib_extrude's draft), loft, loft_with_rails, loft_regions
(smlib_loft, smlib_sweep_stations for rails), sweep and sweep_with
(smlib_sweep / smlib_sweep_placed, controlled ones smlib_sweep_stations). Box,
cylinder and torus are built through those, keeping their face sources and
history; sphere is SMLib's own (smlib_make_sphere), each face from the half
disc's arc as before.
Deleted: every face-by-face build (tsweep/rsweep shells, the revolve-on-axis and
touching-axis builders, poles, reseams, the healed whole revolve, the six-patch
sphere and its rotated variant, the tapered side faces, cap_and_close,
close_sweep, the full-circle sweep), and the taper's crossing pre-check and curve
splitting that only that path used (offset_crosses, offset_splits,
offset_error, offset_tolerance). Wire, loft-face and interpolation helpers other
modules still build on stay until those are ported.
(not built: base in progress; cargo check shows no error in this file)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
sweep_path_with is SMLib's sweep along the path's lines (smlib_sweep_points
through sweep_with, corners mitred). Deleted: the B-spline walls fitted through
sampled stations (smooth_walls, smooth_closed_walls), the ruled walls between
stations (ruled), and the helpers only they used (rotated_wire, start_cos,
swept_volume). The open walls of chains (ruled_sheet, smooth_sheet) stay for
surfaces.rs's sheet sweeps until those are ported.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Brep::sweep_placed (smlib_sweep_placed, fork f6f428b) returns the rigid motions
carrying the profile onto the sweep's first, middle and last sections. Each run
of a path with corners now takes its end pose from the last motion (less the
straight lead-out along the end tangent) and its inside seeds from the middle
one, for every path segment kind: the 3D sketch spline next to a corner is no
longer refused. Deleted: the predicted carry along a run (carried) and
point_path::cubic_split, which only it used. SMLIB_REV at f6f428b.
Fork: build + ctest, only smlib_flat_roundtrip's known incline failure.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
convex_polyhedron builds its schema 4 record straight from the planes (corners
at triple-plane intersections, line edges, planar faces with exact line pcurves,
canonical order) and SMLib takes it (Brep::from_record, trusted) and checks it.
shell, draft and offset_faces are SMLib's local operations (smlib_shell,
smlib_draft, smlib_offset_faces through offset_backend).
Deleted: the half-space shell and draft of convex bodies, the planar-offset
cavity shells (barrel, opening overreach, opening sweep), the non-convex draft
walls, the split-piece press after a failed offset, round_all_edges and the
helpers only they used, and the test of the deleted rim pre-check
(rim_preflight_tests).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
sweep_helix is SMLib's screw sweep (smlib_screw_sweep) and loft_to_point SMLib's
loft to a point (smlib_loft_to_point), with their checks and history as before.
Deleted: the B-spline walls fitted through sampled stations (smooth_helix), the
ruled walls between stations and the face-by-face pyramid, and the Pappus
estimate only they used.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The case expected two bodies for a combine join of an arm 0.01 mm clear of a
ring. A join's tool that meets no target goes into the target as a separate
lump and the tool body is consumed (doc eval Operation::Join; the engine test
join_meeting_nothing_keeps_the_first_body): one body, Ring, of two shells,
with the volumes' sum. The case's point, nothing fuses, is kept: shells 2 and
the summed volume.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
ladder/05/011 expected 8 faces (the stock's 6 and one per hole). With equal
radii the two walls cross along two ellipses that meet at the two points
where the walls are tangent (x = 25 and 35, y = 20, z = 10), so each wall is
left in two pieces that touch only at those points, with no edge between
them: separate faces, 10 in all. SMLib makes those 10 (volume exact); truck's
8 came with an invalid record. Derivation in the case's method.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
With no targets picked, an extrude's Join, Cut or Intersect changed every body
of the active component it met, shown or hidden: a hidden coincident copy was
joined in and disappeared. Where some bodies of the active component are
hidden, the shown ones become the feature's participants, kept with it, so
showing or hiding bodies later doesn't change the model. Explicit targets and
participants, and a face's or a face sketch's own body, still win.
Test: the issue's plate with a hidden copy and a rim joined on.
Main reached the same fix for Cut and Intersect in #3961 (its targets-based
shown_scope in extrude); this one, which also covers Join and keeps the shown
bodies as participants, replaces it. Its test stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A closed path was always followed from the start of its first picked curve,
wherever the profile stood on it. A profile drawn at the middle of a line was
then carried round the path standing off it by the distance between the two
(walls offset, the rounds turned the wrong way, on both kernels). Now the
closed chain begins where the profile's plane crosses it nearest the profile,
the curve there split in two (or at the joint there); open paths and paths
that already start on the profile's plane are unchanged.
Test: a 4 x 3 profile at the middle of the bottom line of a rounded rectangle
swept round it: Pappus volume and the tube within 2 mm of the path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Replaying a Mirror or Pattern feature asked it for its tools, which only primitives
and sketch features have: 'Mirror1 cannot be patterned or mirrored yet' (#3652). A
copy of a pattern or mirror of features now replays that feature's own features with
its moves followed by these (a mirror of Mirror1 reflects the hole Mirror1 made),
nested at most 8 deep, never through itself, and to at most MAX_INSTANCES copies in all.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Face capture estimated its work as every face times every triangle and refused a
body past 2M (#3914, a threaded block: 'face capture work limit exceeded'), though
only planar faces through the point scan the mesh. Each such scan is now counted
when made.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
solid.revolve and surface.revolve took any other axis string as a sketch line id,
so a construction axis by name failed with 'unknown sketch line' (#2516). A name
that is no line of the profile's sketch and names a construction axis is that
axis where it is now, as picking it in the view already passes it ({origin, dir}).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Three defects in restating trimmed surfaces (144/143) as STEP, kernel-agnostic:
- A boundary loop is one closed curve (IGES 5.3 composite curve: each piece ends
where the next starts), but each piece's ends were sewn into vertices by
distance alone, so pieces that miss by more than the sewing tolerance left the
loop open and the face was dropped (#3845). Surfaces are now read first and sewn
after: every meeting point of two pieces is one vertex (union of the two), the
STEP reader then carries the distance as the vertex's tolerance.
- With no parameter-space curve to give the start, a composite whose first piece is
written end to start kept that direction, so the second piece did not connect
(#3913). The first piece now runs from its end away from the second.
- All sewn surfaces were written as one shell, so two separate closed boxes came in
as one body of two shells that SMLib's check refuses (#3972). Each connected set
of faces is now its own shell and body (Body1, Body2, ...); a shell writes only
the vertices its edges use, in the sewer's order.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
EvalSurface::wrap asked uv::closed whether a spline closes on itself every
time it evaluated a point outside the knot interval, and that test walks the
whole control net (its extent, then the first and last rows). A tessellation
of a closed spline unwraps its parameters across the seam, so about half its
evaluations are outside: on a coil's helical tube (thousands of control
points) the test was 80% of the tessellation's time, 30 s of the coil
command on SMLib (sweeps/3832, sweeps/793).
EvalSurface::Nurbs now carries `closed`, set by EvalSurface::nurbs from the
same test; wrap and uv::domain read it. An affine map of the surface keeps it
(closedness does not change under one). The answers are the same as before. The constructor's callers in
crates/kernel/src/tess (size.rs, surface.rs) change with it.
Not built: base in progress (rebased onto kernel-smlib/no-truck 41adb372).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The tests in tess/ build their bodies as before and plan crate::brep_record of them; the
assertions stay. Changed where what they compared is gone:
- surface.rs: each face evaluates through eval_surface and its record-parameter map as the
record's own evaluator (SurfaceEval) evaluates it, point and natural normal over the face's
box (was: as truck evaluates it); the kinds counted are the record's (planes and analytic
surfaces; SMLib's primitives have no spline faces, which truck's sphere was).
- edges.rs: edges are the record's; the intersection-node test becomes: every edge between two
curved faces puts each node, through each face's pcurve at the node's curve parameter, on that
face's surface at the node, within the edge's tolerance (was: Newton's per-node parameters
within 1e-7, which tess/ssi.rs no longer computes).
- tests.rs: "reference mesher" comparisons are against Body::mesh (the same mesher on the same
record, at a finer tolerance for the cross-hole volume). Removed the assertion that
seam::turns_more_than_once finds the thread's winding walls: that test of which mesher to use
no longer exists.
(not built: base in progress) tess/ has no errors under cargo check --tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Each edge node keeps its curve parameter; each face reads its nodes' parameters from its own
pcurve of the edge at that parameter, so nothing on the boundary is projected and a seam's two
uses (each with its pcurve) are its two sides: the cases the projection could not tell apart
("boundary winds around the surface", sides that cross where projection snapped them, poles).
The record's (u, v) go to the evaluator's in closed form (tess::surface::Params): the same for
planes, splines and revolutions; (θ, v) to (−v, θ) on a cylinder's or cone's generator; on a
sphere or torus (θ, φ) to (1 − s(φ), θ), s the rational meridian's parameter, from
tan(φ'/2) running linearly over each span (φ' from the span's middle).
A pcurve that does not cover its edge's interval, or places a node farther from the edge than
ten times the chord tolerance plus the edge's own tolerance, is an error naming the face and the
edge: the record is wrong there (for an analytic face, a trim left in SMLib's internal NURBS
parameters rather than the record's would show here).
(not built: base in progress) tess/ has no errors under cargo check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
On kernel-smlib/no-truck: sweep_point_path_with goes straight to SMLib's sweep (no truck
branch), and PathSeg::flattened (truck's polyline of a path) takes no Cubic arm (the wildcard
keeps the segment).
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A drawn 3D spline reached the kernel only as its sampled polyline (Wire.pts), so SMLib refused
to sweep along it ("not supported yet: SMLib sweep along a smooth path of points"). The doc
holds the spline's fit points; the curve is the centripetal Catmull-Rom spline that
fit_polyline samples. Each span of it is a cubic (Barry-Goldman), and its derivative at a fit
point depends only on that point, its neighbours and the two knot intervals, so neighbouring
spans share it: the spline is exactly one cubic Bézier per span, C1 across the spans.
- geom: catmull_rom holds the curve: samples() (fit_polyline's arithmetic, moved verbatim;
sketch::fit_polyline now calls it) and beziers(), the exact spans; ArcLength on CurveN
(Gauss-Legendre per knot span, Newton for the parameter at a length).
- kernel: PathSeg::Cubic, a cubic Bézier in space (the 3D counterpart of PathSeg::Curve, which
is planar), with its arms wherever PathSeg is matched (start/end, tangents, length, point at
a length, turn, SMLib curve as a degree-3 NURBS, halving). PointPath / PathPiece carry a path
through 3D sketch curves: polylines as points, drawn splines as their fit points (reversed
when the path runs them backwards); PointPath::segs gives SMLib the spans and the lines;
sweep_point_path_with sweeps it (SMLib through sweep_with, so corners, twist, scale, rails and
orientation take the existing routes). A smooth polyline piece (a projected or linked curve,
which the kernel only has as points) is still refused on SMLib.
- doc: path_points builds the PointPath (a wire with three or more fit points is a spline
piece); solid sweep and pipe call sweep_point_path_with; rails, surface sweeps and curve
networks take its points as before.
Checked in scratch (not committed): the spans pass through every fit point and every
fit_polyline sample at its parameter (1e-12), C1 at the joints and round a closed spline;
samples() is bit-identical to the old fit_polyline over 1180 random open and closed inputs;
SMLib sweeps of a radius-2 circle along an open spline and round a closed one match
pi r^2 L (L by independent quadrature of the curve's definition) to 1e-7 and 3e-8.
sketch/wire.rs: the existing closed-spline test reads SPAN_SAMPLES from its new home.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A Join with one target takes every tool, whichever it meets, but it grew the
target by one tool at a time: each union took the whole result so far, and
each step first meshed it to see whether the tool met it (which with one
target decides nothing). n tools cost n^2 in every per-body step of a
boolean (converting the operands, checking the result, exporting it back),
whichever kernel runs it: a combine of 64 separate pegs (booleans/3455) took
400 s on SMLib, of 36 lofted blades and a disk (crashes/2992, sweeps/1856)
485 s and 97 s.
With one target, the target and the tools are now unioned pairwise, level by
level, so each body takes part in about log2(n) unions. The result is the
same union under the target's name, with the lineage of every union. Several
targets keep the absorption order they need. The doc and engine join and
combine tests passed and failed on SMLib as before (measured before the
rebase).
Not built: base in progress (rebased onto kernel-smlib/no-truck 41adb372).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
uv::project, given no hint that converges within `accept`, evaluated a grid
of up to 16384 points of the whole surface for every point it placed, then
sorted all of them by distance. The grid is the same for every point of a
surface, and a face's pcurves place hundreds of points on one surface: on a
modelled thread's helical faces that grid was most of a boolean's time on
SMLib, in the operands' records, SMLib's export of each body and the
result's (threads/2526, the ladder's threads).
uv::Starts keeps the grid, filled on first use; uv::project_with takes it,
and a face's pcurve fit (pcurve::On) keeps one per face. The 32 nearest
starts are picked with a partial select instead of a full sort, in the same
order (nearest first, grid order where equally near), so every point gets
the parameters it got before. (The tessellation's boundary projection can
take the same grid; crates/kernel/src/tess is the mesh agent's.)
Not built: base in progress (rebased onto kernel-smlib/no-truck 41adb372).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
On a B-spline face the export now takes each edgeuse's UV trim exactly as
SMLib holds it (EDGEUSE_NURBS_*): the face's parameters are SMLib's, so the
trim is the pcurve, reversed exactly (knots t -> a + b - t) where the
edgeuse runs against its edge, its knots mapped onto the edge record's
parameter where that is an affine image of SMLib's (identity for spline
edges, as_line's map for NURBS lines, the C side's map for lines), and its
end knots set to the edge interval where they differ by rounding (within
the schema's 1e-9 slack, else an error). Changed: export.rs `curves`
(per-edge TrimMap), new `trims`, `mapped`, `Face::given`,
`Edge::trim_map`, the face loop in `native_ordered`, `pcurves`,
`outer_area`.
Analytic faces and circle/ellipse edges keep brep::pcurve: SMLib keeps an
analytic face's trims on its internal rational B-spline (SmCylinder.h:
"the map from one to the other is not linear") and a circle's on its NURBS
parameter, neither an exact B-spline in the record's angle parameters.
brep::pcurve::EdgeUse gains `given`, a producer's exact pcurve taken as it
is (structure and interval checked, moved by whole periods with its loop,
never refitted). Existing construction sites (STEP reader, tests, truck
adapter) pass `given: None`; the existing tests change only for the new
field.
Built, clippy-clean and tested on 4b2c0eaf before the rebase onto
kernel-smlib/no-truck (not built: base in progress).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
tess::plan takes a brep::Brep: faces, loops, shells' face sides and edges in the record's
order. Surfaces go to the evaluator's form from the record (planes, splines and revolutions
through SurfaceEval; a cylinder, cone, sphere or torus as the exact revolution of its line or
rational-arc generator about its axis). Edges are sampled on the record's own curve evaluator
(CurveEval), an intersection curve included, so tess/ssi.rs and the per-node parameters it
carried are gone; every boundary node's parameters come from projection.
No fallback is left in the planner's path: a curve that does not evaluate on its interval is
an error naming the edge (it was a two-point chord, or truck's evaluation); a surface the
evaluator cannot express an error naming the face and its kind. Errors name the face, its
surface kind and, for a node off the surface, the loop, node and point. The module doc states
the order: a cheaper layout where the face suits one, else constrained Delaunay in (u, v),
else an error.
MAX_ANGLE_BOUNDARY and MAX_RELAX are tess::size's own. Removed with truck: the winding test
and pole-to-pole seam test that chose between this planner and truck's mesher, and
boundaries_on_surfaces (truck blends). The tests in tess/ still build truck bodies and are
left for the record-based Body.
(not built: base in progress) tess/ has no errors under cargo check; the rest of the crate is
the no-truck branch's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Body::mesh / mesh_plan / mesh_of and MeshTolerance (chord, display, display_of, export_of)
replace tessellate, tessellate_with, display_mesh*, display_plan*, reference_display_mesh_angular;
every caller in kernel, doc, engine, io, ui-egui, gpu and the CLI renamed mechanically; the
truck-mesher fallback, mesh_shells*, refined* removed; Body::new orients from the planner;
truck-meshalgo dropped from Cargo.toml with the tests of its hooks. Still to fix: trait imports
in tess/*, splitface/lumps/preview/step_in/boolean_record meshalgo uses, measure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Over the fork's new smlib_brep_bounds (SmBrep::CalculateTightBoundingBox): the box of a body
from its exact curves and surfaces, for measuring without a mesh on the SMLib kernel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Through SMLib, in place of "not supported yet":
- A sweep along a path with corners: each smooth run swept on its own and
the runs joined at each corner in a mitre (each run swept on straight past
the corner and cut back to the plane halving it, where the next meets it
face to face), including the corner where a closed path closes; only
moving along the path, the runs meet on the profile. History: walls from
their segments (found half-way along each run), caps where the path ends.
- A free-form path curve (a spline's Bézier pieces, conics) reaches the
kernel as PathSeg::Curve, its exact piece, instead of the sketch's
polyline of lines: SMLib sweeps along the curve itself; truck's sweeps
flatten it as before (now the piece's own polyline within 0.01 mm, which
turns ladder 04/004 into a pass for truck too). Patterns along a path
flatten it and measure as before. A sweep that twists, scales or follows
rails places its stations by length along the curve.
(Such a controlled sweep still refuses a path with corners.)
- A loft through sections with holes: each section's outline and holes are
one SMLib profile, the holes matched as for truck.
- A loft to a point: SMLib's pyramid (smlib_loft_to_point).
- A draft extrusion with a straight foot: the foot's prism and the drafted
part (sloping from the foot's top to the far end the taper gives) joined
face to face; each segment names its foot and its sloping wall.
Cases now passing on SMLib: booleans/3633, sweeps/1184, 2435, 3111, 3006,
3505, 561, 96, ladder 04/003 and 004.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The fork's new smlib_loft_to_point (a pyramid from a planar section of lines
to an apex) as Brep::loft_to_point. The pin brings the sweeps' other
changes: a loft's section may hold holes (loops lofted loop to loop), a
sweep along a straight path is an extrusion and one turning with arcs about
one axis a revolution (exact walls), and a sweep round a closed path closes
its walls on each other, without caps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The case's own method says one body with an inner shell (the void of
radius 18.5 inside the ball of radius 40), but without a "shells" entry
the rig asks one shell per body. It now asks two, as the method derives.
SMLib still fails the case on its volume: 294558 mm3, near the sum of
the two balls (294604) rather than their difference (241561), so the
inner shell is not taken as a void.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Two SMLib fixes (tangency-labs/smlib r-shell-fillet):
- b0e84f4: faces an extended offset already mated are not intersected
again in the offset's merge (rig shell/3103, a two-half cone frustum
shelled open at the top, now one valid solid at the closed-form
volume).
- ffaf4e8: an intersection edge's end goes to its own vertex when its
ProtoVertex resolved into several (rig fillets/1488, the slab joined
with a nudged pleated loft, valid again; booleans/2801 passes too).
Every rig case that passed on SMLib at 2d7c7ed still passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
On SMLib a sweep with a twist, an end scale, an orientation direction
or guide rails, and a loft with guide rails, were refused. They now
place the profile at stations by the laws the kernel already defines
for them (sweep_path::controlled_stations / railed_stations, and the
loft_rails law: plain loft between the sections moved onto the rails),
each section the profile's exact image, and SMLib skins the walls
through them (Brep::sweep_stations, Brep::loft). The path is sampled
exactly on its lines and arcs, a station per degree of turn: SMLib's
skin interpolates with free ends, its error falling as the square of
the step (a quarter-twisted square is 2e-7 off in volume at 1.4
degrees, 2e-6 at 2.8). Round a closed path the last section has to
land on the first (closing_offset, as for truck) and the ends are
joined. A railed loft needs each section's curves to pair with the
others' (same kind), else "not supported yet".
A path of points (3D sketch curves) was refused outright on SMLib; a
line or a polyline with corners is now swept as its lines (the
corner refusal still applies), and only a smooth polyline of more
points (a drawn 3D spline) stays "not supported yet".
History in the builders' vocabulary: caps from the start and end
section planes, walls from the profile segments (round a closed path,
from points inside the walls at the middle station).
Kernel cases now passing on SMLib: sweeps/1269-sweep-twisted-square-
ninety-degrees, 2005-closed-sweep-twisted-cut-inside-a-torus,
2005-closed-sweep-twisted-new-body, 201-loft-one-guide-rail-cone-
frustum, 2862-three-pipes-meeting-at-one-point, 2891-twisted-sweep-
cut-through-a-tube, 771-sweep-end-scale-cone-frustum, 771-sweep-one-
and -two-guide-rail(s)-cone-frustum.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The wrapper of the fork's new shim (61ee7f5): a planar profile placed
by an affine map at each station, the walls skinned through the
sections, the ends capped, open, or joined round a closed path
(Ends). Tests: a twisted square (V = A L), a scaled disc (frustum), a
square carried round a circle with its ends joined (Pappus), and
singular or too few stations refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
SMLIB_REV moves from 276b509 to capi-solidcraft 2d7c7ed: a fillet crash at an
unsupported corner (#453), rounds ending on a tangent seam (#418), radius laws
with a corner (#430) or that cannot be evaluated (#458), a round over a hole
(#422), a sphere's own patches kept apart (#423), offset surfaces extended the
same way whatever was evaluated first (#419), swept walls sharing their planes
(#434), the internal thread's last vertex gap (#410), shells across rounds
thinner than the wall (#397), nested shells imported in any order (#388) and
the tessellator held to its chord tolerance (#390).
Three tests follow: a radius law with a corner is now built, so the SMLib
tests check its volume (8000 - (1 - pi/4) 20 28/3) and that a law pinching to
zero inside the edge is refused; the split coil's halves now measure within
1e-5 of the closed form, so they are held to it rather than to the whole
coil's mesh, which is only within 3e-3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
The kernel accepts `truck` (as well as unset) for its truck backend, and
`boolean_kernel()` reports that backend as `default`. The rig compared
the raw setting with that name, so a run with SOLIDCRAFT_KERNEL=truck
reported every case as "this build runs booleans on the default kernel,
not truck" instead of grading it. The rig now names unset, empty and
`truck` alike as `default`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A face's grid lines are thinned to MAX_FACE_GRID points (and now to at most that, where the
rounding of thin() could leave it a little over), but the cut grid adds the face's motorcycle
traces to them and then refused when its cells came out over the cap. The face fell through to
Delaunay, whose refinement budget a face of that size is far past: a split coil's wall at the
measuring tolerance got 90 000 triangles where it needs 320 000, and the halves of #793's coil
measured 1.5% short of the whole. The cut grid now thins its full lines (the same evenly spaced
choice as the face's grid, moved into grid.rs) until the cells fit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
SMLib writes a modelled thread's groove walls as splines winding once per pitch, and splits the
bore or shaft into one face per turn, so no face of revolution has a loop turning more than once
and turns_more_than_once missed the body. Truck's mesher then took the winding walls: meshes
reaching 1.4 mm past the block (#1995, #2526, #3311, ladder 06/004), volumes of -1.8e7 and boxes
120 m wide (#2038 join, ladder 06/003), or spade's "!previous_region.is_empty()" assertion inside
its constrained triangulation (#3952, ladder 06/001, 06/002). A spline face now has an axis too:
the mean binormal of an iso-line that winds more than once about it, through the iso-line's
centre; a boundary loop turning more than once about that axis routes the body to the display
plan, as a helical trim on a cylinder already did.
Truck's own coils (sweep_helix's spline walls) go the same way; their walls are longer than a
grid's 1024 lines resolve at the measuring tolerance (three turns of a 20 mm coil need ~1200),
so a direction may now have 8192, the grid as a whole still capped at MAX_FACE_GRID.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A thread's root band that runs out through an end face (a strip a quarter pitch wide winding
ten turns, ending obliquely, so it has no four corners to blend) fell through to the butterfly
of a round face. Its cuts ran lengthwise through the strip, seeded the whole way: ~200 000
points whose triangles zigzagged along the band, six times its area and up to 1.6x the chord
tolerance off the surface. A curved face whose division across one parameter has no inner line
(straight and within a side there) and with no room for a lattice seed now skips the patch and
cut-grid layouts and goes straight to Delaunay of its boundary nodes with sag refinement:
11 600 triangles for the whole part instead of 438 000, within the tolerance, the triangles'
area matching the surface's. A fillet narrower than a side but curved across keeps its layouts
(Delaunay from its boundary alone runs out of refinement budget at fine tolerances).
cdt::has_room tells "no seed fits" apart from "the lattice is too large to look through".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Each loop of a face is projected on its own, so along a closed direction a
hole can come out a whole period away from the outer loop. A revolved
dome (rational spline closed in v, bounded by its rim, its seam both ways
and the pole) has its outer loop opened at the pole into v in [-1, 0],
while a hole cut by a ball next to the seam projected into [0, 1]: the
triangulation covered the hole as well, and the cut measured 48118.36 mm³
instead of SMLib's 46424.50 (tangency-labs/solidcore#433, kernel case
sweeps/3528-revolved-dome-cut-by-a-ball-on-its-seam). Every other loop is
now moved by whole periods to the copy nearest the outer loop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
On a thread's root band (1.4 mm tall at a pitch of 1.5) a point on the band's lower rim lies
0.1 mm from the rim of the turn below. The search's starts are dense across the band and
sparse along it, so its four nearest starts all lay on the turn below, Gauss-Newton settled
there, and the pcurve fit refused the edge ("the curve at 0 lies 0.1008 mm off the surface",
ladder/06-threads/007). The search now goes on through the nearest 32 starts until one
converges within the accepted distance, and stops at the first that does.
The test strips now put their knots at other angles on each turn (7.3 control points a
turn), as SMLib's screw sweeps do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
SMLib's intersection curves carry knots of full multiplicity where the curve is in fact
differentiable (its derivative the same either side). The pcurve fit took every such knot
for a corner and fitted the two sides apart; on a thread's flank, whose u runs hundreds of
times as fast as its v, the pieces met at up to 1.4 deg in (u, v), and SMLib's check refused
the record read back (Test #7 Bad CurveKink, offset 0.9997 against cos 1 deg).
A knot that leaves a spline only continuous by its multiplicity now gets a cubic with a
tangent there when the spline's one-sided derivatives (EvalCurve::sides) agree within 1e-9
of their size. Knots where the curve does turn are corners as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
A thread cut by SMLib's screw sweep leaves flanks that are cubic splines of hundreds of knot
spans along the helix (an M6 through 20 mm: about twenty turns over u in [0, 1]). With no
hint (an edge's first sample), the projection started from a 17 x 17 grid over the parameter
box: one start every turn and a quarter, so the nearest starts lay on the turns beside the
point's own, Gauss-Newton settled there, and the pcurve fit gave up with "the curve at 0
lies 0.87 mm off the surface" (pitch x cos 30 deg, the next turn's flank). Every modelled
thread failed its export that way.
The search now starts, along each direction where the surface has its own knots (a
spline's, a revolution's profile's), from each knot span's start and two points inside it as
well as the 16 even steps, capped at 16384 points of the surface, so every turn is searched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
Without the native feature nothing reads a sweep's curve list, and clippy -D warnings
refused its two fields as dead code. The list is built through List::new in both builds;
only the native build keeps the slices the C API reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
SMLIB_REV moves from e1ac3c3 to capi-solidcraft 276b509: thread groove
booleans (#435, #410), a holed profile swept along a bent path (#411), a
revolution a turn past its range (#428), shell and offset faces (#402, #380,
#403), the check linear in the body (#431), a far box that leaked (#377),
fillets and chamfers where a neighbouring round collapses (#416, #417, #420,
#379), tangent faces round a non-manifold edge (#383) and split faces of an
exact NURBS surface (#387).
The last one is why the SMLib block-and-cylinder decode test no longer finds a
spline edge on part of its curve: SMLib keeps a split face's parameterization
now. The test keeps its volume check; the decode of such edges stays covered
by spline_edges_on_part_of_their_curve_load_as_that_piece.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
An SMLib result where one shell uses an edge four or more times (a hole tangent to the outline,
touching holes, stepped cuts touching along a line) was refused as "not supported yet: a solid
touching itself along an edge". The export now reads SMLib's radial order round each edge
(EDGEUSE_NEXT_RADIAL, EDGEUSE_RADIAL_ENTRY) and the side of each face its material is on, and
gives each pair of faces across one sector its own copy of the edge: the pairs across material,
so the surface is cut apart round each wedge of material, unless one face would get two copies
(a face on both sides of the line), which SMLib's check refuses as coincident edges (tests 33,
34); then the pairs across the empty sectors, so that face keeps one edge on both sides, as
SMLib's crack edges do. The vertices at the ends stay shared: copies would give a face passing
the point twice two coincident vertices, which SMLib refuses too (test 32). Geometry unchanged;
each copy keeps its edge's tolerance and history (Order.pieces). The radial arrays are checked
as hostile input (in range, cycles on their own edge).
Tests: a hole tangent to a block's outline exports with the line copied once per wedge (each
copy joining faces on one side of it), every edge two uses, and goes back into SMLib valid at
the same volume; a blind hole tangent to a side leaves the side a crack; through SMLib both come
back as one solid with verified history and empty validity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
After a parameter edit, a feature's stored face point is moved onto the
face its name finds. That used the face's nearest point to the old pick,
which lies on the face's boundary whenever the pick no longer lies on the
face: a shell opened at the end of a bar picked (150,15,10), on the edge
it shares with the bottom face, so truck opened the wrong face and SMLib
refused the pick (#2531).
FaceHit::pick_point keeps the point where it still lies on the face clear
of its edges, and otherwise takes the face's nearest point among points at
least a margin inside its triangles (a few pick tolerances, capped at half
the roomiest triangle's inradius on narrow faces).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
With SOLIDCRAFT_KERNEL=smlib, kernel::draft hands the picked faces (a point inside each,
from the face under each pick) to Brep::draft, with SMLib's own history: the tilted faces are
the faces themselves (Modified). Faces SMLib can't draft (a round wall, a face not parallel to
the pull) are "not supported yet", never truck. Tested in modify_history_tests: a 40 x 30 x 20
block's side at 5 degrees removes 20 x 20 tan 5 deg / 2 x 30, with its history checked against
the geometry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Flat faces tilted by an angle about the line where each meets a neutral plane, leaning in along
the pull, and the faces around them extended or trimmed to meet them
(tangency-labs/smlib@e1ac3c3, SMLib's local operation). A face that isn't flat or isn't parallel
to the pull is Error::Unsupported. The test drafts a cube side by +-5 degrees, removing or adding
the analytic wedge to 1e-6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cargo xtask kernel-cases --kernel both, solidcraft-cli built with smlib
at c65fc91a (fork bf7c272), SMLib through SOLIDCRAFT_KERNEL=smlib. 372
cases: default 235 pass, smlib 269. SMLib's gains are booleans (50 to
66), fillets (32 to 41), shell (5 to 11) and STEP import (13 to 17, its
solids now SMLib bodies); its losses are sweeps (54 to 51 pass, 15 not
supported yet on SMLib) and threads (17 fail, of which the default kernel
hangs on 4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A face on a B-spline surface whose first and last control columns are the
same (closed around u), bounded by loops that go once around without a seam
edge, was refused ("a loop across a closed spline surface's seam"). It now
gets a seam as cylinders and cones do, along the surface's own boundary
column (u at the start of its knots, an exact iso-curve of the net), each
loop split where it crosses that column when it has no vertex on it; the
period is the knots' width. At a pole a loop may jump across u, where the
seam's two uses meet a period apart.
The step-conformance NURBS cylinder, dome and torus ring now read. With
SOLIDCRAFT_KERNEL=smlib all 17 parts import into SMLib with their exact
volumes and face counts. A spline closed in v only, or both ways, is still
refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A gap the boolean fixed on one side only left the freed edge in its common-edge map, and the next
pass read it: a square-wire coil cut into the wall of a bore crashed the process on SMLib
(sweeps/2552). It now fails with an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SMLib leaves a corner patch where blends meet untagged, and blends() matches no single edge to
it, so it had no origin, and the edges between two blend faces (two chamfers' mitre) none
either. The history checks refused both (modify_history_tests' corner, whole-box and mitre
cases on SMLib). Now a patch without origin is made in place of all the edges of the blend
faces beside it, as truck records a corner, and every edge of a face new to the blend is where
its two faces meet.
modify_history_tests with SOLIDCRAFT_KERNEL=smlib: the three blend cases pass (28 of 30; the
painted shell and the painted box cut's pieces are the shell's and boolean's). Truck: 30 of 30.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With SOLIDCRAFT_KERNEL=smlib, chamfer_sides (two distances, or a distance and an angle) runs
SMLib's new unequal chamfer instead of being refused: `d1` back along the face facing up most
(down most with `flip`, as truck's chamfer_sides picks its first face), the other side by the
distance or angle, with the body's history as for the other blends. A radius law SMLib refuses
as having a corner is "not supported yet", never truck's round.
Tests (with the feature): 5 and 2 on a cube edge either way round (the top face keeps 15 or 18
of its 20, the chamfer face comes from the edge), 5 at 45 degrees, and the corner law refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brep::chamfer_sides reaches the fork's smlib_chamfer_sides: `distance` back along the face
whose outward normal leans most along a given direction, the other side by a second distance
(ChamferSecond::Distance) or at an angle to the first face (ChamferSecond::Angle). The fork's
smlib_fillet_variable refuses a radius law with a corner with SMLIB_ERR_UNSUPPORTED, which the
crate reads as Error::Unsupported ("not supported yet"): SMLib's section fit ran smoothly
across the corner and missed the law (4 -> 2 -> 4 on a 20 mm edge took 35.1 mm3 for 40.1;
tangency-labs/solidcore#430). Both are in the fork at the pinned bf7c272.
Tests (ops_tests.rs), against analytic values: 5 and 2 either way round (the centroid tells
which face took the 5), 5 at 30 degrees, rejected arguments; a quadratic law 4, 1, 4 and a law
down to zero (1e-5), the corner law refused and the same points joined C1 built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With SOLIDCRAFT_KERNEL=smlib, kernel::measure and measure_mass take volume, area and centroid
from SMLib's Brep::mass_properties (integrated on the exact surfaces, accuracy 1e-8). The mesh
measure is still made, as an independent check. Where SMLib disagrees with it beyond the mesh's
own accuracy (1e-3; tangency-labs/solidcore#384), or can't take the body, the mesh measure
stands and the measurement says why. BodyMeasure and MassMeasure carry `measured_by` (smlib |
mesh) and `notes`, and inspect.measure shows both. The box and topology counts stay the mesh's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SMLib's tolerance update now fixes the vertex gaps its check reports (gaps over half a vertex's
tolerance), and every boolean, blend, shell, offset and sweep result grows its vertex and edge
tolerances over gaps below SMLib's own rounding bound before it is handed out. A thread's groove
cut from a shaft no longer fails SMLib's check on vertex gaps of about 1e-5 mm, and an offset
sphere face no longer fails it on an edge gap of 5.015e-6 against 5e-6 (shell/2939).
Also carries the fork's blend commits since ca2559f (second offset per edge, two-setback
chamfers, variable radius laws with a corner refused) and the IGES knot check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where two edge uses of a loop meet at a pole (meridians of a sphere), the periodic parameter's
jump there is free, and the pcurves took whichever jump made the loop counter-clockwise. Two
meridians a three-quarter turn apart bound a quarter of the sphere or three quarters of it, and
the loop alone can't tell which: a half disc revolved 270 degrees on SMLib came back as a quarter
ball turned inside out (sweeps/2667, V = -226 against 678.6).
pcurve::pcurves_within takes the face's parameter box, as SMLib gives it, and picks the jumps
whose extent in that parameter is nearest the box's; pcurves is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each solid item of a STEP file goes through the schema 4 reader
(solidcraft_step::topology, at the file's units and uncertainty) into
SMLib through the flat converter, healed where SMLib's check refuses it,
and comes back as a kernel body; its faces keep the file faces they came
from. A solid the reader or SMLib refuses is not imported, with the
reason: there is no truck fallback on this path. truck's own reader is
unchanged and stays the default.
Test: the 17 step-conformance parts read into SMLib with their exact
volumes and face counts, every face traced to an ADVANCED_FACE; the three
NURBS parts stay refused by the reader (a loop across a closed spline
surface's seam), as on schema 4 today.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
smlib_record_ordered is smlib_body_ordered for a record of any origin (a
STEP file's, not only a body's): SMLib's body of it, healed where SMLib's
check refuses it as given, with where each of SMLib's faces and edges is in
the record's order. A record SMLib refuses outright now goes through the
healer too before the import gives up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schema 4 lets a circle or ellipse edge run up to one turn past its seam
(a quarter arc after the seam is [2π, 2.5π]); SMLib's circles take angles
within [-2π, 2π], and the flat import refused the whole body as invalid
input. The mapping now gives SMLib the same arc a whole turn back, and moves
the edge's pcurves on B-spline faces by the same amount, since their
parameter is the edge's. step-conformance's plate_100_holes now imports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Solid::face_origin lists the ADVANCED_FACE or FACE_SURFACE entity of each
of the record's faces, in the record's face order, so a body made from the
record (in another kernel) can trace its faces to the file, as the truck
import's face_origin does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Imprint cuts each face of a body where a tool body meets it (the fork's
smlib_imprint, SMLib's imprinting boolean) and gives None where the tool
crosses no face. Trim builds on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
169 rig and ladder cases on both kernels, graded against each case's own answer: where SMLib
fails, grouped by what goes wrong (wrong geometry, a crash, the thread groove's boolean failing
SMLib's check, shell refusals, sweeps with no SMLib path yet).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SolidCraft's own record of a body SMLib made (the rig's record check, the saved body, the next
SMLib import) re-fitted every pcurve at the kernel's working tolerance, 1e-5 mm. SMLib's fillet
and chamfer surfaces lie some 2e-5 to 2e-4 mm off their faces, so 15 fillet and chamfer cases
failed that check although SMLib's own passed. And SMLib reported one tolerance for a whole body,
the largest any part of it needed, so one loose edge (a pocket cut left one at 0.23 mm) made the
whole body loose on its way back into SMLib.
Now the tolerances go per edge and per vertex, both ways:
- solidcraft-smlib reads and writes the fork's new SMLIB_FLAT_EDGE_TOLERANCE and
VERTEX_TOLERANCE arrays. Export gives each edge and vertex record its own (never below the
floor, which becomes the body's). Where those understate what an approximated edge needs on
its face (SMLib holds a blend's end curve 2.3e-5 mm off its plane at 2e-5), the export falls
back to SMLib's largest everywhere, as before. Import writes each record edge's and vertex's
tolerance.
- `geom::brep::pcurve::EdgeUse` carries its edge record's tolerance, and a pcurve is fitted to
it where it is larger than the face's.
- The kernel's decoded body keeps its record's edge and vertex tolerances
(`Body::tolerances`, in its edge and vertex order) after an SMLib operation and a saved body's
decode, and keeps them through a rigid placement. Its record writes them again; anything that
builds a new solid drops them.
SMLIB_REV moves to the fork commit that adds the arrays (smlib-blends-abi2 4a67471, ABI 2).
Rig and ladder fillet and chamfer cases on SMLib: 43 pass before, 60 now, none lost. Tests: a
pcurve fitted to its use's own tolerance, and the ladder's 08/006 cylinder rim chamfer whose
record, saved body and placed copy all stay valid and SMLib's check takes back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
A pick at the vertex where a cylinder's seam meets its rim lies on both edges. The body's
nearest-edge order took the seam on SMLib-made bodies, and SMLib refused to chamfer it
(the ladder's part chamfer, step 22, SM_ERR_INVALID_INPUT). A seam or a tangent join is never a
blend's edge, so a pick as near to one as to a sharp edge takes the sharp edge; anywhere else the
nearest edge stands.
Test: every pick round a cylinder's top rim, the seam's end among them, takes the rim.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Every blend entry point asks `backend()` and, on SMLib, hands its arguments to blend_smlib.rs:
fillet, fillet_each and fillet_radii (a radius per edge), fillet_g2 (SMLib's G2 blend-curve
section between the round's contacts), fillet_chord (constant-width section), chamfer, and
fillet_variable_spline (the radius law as an SMLib B-spline law; fillet_variable and
fillet_variable_points reach it, and fillet_max, fillet_max_each and fillet_tapered run on these).
Options SMLib has no construction for are refused as "not supported yet", never built by truck:
G2 corner patches, and chamfers with two different setbacks (distance and distance, or distance
and angle; equal setbacks are the equal-distance chamfer).
Each pick takes the body's nearest edge in its own edge order, as truck's blends do, and SMLib is
given that edge's midpoint, so a pick at a vertex never leaves SMLib to choose between edges. The
body goes in tagged as operand 0, SMLib's check must pass, and the decoded result carries SMLib's
history in the body's orders, the blend faces given FaceOrigin::Edge of the edge each replaced
(the picked edges and the rest of their tangent chains). The truck path is unchanged.
Tests (with the feature): a round's volume and its face's origin, a size per edge for rounds and
chamfers, a linear law from either end, refused sizes and picks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The Rust side of the fork's smlib_blend and smlib_fillet_variable, which the kernel's fillets and
chamfers need on SMLib: `Brep::blend` takes a size per picked edge and a `Blend` (circular round,
curvature-continuous round between the round's contacts, constant-chord round, equal-distance
chamfer); `Brep::fillet_variable` rounds one edge with a clamped B-spline radius law from the end
nearest a start point. Selection errors map as for the other picking operations.
SMLIB_REV moves to the fork commit that adds them (smlib-blends, on capi-solidcraft ffcf56d).
Tests: per-edge rounds and chamfers and the chord round against analytic volumes, the linear law
from either end, and refused laws and picks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The fork's booleans now hand out closed non-manifold results (SMLIB_REV e87e9b3, ABI 3): solids
touching each other along an edge or at a vertex, which the C API refused as "not a closed
solid". The SMLib → schema 4 mapping now gives each closed boundary its own record shell: the
faces sharing one runtime shell on the empty side and one on the material side, so solids that
share their empty side's runtime shell get a shell each. An edge or vertex faces of more than
one shell use is split into one copy per shell (same curve, interval and point), so every
shell is a closed manifold boundary; regions list each shell as before. The history follows: an
Order's `pieces` give each record edge of a split SMLib edge with the faces it bounds, and each
piece keeps the edge's origins.
SMLib's own check refuses the radial order of faces round an edge solids share, so a
non-manifold result is checked as the separate solids it becomes (imported as given). A solid
touching itself along an edge (one shell using an edge more than twice) is still refused, now
as "not supported yet: a solid touching itself along an edge".
Tests: cubes sharing an edge or a vertex unite into two shells of 1000 mm³ each, every face and
edge's history verified against the geometry; two opposite quarters cut off a slab leave two
solids. Of the pinch tests run through SMLib, cut_leaving_pieces_meeting_along_a_line now
passes; the other nine are solids touching themselves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The kernel runs exact helical sweeps and never resizes; the check that
refuses a thread off its hole's or shaft's size is the engine's sugar
(AGENTS.md). So the seven cases (995, 1443, 2152, 2969, 3366, 2369,
2705) go back to expecting that refusal and its message, as 1375e905 had
them, reversing 1588dd70.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On SOLIDCRAFT_KERNEL=smlib the thread's groove (the ISO 68-1 trapezoid, as before) is a B-rep
screw sweep (kernel::sweep_helix, SMLib's screw sweep) cut from the body by the B-rep boolean,
whose history carries the threaded face's names on. Truck keeps the faceted groove and the
faceted boolean. thread_groove_shape holds what both build from; the size checks before it
(a hole or shaft off the thread's size is refused with the size to use) are unchanged, so
neither kernel fits a thread to a face that is too large or too small.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
With SOLIDCRAFT_KERNEL=smlib, extrude (and extrude_whole_sides, so boxes and cylinders too),
revolve and revolve_whole, extrude_tapered(_between), sweep and sweep_with, loft,
loft_with_rails, loft_regions and sweep_helix (coils, and the threads' grooves) build their
bodies with the fork's sweeps (smlib_sweeps.rs): the profile's segments go to SMLib as curves
(lines, arcs, Béziers and conics as NURBS), the result comes back through its schema 4 record,
and one SMLib's own check refuses is an error. What SMLib has no sweep for answers "not
supported yet", never truck's sweep: twists, end scales, guide rails and orientation
directions; paths with corners and paths of points; lofts with holes or to a point; draft
extrusions of profiles with holes or with a straight foot.
History is in the builders' vocabulary, found from the result's geometry as provenance.rs does
for operations without exact history: a wall comes from the segments its start edge runs along
(and, for extrusions and revolutions, whose middles it holds half-way), the face in the start
plane where the profile bounds it is the start cap, the one at the end the end cap. Straight
extrusions also carry their faces' construction sources (ExtrudeCap, ExtrudeSide), which the
box and cylinder certificates need. The loft's section matching is truck's own, now
loft_numbered_loops (split out of loft_numbered_wires, unchanged), so both kernels pair the
same vertices.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Brep::extrude (straight or drafted), Brep::revolve, Brep::sweep (turning with the path or only
moving along it), Brep::loft and Brep::screw_sweep wrap the fork's new smlib_sweeps.cpp shims.
Profiles, paths and sections are lists of sweeps::Curve (lines, circular arcs, NURBS), turned
into the C API's curve lists here; the FFI is ffi/sweeps.rs. SMLIB_REV moves to the fork head
that has the shims (c33df0f, on capi-solidcraft 1499681).
Tests check each sweep against closed-form volumes and SMLib's check, and hostile curve lists.
(The crate's test build currently stops at import/tests.rs, which calls import::surface without
its new analytic_planes argument; that is the import's, not this change's.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The SMLib shell passed the user's pick points to SMLib, which refuses a point as near to two
faces; a pick on a hole's wall at the line between its two halves (kernel-cases shell/4010)
failed. The open faces are now picked as truck's shell picks them (the face of the nearest
triangle), grown by the faces on the same surface joined to them (offset::with_same_surface:
a round wall stored in pieces opens whole), and SMLib gets one point inside each.
Offset faces keeps its design on both kernels: when moving the faces fails on a body with
split lines, the chosen pieces are pressed (swept and joined or cut) instead, as truck's path
always did; the booleans of the press run on the selected kernel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
smlib_body_ordered keeps SMLib's face and edge order for history only where SMLib's export
numbers the body as its record does, and compared each loop edge for edge in the same order
and direction. SMLib walks a face whose surface it turned (a hole's wall, a concave cylinder)
the other way round and may start the loop elsewhere, so a plate with a hole lost its history
through every SMLib boolean and shell although its faces and edges are numbered alike. A loop
now matches when it walks the same edges in the same cyclic order, or in the opposite order
with every edge reversed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
With SOLIDCRAFT_KERNEL=smlib (the one kernel switch), shell, offset_faces and thicken send
the body to SMLib tagged as operand 0 (offset_backend.rs) and bring the result back with
SMLib's own history, as booleans do. A result SMLib's check refuses is an error.
SMLib's tags travel through the copies its offset makes, so a shell's inner wall and a
thickened sheet's moved copy read as pieces (Split) of the face they came from. A result face
lying more than half the operation's distance from its tagged face is recorded as that face's
offset (FaceOrigin::Offset) and its edges as where its faces meet; a thickened sheet's walls,
new to SMLib, are recorded as made in place of the sheet edge their edges are tagged with, as
truck's thicken records them. Offset faces pick the faces truck's path picks (a round wall's
pieces together) and pass SMLib one point inside each; the moved faces are Modified.
The histories are checked against the geometry as truck's are (modify_history_tests), with
the analytic volumes, the thicken direction, and the refusal of walls past the L-shaped body's
inradius (8.79) that its box (half extent 10) let through before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Brep::offset_faces and Brep::thicken reach the fork's new smlib_offset_faces and
smlib_thicken; SMLIB_REV moves to the fork commit that adds them. The shim now refuses an
inward wall that leaves no room inside the body with its own status, Error::NoRoom, instead of
an argument error, so callers can say what is wrong; the test that a wall thicker than the box
fails now expects exactly that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The analytic mapping refused every revolution that is none of SMLib's plane, cylinder, cone,
sphere or torus (a profile of any other curve, a placement that is no rigid motion), which
now also kept those faces from kernel-smlib's exact rational B-spline import (da7ccba9). Such a
face now stays a revolution, so the import sends it as its B-spline; the analytic surfaces are
still taken wherever they fit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
All operations move to SMLib at once, so the selector is no longer the booleans' own:
`SOLIDCRAFT_KERNEL=smlib|truck` (read once, unset is truck, the same refusals for an unknown
value or SMLib in a build without it) is answered by `solidcraft_kernel::backend()`
(crates/kernel/src/backend.rs), which every operation's dispatch calls. The boolean's SMLib path
moves to smlib_boolean.rs. The regression rig's kernel check (`boolean_kernel`) now answers from
the switch, and the rig and CLI set SOLIDCRAFT_KERNEL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The record of a truck body is built to the body's working tolerance (1e-6 of its size, at
least 1e-5 mm), and truck's offsets and blends leave some edges a few times farther than that
from a face they bound: no pcurve fits, so the record and every SMLib boolean on the body
failed ("pcurve of loop 0 use 0 ...: the curve at 0 lies 3.2e-5 mm off the surface" in a
shelled revolve of the robustness generator). The record for SMLib now retries at ten and a
hundred times the working tolerance for the body, its edges and vertices before refusing; the
saved native body keeps the working tolerance. Robustness seed 64 (a filleted extrusion
joined to a cylinder), refused this way before, now builds as truck does to 1.6e-6 of its
volume.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
An SMLib cylinder, cone, sphere or torus decoded as a truck revolution with its parameters
swapped (angle, profile), which truck's own revolutions never have. The tessellator then put a
cone's apex at the wrong angle: a drill point through SMLib and back meshed 41 % too large and
oracle cases 18, 55 and 56 measured wrong areas and volumes. The profile now runs against the
record's v, unswapped, which keeps the record's natural normal and truck's (profile, angle).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Truck's bodies are trusted into SMLib as given, but some miss SMLib's rules by a little (an
edge's vertex 4.4e-5 mm off it against a tolerance of 3.3e-5 after a truck operation), and
every boolean on them was refused by the check of its result. An operand whose trusted import
fails SMLib's check is imported again through SMLib's healer, and the healed body is used only
where SMLib then finds it valid; its numbering is checked against the record as before, so a
heal that renumbers leaves the result without history rather than with a wrong one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
A revolve's flat ends (a line square to the axis turned about it) were refused, so any
revolved profile off the axis could not reach SMLib (oracle case 29). Such a face now goes as
SMLib's plane square to the axis, its range the box round the sector the line sweeps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
kernel-smlib already gives solidcraft-engine and solidcraft-cli an `smlib` feature (for the
regression rig); the note on SOLIDCRAFT_BOOLEANS now says they are how the oracle, the naming
corpus and the robustness goldens run their booleans through SMLib.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The export takes a closed boundary's faces from the side in the empty region and skipped
every use there that is not `same`, a rule meant only for a sheet (a shell that is its own
mate). It tested the chosen side against the mate instead of the shell against itself, so a
cavity's shell, taken from the void's side, lost its faces whose uses run opposite to their
surfaces: "a face no shell uses" for every cut that leaves a void.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Truck extrudes a circle into rational splines, so every truck cylinder reached SMLib as
splines, and a box cut by one came back with edges 0.78 mm off their faces (SMLib's own check
refused it). A rational spline that is a circular arc swept straight along its axis (one
direction linear, the second row the first moved by one vector square to the arc's circle,
weights alike) now goes as SMLib's cylinder with the face's range on it.
The revolution mapping becomes one module for both (import/analytic.rs): each candidate face's
analytic surface is checked against the record's at 25 points of its domain, each point placed
on the analytic surface by its own (u, v), and turned over where the normals disagree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Truck's planes run through three points, their axes the face's sides, so the SMLib import sent
every truck planar face as a bilinear NURBS patch: SMLib then intersected splines where it has
planes. A plane on any axes now goes as SMLib's plane on unit axes along U and at right angles
in the plane, its range the record's domain on those axes (a face's range need not be tight;
SMLib rebuilds an analytic face's trims from its edges).
A revolution's analytic frame turns to the face's first angle so u starts at 0: SMLib's check
refuses a cylinder whose range ends past 360°. A failed SMLib check of a boolean's result now
says whether an operand already fails it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Truck holds every round surface as a placed revolution, and a body SMLib made comes back from
truck that way too, so the SMLib import refused spheres, tori and any second boolean on a
first one's result. The import now turns each revolution of a line along or through the axis,
or of a circle centred on it or in a plane through it, under a rigid placement, into SMLib's
analytic surface: the face's range mapped from the revolution's parameters (angles moved into
the first turn: SMLib's torus breaks on a tube range below zero), checked against the
revolution at 25 points, and the face turned over (loops reversed, shell uses flipped) where
the analytic normal runs the other way. Other revolutions are still refused.
A result body's order is checked by each face's boundary edges as well as its identity:
Body::new rephases revolved faces onto new faces with the same edges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
With the `smlib` feature, SOLIDCRAFT_BOOLEANS=smlib (read once per process, default truck)
sends every kernel::boolean through SMLib: each operand's schema 4 record imported and tagged
as operand 0 or 1, SmBrep's boolean, SMLib's check at every level, the result exported and
decoded (with its geometry check). A result either check refuses is an error. The setting
naming SMLib in a build without it, or anything unknown, fails every boolean instead of
quietly running truck, so a comparison never compares truck with itself.
The history is SMLib's tags renumbered into Body::history's index spaces. The export now
reports where each SMLib face and edge lands in canonical order (its arrays follow
SmBrep::GetFaces/GetEdges, the order the tags use). The result's order is checked against the
decoded solid by face and edge identity, the operands' by re-exporting the imported body and
comparing its numbering with the record's; where either differs the result has no history
rather than a wrong one. The result gets the operands' regularization protection and planar
roles as truck's does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The owner's rule for the kernel-smlib branch: the kernel runs exact
operations and refuses what it cannot do exactly; sizes, standards tables,
defaults, pre-checks and conveniences live in the engine and doc. Threads
as the example: a helical sweep in the kernel, while the size table, the
off-size refusal and multi-start threads are sugar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Only the Delaunay layout checked its triangles against the surface. The
grid divides each parameter direction within the tolerance, but on a
doubly curved surface the two sags add up in a cell's middle, along its
diagonal (SMLib torus 1.16x at 0.01, truck sphere 1.23x at 0.1, truck torus
1.40x at 0.01); the patches round a pocket cut into a sphere at its pole
fan out from the cut with no check at all (4.34x at 0.1, 1.90x at 0.01).
Every curved face's finished triangulation now goes through tess::sag:
each triangle is measured at its centroid and at the middle of each side
inside the face, across the surface (along the normal at the point of the
same parameters, which near a pole need not be the nearest), and the side
that leaves the surface most is split, both triangles on it together, so
the face stays conforming. Boundary sides are never split: they carry the
edges' own nodes, shared with the neighbouring face. At a pole the free
parameter of a midpoint comes from the other corners.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
A sphere from SMLib cut through its centre keeps one face bounded by its
rim and by its seam from the pole up to the rim, used both ways. Unwrapped
along the loop, both runs of the seam landed on one parameter line and the
loop came out one period short, so the planner took it for a face winding
round the sphere and cut it along a new line of its own. The two seam runs
stayed inside that strip as a slit of zero width, triangulated on each side
separately: open edges along the seam up to the rim (gaps up to 9.5e-3 mm
at chord 0.1), a zero-angle triangle and a non-manifold edge at the pole.
Such a loop is now unwrapped from the node after the pole round to it and
started at the node before it: the seam's uses lie a period apart on either
side of the face's parameter box, which then meshes like any sphere patch,
both uses on the seam's own nodes. It is kept as it was unless the opened
loop spans at most a period with a positive area and no longer winds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Eight are hand-written synthetic files (MIT OR Apache-2.0). The other eight are byte copies of
CC0 files from tests/step-conformance and say so. Each row names the issue its case comes from.
cargo xtask assets passes again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SMLIB_REV moves to tangency-labs/smlib capi-solidcraft 1499681, eight SMLib
commits for surface/surface intersection: an empty curve is no longer
reverse-traced (SM_ERR_NULL_POINTER), tangent curves are traced once, curves
split at singular crossings, interior tangential contact points, tangential
curves and branches through tangent nodes, small loops above tolerance, and
high-order contact curves covered across their band. The corpus test's pass
list grows from 81 to 111 cases; the 8 still failing are all below SMLib's
1e-5 zone tolerance (angles 1e-6..1e-8, offsets 1e-7, loops of 2e-6 or less).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the native feature, smlib_global_intersect_at_1e_6 builds both
surfaces of every case through solidcraft_smlib::ssi, intersects them
at approximation tolerance 1e-6 with the corpus runner's 4001 samples
per curve, turns the answer into what the runner writes for gsi, and
scores it. It asserts the exact set of passing cases (81, as score.py
grades the runner: 81 PASS, 25 INCOMPLETE, 11 WRONG TOPOLOGY, 2 ERROR),
so a regression and an improvement both fail until the list changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ffi/ssi.rs declares the C API's SSI block; ssi.rs wraps it: Surface
(plane, cylinder, cone, sphere, torus, NURBS, with the SSI corpus's
fields, angles in degrees), Method (Global, Advanced, Basic) and
intersect(), which returns every curve's type, UV flags, deviation,
interval and samples (xyz, uv on each surface, each surface at its uv)
and the isolated points. Without the native feature every call returns
NotBuilt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ssi_score::score_case grades an engine result (sampled curves with xyz,
uv1, uv2, both surfaces at the UV samples and the curve type; points;
status; or a crash) against a corpus case: CRASH, ERROR, INCOMPLETE,
WRONG TOPOLOGY, INACCURATE or PASS, with score.py's flags and numbers.
ssi_corpus.rs checks every case parses, every NURBS surface lies on its
truth, the truth itself passes (bar unsplit singular points), and the
verdict rules. The ignored parity test scores the corpus runner's raw
outputs and compares with score.py: 119/119 identical verdicts and
flags for SMLib GlobalSurfaceIntersect at 1e-6 (81 PASS).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test-support port of the parts of the SSI corpus's ssi_geom.py and
score.py the scorer needs: exact distances to the implicit truths, the
Gauss-Newton closest point on traced param patches, B-spline/NURBS
evaluation of the case surfaces, and score.py's segment-hash polyline
set, polyline distance, components, free ends and topology signature,
following numpy's arithmetic order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 119 surface/surface intersection cases of jyviko/ssi-corpus (b04c8a6,
MIT OR Apache-2.0), gzip-compressed, with its licence texts and a README
on the case format and the scoring rules. The 25 y23 cases come from
Yang, Jia & Yan 2023 (ACM TOG 42(6) Art. 211, CC-BY 4.0); both are in
ATTRIBUTION.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PlacedRevolution record goes to SMLib as its exact rational B-spline
surface: each profile control point swept round the axis by a full circle of
four rational quadratic quarters starting at the face's first angle, then
mapped by the placement. The circle's parameter is not the angle, so the
face's angle range is converted pointwise and its pcurves are not carried
(SMLib rebuilds the trims). Where the placement mirrors, the record's natural
normal is the opposite of the map's, so the directions swap.
The B-spline matches the record's surface to 1e-12 with the same normal,
forward or not, mirrored or not; truck's sphere and torus now reach SMLib
with their analytic volume (1e-6) as manifold solids.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
784, 1753 and the two 1666 lofts came from holes in exported meshes, so
each now also requires a mesh with no open or over-shared edge. On today's
kernel 784 and both lofts are watertight; 1753's bowl, its rim cut by a
cylinder, still has 3 open mesh edges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two faults made simple curved solids look open (#818's blind hole: 80 open edges, a stadium
slot: 52):
- It meshed with `refine`, the measuring mesh, which splits triangles across curved faces on
their own and leaves T-junctions on the edges they share with neighbours. It now checks the
display mesh, whose faces share each edge's points.
- It matched vertices by rounding to a 1e-6 mm grid, which splits points a rounding error apart
across a cell boundary. It now joins vertices closer than `tolerance.weld_mm` (default 1e-4 mm).
Both cases now have 0 open edges, also with the weld at 1e-6 mm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2822, 3952 and 4149 cut long modelled threads into filleted or revolved
bodies; each takes minutes on today's kernel, so they get timeout_s 600
instead of the default 180.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For the history areas' cases (threads, tessellation):
- A count may be an inclusive range `[min, max]`, where the geometry only bounds it (a modelled
thread's faces).
- `"watertight": true` meshes each body (chord tolerance `tolerance.mesh_mm`, default 0.01 mm)
and requires no open and no over-shared edges, with vertices matched by position.
- `"timeout_s"` gives a case longer than the run's timeout, at most 30 min.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
372 cases. The default kernel passes 232. smlib reports "not supported yet" for every case
until the SMLib boolean switch lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- tests/kernel-cases/sources.json gives each looked-at number's kind: an open or closed issue, a
merged PR, one of the frozen truck-geometry PRs, a kernel-paused PR. The report's coverage by
source counts how many of each were looked at, became cases or were skipped.
- crashes/2357: one join of a ring of 90 overlapping cylinder segments. The right answer is one
closed ring body. The old kernel still panics in its intersection curves and runs past the
timeout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2969's description named another product's behaviour; 1443's and 2152's
still described the refusal. Each now says the hole is drawn at the
thread's nominal size and gets the fitted thread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reverts 1375e905. A modelled thread fits by default: a hole too wide for it
is filled to the tap drill, a shaft too thick is turned down to the major
diameter, so the seven cases (995, 1443, 2152, 2969, 3366, 2369, 2705) go
back to expecting the fitted thread and its volume.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A modelled thread never changes the size of the hole or shaft it is on
(#1018, #2853): one that isn't the thread's size is refused, naming the
size to use. The seven cases that expected the hole filled to the tap
drill or the shaft turned down to the major diameter (995, 1443, 2152,
2969, 3366, 2369, 2705) now expect that refusal, with its numbers worked
out from ISO 68-1 and the message in `refusal`. An opt-in fit option, if
it comes, gets cases of its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runs are compared with the golden's times, so a run sharing the machine between cases flags
slowdowns that are only contention (seen with --jobs 4: four cases two to ten times slower).
--jobs still overrides it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Tiers: primitives; booleans in the degeneracy classes (coincident and coplanar faces, edge-on-edge,
vertex-on-face, tangent along a line and a circle, near-tangent gaps and overlaps of 1e-3 and
1e-6 mm, seam and pole crossings) and cuts that end in a single line (a knife-edge wedge, a V
groove and a cylinder that pinch the part in two, a hole tangent to a face, a notch flush with an
end); line-and-arc extrudes and revolves (partial, touching the axis); spline profiles, sweeps
and lofts; holes and tapped holes (blind, drilled, counterbore, countersink, M5-M8 taps, into a
curved face, near and tangent to an edge, crossing); modelled threads (external, fine, partial,
to a shoulder, internal, into a chamfer, at a filleted shoulder); fillets (constant, tangent
chains, convex and concave, variable down to 1e-3 and to 0 at a vertex, corners with equal and
unequal radii, g2 corners); chamfers (two distances, angle, three- and two-edge corners, curved);
shells and offsets; patterns and mirrors (overlapping instances that cut a core free); and
cumulative parts.
A running part grows one tier at a time through `base` (back wall, revolved boss and slot,
spline window, tapped holes, rounds, chamfers, shell, vents), and the last case adds its thread:
it carries every tier. Answers are analytic wherever the geometry has them (Pappus, Steinmetz,
ISO 68-1 basic thread profiles, exact Gauss quadrature of Bezier profiles, inclusion-exclusion
for chamfered corners), otherwise invariants; each `method` gives the derivation, and
tools/kernel-ladder/cases.py (standard-library Python, like tools/oracle-analytic) writes them.
Capabilities SolidCraft lacks are `missing` cases: multi-start threads, thread runout lengths,
fillet corner setbacks.
The golden is the first recording on truck, each tier sorted by its time. SMLib booleans are
not on this branch yet, so the smlib column records "not supported yet" for every case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The ladder orders kernel operations by complexity instead of by random goldens: a manifest
(tests/kernel-cases/ladder/ladder.json) lists tiers in order and each tier's cases in order;
every case is graded against its own answer like any rig case, and a golden
(tests/kernel-cases/ladder/golden.json) records per case and kernel the status, the commands'
time and what the kernel made. A run compares with the golden and flags slowdowns (over twice the
time and 0.25 s more), status changes and changed counts. `--record REASON` re-measures one case
at a time, rewrites the golden with its reason and sorts each tier by the first kernel's time.
The report, docs/kernel-ladder.md, gives per tier and kernel the counts per status and the
frontier: the first case in ladder order the kernel fails. It reuses the rig's process-per-case
runner, timeout and cells; results now carry their path under tests/kernel-cases/ so cases from
other areas can be listed in a tier. The README documents `base`, `missing` and the ladder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
What the complexity ladder needs from the rig's case runner, kept in the one runner:
- A script may name a `base` case directory: that case's commands (and its own base's, up to
32 deep) run first, so a ladder case starts from the previous tier's finished part without
copying its commands. `$CASE` is each recipe's own directory; `$OUT` is the running case's
scratch directory, in its bases too.
- `missing` in expected.json reports the case as not supported yet without running it: a
capability with no command or parameter (multi-start threads, fillet setbacks) stays visible
instead of being faked with a stand-in.
- Each result carries `seconds` (the commands' own time, without process start or grading) and
`measured` (body count, face/edge/vertex counts, volume, area), for the ladder's golden.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
A revolved cone tip with an exact volume and area; auto-reports without
a design are listed in the skipped.json files.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A tray with seven rounded edges, both rim rounds of a tube, a chamfer on
a joined L-shaped body, and a circular pattern of a chamfered hole, each
with an analytic volume.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An M8x1 thread in a 7 mm hole, and three booleans of finely faceted
prisms (1000 and 1200 flat faces) with planar and curved partners, each
with a closed-form volume. Three auto-reports without geometry are
listed in skipped.json.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`"outcome": "solid_or_refused"` takes a clean refusal, or else grades the result as usual. Two
bodies that touch along a circle only (#168, #3841) have no one-solid union: a refusal or the two
bodies left apart are both right; fusing them is wrong. The two cases said opposite things
(two bodies, a refusal) and now agree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `"shells": N` in expected.json grades the total number of shells: a body with a sealed void
holds two, and some joins keep several lumps in one body. Those cases (#3175, #3455, #3526,
#3730) are graded on validity again instead of being marked not valid.
- When SMLib's record conversion says "not supported yet" (revolved surfaces, traced intersection
curves), its check gives no verdict. That's shown but not graded, since it isn't a fault of the
body. solidcraft_kernel::SmlibCheck carries the three outcomes.
- `$OUT` in a recipe is a scratch directory outside the repository, for files a case writes. The
two STEP-export crash cases write there instead of /tmp.
- cargo xtask kernel-cases --results FILE keeps every result as JSON.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every other sweep, loft, revolve, pipe, coil and rib issue and PR, one line each: auto-filed reports that carry only command parameters, reports cut off in the source, UI and dialog items, feature requests, and failures that belong to import, shell, fillets, booleans or tessellation.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Coils split and trimmed by a plane, cut with a negative pitch, joined flush round a cylinder, intersected with a cylinder, cut as a thread groove into a bore, and placed with a square wire outside the diameter (screw-motion Pappus); surface revolve, sweep and loft sheets with areas worked out by integration, and a rail-guided surface loft in either section order.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
End faces of a loft, a sweep and a coil extruded, closed twisted sweeps round a torus, a twisted square cut through a tube, a ring swept along a short arc and joined to a collar, two overlapping swept arc walls, a rib grown along the sketch normal, and three pipes meeting at a point.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
End scale, parallel orientation and guide rails on straight paths (frustums and a sheared cylinder), a rectangle swept round a mitred corner, a profile mid-line on a closed rounded rectangle, second sweeps from a plane along a closed circle, hollow and cutting pipes, and a flat wall of a swept corner extruded.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Refusals for a profile across the axis and an axis outside the sketch plane, a construction axis picked by name, a cone left by a revolve cut, revolve joins and cuts whose end faces lie in a face of the target, a dome cut by a ball on its seam, a horn-torus turn, and half-disc revolves with a refused hostile sweep scale.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Spherical shell sketched off the origin, adjacent profiles on the axis, whole planar faces of full and partial turns and a face pick on a fresh revolve, a half slot turned about its centreline (alone and as a cut), a torus cut by an overlapping ring, and a 300 degree revolve crossed by a plate.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Added: 451 (block inside / crossing a hollow spherical-shell tool), 1107 (hollow hub joined
with nine blades), 1140 (spiral hollow lofted tube cut in half and joined), 2928 (cap-touching
lips with 0.0001 mm offset walls), 3850 with 3741 (cross cut touching a round side, and the
hole after it), 1984 (near-coaxial pin and rivet overlaps). #3573 is now a source of
shell/3576. Their lines are gone from the skipped files; #2259, #3518 and #3602 stay skipped
with corrected reasons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hollow elbows lofted along a bend (Pappus), hollow bands halved and joined, a flange touching the end of a bent tapered loft, tapered and twisted lofted blades patterned round a disk, and a loft joined into a window that it overhangs, alone and mirrored on a hollow case.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A shallow loft cut from a slab top face, a loft cut whose sections touch the block sides given in either order, circles lofted into a cone on a turned-over plane, a loft guided by a rail, a circle lofted to a comb outline, a square lofted to a circle, and a circle-to-rectangle loft joined to a cylinder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Ruled lofts of zig-zag outlines worked out with the prismatoid formula, rotated rectangle sections turning through half a turn (Pappus), ring sections with round and rectangular holes, a cone loft meeting a cylinder wall near its top, a loft standing on a box face, a loft cut limited by targets, and a refusal for sections in one plane.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The rest of the thread cases (holes at the nominal size, shafts and holes
off the thread's size, the crash and sketch-attachment repros) and the
triage of the sources that are not kernel geometry.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
External and internal modelled threads, tapped holes and threads beside
other features, each graded against the ISO 68-1 groove volume (Pappus)
and the part's own closed-form volume.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
SMLib bounds an intersection edge on a piece of its spline (seed 32 intersect:
a block and a cylinder, the top disc's edges), and the truck decode refused
such an edge ("not supported yet: a spline or intersection edge on part of its
curve"), since truck bounds an edge by its curve's whole range.
bspline::segment cuts a B-spline to a sub-interval of its domain exactly:
Boehm insertion of both ends until each has multiplicity p, then the knots and
control points between them, clamped on the interval. The same points at the
same parameters, so the edge keeps its interval and the record its numbers.
load_edge_curve loads a spline or rational spline edge on part of its curve as
that piece; an intersection edge on part of its curve is still refused. Our
own records (interval = the curve's range) load as before.
Tests: segments of a rational cubic with a double knot, inside a span, across
knots, from or to a domain end, match the whole curve within 1e-12; both
spline kinds load on a piece with that range and the same points, and beyond
the domain are refused; SMLib's block ∩ cylinder decodes (it was refused) with
π r² h within 1e-3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The adapter loads an analytic torus as truck's revolution of the whole profile
circle, a rational quadratic closed on itself, so an SMLib torus is one face
bounded by its two seam circles, each used both ways. The display evaluator's
domain gave a revolution a period around its axis only: projected along the
profile the loop jumped from one end of the knot interval to the other at its
start vertex, the plan failed ("boundary crosses itself") and the body fell
back to truck's mesher, which meshes a sphere bounded by its seam alone as
nothing (seed 60 union, a torus beside a sphere: 925.0 of 990.7 mm³).
tess::uv::closed_curve detects a closed profile the way closed does a closed
spline surface (clamped knots, first and last control point and weight the
same within 1e-9 of the polygon's extent; the shared checks are factored out),
the domain then has period = the profile's knot interval along it, and
EvalSurface::wrap/derivs wrap that parameter back into the interval, so the
plan's projection, unwrapping and both evaluators treat it as periodic. A
cylinder's or cone's line profile is unchanged.
Tests: a revolved whole circle, either parameter order, has both periods and
evaluates a period away as inside; SMLib's torus, alone and beside a sphere,
decodes without meshing to orient it, plans every face and measures within
1e-3 of 2π²Rr² (+ 4/3 πr³). With the closed profile undone the torus plan
fails as above.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Results of SMLib booleans on its own spheres came back wrong through
body_from_smlib (side-by-side runner: seed 13 cut −1611.9, seed 61 cut −252.4
against SMLib's 4056.9, seeds 46/61 union 80 % short). The schema 4 records were
right (check_geometry passes, outer loops counter-clockwise, shells out of the
material); three things on the truck side were not:
- Body::new orients a solid by the volume of truck's coarse mesh, and truck's
mesher meshes a sphere bounded by its seam alone as nothing: the rest of the
body set the sign and turned it inside out. A foreign record is oriented by
its shells, so body_from_smlib now builds the body with Body::oriented
(revolved faces turned as before, orientation kept, nothing meshed).
- rephase_revolved turns each revolved face so it sits around angle π. A face
that uses its seam both ways goes all the way round; turning its surface ran
the wrap through the face's hole, the display plan failed and the sphere
meshed as nothing. Such faces now keep their wrap on their seam (truck's own
primitives have none: their round faces are split).
- The display plan opened only a loop of one seam used both ways. A sphere face
whose meridian a trim splits uses each piece both ways, turning back at each
pole, and projected onto one parameter line ("boundary crosses itself"), so
the whole body fell back to truck's mesher and lost its sphere faces. A loop
through both poles that turns straight back at one now has its run between
the poles moved one period along, whichever way keeps it within one period
with a positive area (poles split as the planner splits them); the routing
check finds such loops too.
Test: SMLib's sphere less a box cap beside its seam, across it (seam split) and
a bar bored into its side away from the seam decode without meshing to orient,
oriented as recorded, every face meshed, within 1e-3 of the exact volume.
Before, the decode meshed the body to orient it; with only the plan's pole
handling undone the −x cap measures 50.3 of 469.1 mm³, with only the
rephase undone the bored ball 252.4 of 4059.5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Every other fillet, chamfer and blend issue and PR, one line each: auto-filed reports from private designs (picks and error only), sketch fillets and chamfers, UI, previews and navigation, feature and fit-option requests, reference cases that already exist in tests/oracle, surface modelling, and failures that belong to booleans, shell, import, tessellation, sheet-metal or threads.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The graded volume was right; the derivation text quoted the union as 46476.5 instead of 45571.7.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Copies of filleted and chamfered features (a pasted block, a pattern of pockets with corner rounds, a pattern of chamfered bosses), a grille of rib roots, a join that needs a boolean retry followed by a fillet, and curvature-continuous fillets worked out from the documented quintic section.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rounds along curved outlines, with the removed volume worked out by Pappus or by integration: merged cylinders, a D prism, a rounded rectangle with 0.02 mm end edges, a long box beside quarter turns, vertical corners after bottom rounds, a bent tube end, a boss's bottom arc, bars running into a boss, an edge ending on a curved wall, and the knife-edge arc corner.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rounds with their own right answers: a radius larger than the ring it runs along (refused), a pick on a smooth seam (refused), a far edge pick (refused), a round that uses up an end face or a wall, a round over a hole, rounds on both sides of a thin wall and concentric with an inner one, a round on a sphere's flat cut, an edge ending on a tangent seam, and rims on B-spline ramps.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Chamfers on boxes, plates, discs and sectors with closed-form volumes: all twelve edges in the order that was refused, a concave chamfer that adds material, a chamfer next to square openings and one that runs into them, a hole rim tangent to the bottom face, a hex hole's corner bevels, a one-pick chain on a large part, a bottom loop as wide as the corner round, two distances on a circular rim, the end outlines of a rounded prism, a chamfer over a joined pin, and the equal chamfer of a slanted plug. Spline-sided pillar (invariants only) for chamfer and fillet.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Cases: mirror of a cut under a wall (#4011), coplanar intersect (#3671), multi-lump
intersect (#3526), split along a face plane (#3447), 64-peg join (#3455), boss on a rod
(#3485), collinear cylinders and pin in a bore (#3497), swept G2 cut (#3604), spokes
tangent to a hub (#3612), sphere and pyramid (#3633), column in a channel (#3665),
extrude to a point on an edge (#3729), emboss on a cylinder (#3782), circle-only join
(#3841), ring on a perforated plate (#3887), symmetric revolve cut (#4008), walls round a
floor (#4058), slab beside a bore (#4095), and others. The rest are skipped with a reason
in skipped-c.json.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every other sheet-metal-keyword issue and PR, one line each: UI, docs, file
format, naming and attachments, DXF output, feature requests, user designs
with no repro, and failures that belong to booleans, fillets, shell, sweeps
or threads.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Trays at 72 degrees and with four angles and heights, a tray shrunk by a
parameter and unfolded, an L bracket unfolded and refolded, opposite flanges
at 105 and 120 degrees, torn corners folded, and a rhombus whose square
reliefs would overlap.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Unfolded trays graded by the flat size W + 2(H - BD): torn, round and square
relieved corners, per-feature reliefs, rounded plate corners; a shelled tray
and a bracket converted at their bends; an L converted to the rule's radius.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A flange beside the leg of an L, along what a cut-out leaves of an edge,
beside a ledge as tall as its setback, a return on a flange's far edge, a
round hole, a slanted slot and an ellipse across a bend, a hundred slots with
four folds, and the refusal of rule values sheet.flange would ignore.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Cut-outs folded and refolded, a hole square to a 45 degree flange, a slot
that parts a flange from its base, an inside flange on a plate turned
62 degrees, cuts after cuts, a hole beside two adjacent flanges and a flange
on a flange's side edge; refusals for a flange on a bend line and a flat
pattern after a fillet; a mirrored sheet body that must stay sheet metal.
Volumes by hand (K = 0.5 folds: flat area * t; otherwise bend volumes).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Plates converted to sheet metal (holes, long thin strips, engraving, a
channel with modelled bends, position kept) and holes and cut-outs
through or into a sheet; volumes by hand from the plate and the holes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Folds of tabs and plates, graded by closed form: a fold keeps the blank's
flat length, so V = flat area * t + theta * w * t^2 * (1/2 - K). Tabs
on one line (#264, #638), a tab on an L's inside edge (#639), a fold just
inside a tab root and a refusal at the root (#262), a fixed point on the
top face of a large plate (#685).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Cases for the near-contact gap join, a countersunk hole beside a distant
body, a perforated plate under a ring, a sealed void, a rim and tooth join,
a frustum cut, a line-to-line pin, a flush notch, spoke patterns and a join
into a rotated occurrence. The rest of the bundle is in skipped-b.json with
one line each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hand-derived cases for tapered and collinear extrudes, pattern and mirror
copies keeping their targets, a cut up to a face, a prism joined to a
round-cornered slab, a clip round a small hole, and near-contact joins.
2564, 2583, 2601, 2754, 2755 (pattern, mirror), 2801, 2810, 2826, 2580,
2896 (overlap)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spheres cut twice (#1717), box/cylinder intersect (#1647), crossing cylinders
moved far (#1649), coaxial cut from a thin disc (#500), coincident cylinder
walls (#114, #2008, #1910, #1865), rim touching a coin (#168), holes along
walls and beside rounds (#559, #562, #1279, #1355, #2055), tangent holes
(#307), blind cut in a cylinder (#308), through-all cut (#423), shallow cuts
(#1858), skin left by a short cut (#1730), delete face (#1927), collinear
extrude sides (#1949), hidden bodies (#2464), mirror and patterns (#2003,
#2524, #2076), tapered extrudes (#831), text cut (#1195), split (#13).
Every other item in the bundle is in skipped-a.json with its reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every other tessellation-keyword issue and PR, with one line each:
rendering and GPU drawing, UI, performance, display-plan plumbing, and
failures that belong to booleans, fillets, threads, import, shell,
sheet metal or crashes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
311 ramp cut across a round blank, 784/811 filleted and drilled bodies
for STL export, 818 blind hole in a filleted ring, 819/921 pocket that
splits a wall seam, 1263 concave arc with a knife-edge tip, 1615/1653
crossed lofted tubes, 1648/1850 sphere touching a box edge, 1666/1670
two twisted lofts, 1753 bowl rim cut by a cylinder, 1813/2712 lens face
thinner than the tolerance, 2069 notched ring moved far away, 2528
slot joined with an overlapping cylinder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
140 join with a pad hanging past the edge, 1219 bore fillet then grooves,
1985/2243 STEP export of two enclosures, 3540 cut with a coincident cylinder
face, 3685/1559 delete a fillet face, 4092 loft + ribs + pad union (two
recipes), 2992 disk and 36 twisted loft blades. Everything else is skipped
with a reason (private designs, other areas, UI, perf only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4009 closed shell, 4010 shell open at a hole wall, 2531 point pick after an edit,
3468/3898 offset next to a fillet, 12 split face piece, 566 swept bar, 745/3893
freeform boss (two variants), 3103 tapered cylinder, 3669/2990 loft, 3283/3650
and 2939 curved offsets, 3506 and 1972 split then shell, 3576, 391, 1494, 1525.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cases: #3702 (a board in the cavity of a case with a filleted rim, cut leaves the case
unchanged) and #258 (STEP cone to a point, with and without an apex vertex, and the 0.5 mm
pin). Everything else in the bundle is skipped with a reason in skipped-other.json.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- solidcraft-cli and solidcraft-engine forward an `smlib` feature to solidcraft-kernel/smlib, so
`cargo build -p solidcraft-cli --features smlib` gives the rig SMLib's check.
- solidcraft_kernel::boolean_kernel() says which kernel this process runs booleans on.
`default` until the SMLib switch (SOLIDCRAFT_BOOLEANS=smlib) lands and answers there. An smlib
run on a build without the switch reports "not supported yet" instead of grading the default
kernel under SMLib's name.
- An area may keep its skipped list in parts (skipped-<part>.json).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows without an analytic volume (the fuzz seeds) get one from the
primitives' definitions, by no kernel: along each vertical line both
operands are exact z intervals, combined exactly, integrated over a grid
whose cell edges include every straight face, with the half-resolution
grid bounding what error is left. Each kernel's volume is graded against
it (within 1e-3 relative or 3x that bound), in the JSON per row and in the
report's summary.
On 64 seeds truck is right on all 192 rows. SMLib's own measure is wrong on
13, all with cylinders or spheres, although truck's measure of the same
exported bodies is right on every one of them; 5 give no closed solid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every geometry case the old kernel got wrong becomes tests/kernel-cases/<area>/<id>/: an
engine script or oracle recipe, and expected.json with the right answer, from analysis or a
hand calculation, or else the invariants. Never a kernel's output.
- solidcraft-cli kernel-cases <dir>...: runs a case and grades it. The checks are the outcome
(succeeds, or refuses cleanly), the body count, and one closed shell per body that passes the
record's structure and geometry checks and, with SMLib built, SMLib's check(All). Counts,
volume, area, centroid and box are graded where the case gives them. Each run is pass, fail,
not supported yet or crash.
- cargo xtask kernel-cases [--kernel default|smlib|both]: one process per case, so a native crash
or a hang (--timeout) is that case's result. It writes docs/kernel-cases.md: coverage per area
and source, results per kernel, and the sources looked at and skipped (<area>/skipped.json).
- solidcraft_kernel::validity and smlib_built, which the rig's checks use.
tests/kernel-cases/README.md gives the format.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When SMLib's volume of a boolean result differs from truck's, the report could
not tell a wrong body from a wrong SMLib measure. The runner now measures the
result it exports back to truck as well, keeps that volume in the JSON, and
shows it in the export column wherever it leaves SMLib's own measure or SMLib
could not measure the body at all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The side-by-side runner now reports how long each boolean takes on truck and
on SMLib (the call alone), and apart from that the SMLib side's conversions:
the operands' truck→SMLib import and the result's SMLib→truck export, which
the app pays while truck still holds the bodies. The report gains a times
summary (median, worst, total), solid counts, the analytic volume of each named
case's union, intersection and cut, and how many results land on it.
It also runs the "each cut gets slower" case from TASK_kernel-smlib-booleans as
a named sequence: a 63.6 × 31.7 × 9.5 block, a Ø20 × 10 boss joined on top, a
slot front to back, a Ø6 cross hole through the boss and a slot that splits
the block, each step on the previous result. SMLib keeps its own results from
step to step (only the tools come from truck), in a child process like the
cases, and every step's volume is checked against its closed form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The smlib_booleans example answered "not supported yet" for every SMLib case.
Now it runs solidcraft_smlib::Brep::boolean on the tagged operands (cut maps
to Subtract, None is an empty result), reads the result's history from the
tags, and measures its volume with mass_properties at MEASURE_ACCURACY, so the
report compares truck and SMLib on real results.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
truck lists some planar faces' hole loops before the outer loop, so the
rule that loops[0] is the outer loop never held for truck's records; moving
it in the adapter would renumber those faces' edges, which history and
names stand on. The outer loop is the one that runs counter-clockwise about
the natural normal (what check_geometry checks); the order is the
producer's, and SMLib's import puts the outer loop first, as SMLib needs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
check_geometry compared pcurve ends with edge points only, so a loop whose
pcurves enclose nothing passed whenever its uses lift onto the same 3D points:
SMLib's hole-rim fillet with both seam uses on u = 0, or a whole sphere whose
pole-to-pole seam has both uses on one line (the continuity check skips the
pole). Such a face has no surface and meshes to nothing.
Each edge loop's signed (u, v) area is now taken over 16 points of each use's
pcurve: a loop within 1e-9 of the square of its extent of zero is refused
(Problem::Empty), and each face must have exactly one counter-clockwise loop
about the natural normal, the rest clockwise (Problem::Turn). Outer and inner
are told apart by area rather than position, because the truck adapter writes a
face's wires in truck's order, which puts a hole first on some planar faces of
the sample designs (the model cache test caught it).
Fixtures: the pole-to-pole seam on one line, and a sheet whose plane axes are
swapped so its loop runs clockwise; a seamed sphere passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
A spline surface whose first and last control rows coincide in a direction
(points and weights, knots clamped there) closes on itself, but the display
evaluator's domain gave NURBS no period. The pcurve routine therefore never
moved a seam's second use by a period: SMLib's fillet round a hole's rim (a
rational spline closed in u) got both seam pcurves on u = 0, its outer loop
enclosed no (u, v) area, the decoded face tessellated to nothing and the body
measured 6839.73 mm3 against SMLib's 6989.00.
tess::uv::closed detects the closure from the record, within 1e-9 of the
control net's extent; the domain then has period = the active knot interval's
width, and the evaluator wraps a parameter outside that interval back inside
it, so pcurves, projection, sizing and the display plan all treat the direction
as periodic. The record stays as it is. The display plan hands both evaluators
wrapped parameters (the GPU does not wrap).
Tests: a rational torus band, a NURBS cylinder and a NURBS dome with a pole,
each bounded by a seam used twice, get seam pcurves one period apart; a record
of the band passes check_geometry and its seam uses on one line are refused;
SMLib's hole-rim fillet decodes with every face meshed and SMLib's volume
within 1e-3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Brep::boolean (union, subtract, intersect; Ok(None) for an empty result), Brep::solid_count,
Brep::fillet, Brep::chamfer and Brep::shell, over the fork's smlib_ops.cpp shims (ffi/ops.rs).
Edges and faces are chosen by points, each the nearest within a tolerance; a point that
selects nothing or cannot decide is Error::NoMatch / Error::Ambiguous with its index, and a
solid input that gives anything but a closed solid is Error::NotSolid. Fillets and chamfers
take whole tangent chains; a chamfer distance is the setback along each face; a shell's
thickness is negative inward.
The C API with these shims is version 2 (the fork's capi-flat line), so ABI_VERSION is 2.
Tests (ops_tests.rs) against analytic volumes and counts, SMLib's own check at every level on
each result, and history through a tagged box minus box: union 9000 mm3 [11, 24, 16]; slot
6000 [10, 24, 16]; intersection 1000; empty results; a split body (2 solids); a cube with all
12 edges rounded r = 2 against the Minkowski formula; one chamfer (7910, [7, 15, 10]); shells
open at the top inward (3392), outward (4672) and closed; a tangent chain from one pick equal
to picking all three edges; hostile sizes, tolerances, points and counts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
build.rs asked Cargo to rerun only when capi/ changed, so a fix in the fork's source/ (SMLib
or SM_API) left the old objects linked: the tests kept crashing on a kernel bug already fixed
on disk. It now watches source/ too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The hyperbola and parabola cone pieces and the revolved cylinder, read by solidcraft-step into
schema 4 and built back into truck bodies, keep their closed-form volumes with their faces
pointing out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SURFACE_OF_REVOLUTION reads as a placed revolution (u the angle, v the profile's parameter,
STEP's normal); SURFACE_OF_LINEAR_EXTRUSION as an exact spline surface, the profile as a
(rational) B-spline times degree 1 along the vector; a circle extruded along its axis or
revolved in a plane through the axis as the cylinder or ring torus it is. Line, conic and
open-conic profiles are bounded by the face (a closed conic's seam outside it, a revolved
profile stopped at the axis). OFFSET_SURFACE of the analytic kinds reads as the moved
surface, checked at each level. Hyperbola and parabola edges read as exact rational
quadratic pieces and a Bezier on STEP's parameter. Faces winding around a revolution get
seams along its profile. Every mapping is checked point by point and normal by normal
against STEP's own surface.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schema 4 counts every pcurve's control points as items, which schema 3 had
none of, so a body that fitted the old 1,000,000 no longer does (a STEP
plate of 50k faces). The binary envelope's own caps on bytes, values and
entries per collection still bound a body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
A face winding around a closed (non-periodic) spline surface without a seam edge has loops
that jump across the surface's domain; it is refused for now instead of making a record whose
loops do not close. The step-conformance parts are read in a test, and the kernel's ignored
corpus comparison builds each record back into a truck solid to compare volumes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A face on a cylinder, cone or sphere bounded by loops that go round the axis (a band
between two circles, a dome on its equator, a cone on its base and apex) gets a seam:
a ruling or meridian from one loop to the other (split where it has no vertex at that
angle) or to the pole it covers, so every loop closes in (u, v) as schema 4 requires.
Pcurves may be fitted up to 1 mm off for edges that lie off their surfaces. The kernel
gets an ignored corpus test comparing truck's face and edge counts with schema 4's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
read_solids walks MANIFOLD_SOLID_BREP, BREP_WITH_VOIDS and SHELL_BASED_SURFACE_MODEL
into solidcraft_geom::brep records: lines, conics, B-splines and polylines; planes, the
analytic surfaces and B-spline surfaces; edge intervals from the vertices' parameters;
loops relative to the surface's natural normal; outer loops, regions, voids and sheets;
pcurves from the 3D edges; the canonical order. Unsupported geometry refuses its solid
with a reason and the rest of the file is still read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Boxes and cylinders go into SMLib as truck's own B-reps, through their
schema 4 records (smlib_body), so both kernels start from the same input
whatever the axis. Spheres and tori are SMLib's primitives about +Z, like
truck's, since the import doesn't take truck's revolved surfaces yet. All
41 cases' operands now build; only the boolean itself waits for its shim.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cylinders (also against oracle 02 and 38's expected.json), spheres, cones, frustums and tori,
at the origin and 2e5 mm out: area, volume, centroid, moments and products within 1e-6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SMLib B-reps leave the C API as its flat arrays, read through the crate's
one flat wrapper (crate::flat::Flat, copied out as import::Arrays), and
become schema 4 records: shell pairs merged, sheets collapsed, radial cycles
normalised, analytic surfaces as their kinds (narrowing cones kept), loops
reversed where SMLib's surface is inside out, every pcurve from
brep::pcurve, canonical order, then validate() and check_geometry(). Edges
arrive as their own kinds; NURBS edges that are lines or circular arcs (the
primitives' circles) are recognised as a fallback and counted. With the
kernel's `smlib` feature, body_from_smlib() decodes them into a Body.
Adds the cylinder, sphere, cone and torus primitives. Each primitive passes
SMLib's own check at every level, its volume from the exact mass properties
matches the closed form, and it goes round SMLib -> schema 4 -> SMLib with
its counts, volume and a clean check. Truck now meshes a whole sphere
bounded only by its seam (SMLib's sphere) through the display plan.
SMLIB_REV moves to the fork's capi-primitives (the primitives on capi-flat
40bb855).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
SMLIB_REV moves to the fork's capi-static 40bb855 (flat arrays, C ABI 2).
flat.rs wraps them (Flat: arrays by id, Brep::export, Flat::import,
Brep::is_manifold_solid; FFI in ffi/flat.rs). import.rs lays a schema 4 record
out as those arrays: a closed shell becomes one SMLib shell per region it
separates, a sheet's shell holds both sides of each face, the infinite region
is marked void as SMLib has it, pcurves become UV trims on B-spline faces only
(reversed for edgeuses against their edge), planes with non-unit axes go as
their exact bilinear patch. Brep::from_record builds the SMLib body.
kernel: brep_record(body) gives a body's schema 4 record, and smlib_body
(feature smlib) its SMLib body, trusted (no healer: healing moved a
cylinder's volume by 9e-7). Box, cylinder and tilted cylinder keep their
analytic volume in SMLib (1e-9, 1e-6) and are manifold solids; truck's
spheres and tori are revolutions, refused with "not supported yet" for now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A whole sphere bounded only by its seam (SMLib's sphere: one meridian
used twice) came out with both uses on the same u line, a loop that
encloses nothing. Two causes in the per-loop closure:
- The pole-junction search took any positive signed area as the right
turn. With the two uses on one line the area is rounding, and in most
frames that rounding is a tiny positive number, so "no shift" won over
the period shift. A turn now needs more than 1e-9 of the loop's
period box.
- Junctions counted as poles only where the surface collapses exactly.
Ends within the tolerance of a pole (as a kernel's records have them)
were closed to the nearest period instead. A junction is now free
where moving its periodic parameter by half a period stays within the
tolerance, and fit samples within a quarter of it take their free
parameter from a neighbour, so the pcurve runs straight into the pole.
Tests: the sphere bounded by its seam in four frames, three meridian
angles, circle and NURBS meridians, both loop orders, ends at or a tenth
of the tolerance off the poles; and a torus bounded by its two seams.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Area, volume, centroid, and inertia and products about the centroid (with inertia_tensor()),
integrated by SMLib on the exact surfaces, through the fork's smlib_brep_mass_properties and
smlib_brep_area. MEASURE_ACCURACY = 1e-8, the finest SMLib allows. At that accuracy the fork's
own test puts boxes, cylinders, spheres, cones and tori within 2e-7 of their closed forms.
Tests: tests/oracle/01-box against its expected.json (to 1e-12), a box 2e5 mm out keeping its
inertia, and accuracy arguments. SMLIB_REV moves to the fork's measure commit (3e95b79).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ValidatePointers, then AssertValid over the whole topology graph (the fork's
smlib_brep_check), at CheckLevel::Basic, Geometry or All. A body that fails is still Ok, with
the failed tests counted and one line each in the CheckReport; Err means the check could not
run. Tests run it on every SMLib result and import.
SMLIB_REV moves to the fork's check commit (39f398e), which also fixes a seam-curve leak that
AssertValid hit on every periodic face.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
decode builds topology with Edge::new_unchecked and Face::new_unchecked,
so a record whose curves miss their vertices or whose pcurves miss their
edges loaded and failed later inside truck. It now runs check_geometry
after validate and refuses such a record (a cache miss at the IO
boundary). Only our own cache feeds it today; STEP and SMLib exports will.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
validate() is structural only, and a kernel adapter that builds topology
from the records without its own checks (truck's new_unchecked) would take
an edge whose curve misses its vertices and fail later inside the kernel.
check_geometry spot-checks what it assumes, each against the record's own
tolerance: every edge's curve meets its vertices at its interval's ends;
every oriented edge's pcurve, lifted through its face's surface, lies on
the edge's curve at the interval's ends and middle and continues the use
before it on the same period; every vertex loop's vertex lies on its
surface. A fixed number of evaluations per record, so linear in the item
budget; a point that cannot be evaluated is an error (Mismatch, naming the
record), never a panic.
The schema fixtures now carry real planes and pcurves (from
pcurve::pcurves), so every form passes; one-defect fixtures and hostile
records cover the refusals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
SMLib makes a fillet or chamfer face with no tag, so history::blends gives
each Generated face FaceOrigin::Edge for every blended edge whose two faces
both lie next to it and no longer meet directly. For the fillet shim's
history once it lands; a corner patch between blends keeps no origin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
examples/smlib_booleans runs truck and SMLib on the same cases (named pairs
whose results are known by hand, then fuzz::boolean_shapes for N seeds),
each as union, intersection and cut, and compares volume, face and edge
counts and the shape of the history (how many faces Kept, Modified, Split,
Merged, Generated; edges from an operand edge, new, or without origin). It
writes a JSON and a Markdown report and never changes the default kernel.
SMLib runs in a child process per case, so a crash inside it is reported as
that case's result. Its primitives and booleans arrive with the operation
shims; until a call exists the case is reported as not run on SMLib.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brep::tagged gives a copy whose faces and edges are tagged as one operand's;
after an operation on tagged copies, Brep::history reads back each result
face's origins and change (Kept, Modified, Split, Merged, Generated) and each
edge's faces and origin edges, as SMLib itself carried them through the
operation's splits, merges, copies and coincidences. Raw::history turns that
into solidcraft_geom's History, renumbered into canonical order by each
body's Order, with a new edge recorded as where its two faces meet.
The FFI declarations are in src/ffi/history.rs, beside ffi.rs. Tests cover
the C layout, hostile records, the meeting edges, renumbering and bad
orders, and (with SMLib built) a tagged box.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new crate, solidcraft-step (L1, no modelling backend), holds the Part 21
parser (moved from the kernel, which now reads through it) and STEP geometry
as plain data: lines, conics, B-splines (rational and implied knots),
polylines, trimmed curves, surface curves with their pcurves, 3D offset
curves; planes, cylinders, cones, spheres, tori (degenerate too), B-spline
surfaces, revolutions, linear extrusions, offset and rectangular trimmed
surfaces. Each keeps STEP's own definition and parameterisation, in mm and
radians, with pcurves scaled by their surface's parameter units. Shared
entities stay shared. A writer emits the same entities back, deduplicated.
Tests per entity check values against analytic points, units, round trips
and hostile input; an ignored test reads and round-trips every edge curve
and face surface of the tangency-labs/models STEP files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ffi::line was a safe public function reading any raw pointer. The log
callback now lives in ffi.rs, reads the C string there, and hands the
crate a String through a safe sink, so no safe function takes a pointer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
A new crate (L1, only the kernel uses it) builds tangency-labs/smlib's SMLib,
SM_API and C API with the cc crate when its `native` feature is on, from the
commit in crates/smlib/SMLIB_REV (scripts/fetch-smlib.sh fetches just those
sources). The kernel's `smlib` feature turns it on. Without it nothing C++
is built and every call returns NotBuilt, so workspace builds need no C++
toolchain.
unsafe is confined to src/ffi.rs. The wrapper owns handles, maps statuses to
errors, checks the C API version, and sends SMLib's per-call log to the log
crate. Crash reports note the native kernel call in flight.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SMLib makes narrowing cones with a negative half angle; schema 4 now takes
them as they are (r(v) = r0 + v tan a already covers them), with the
natural v range below the apex. The check that no region lists a shell
twice was quadratic per region (about 1e9 comparisons at the caps); it now
remembers the last region that listed each shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The adapter now saves each edge's curve record and interval when the edge first appears and
computes every face's pcurves from those records and the surface record it writes, with the
shared plain-record routine; the truck-face version in native/pcurve.rs is deleted. Loops go
in as the record keeps them (relative to the surface), so a reversed face no longer has its
wires reversed and back again.
The native tests are ported (adapter::face_alone gives one face's record for them). The cut
body's intersection pcurves now fit at 1e-6 mm too: the routine follows the converged
intersection, and truck's own points are within its 1e-6 mm of it, which the lift check
allows for on intersection edges. docs/native-brep.md says where pcurves come from and what
a surface intersection's point at t is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
solidcraft_geom::brep::pcurve::pcurves(surface, loops, tolerance) computes every edge use's
pcurve from our own records: the surface record and, per loop, each use's curve record,
interval and direction. No backend's parameter space is involved, so the truck adapter and an
SMLib conversion can share it (owner decision 2026-10-21).
brep::eval evaluates every schema 4 kind: planes, splines and revolutions through the display
evaluator (EvalSurface, EvalCurve, tess::uv), cylinders, cones, spheres and tori in closed
form in angle and height, and a surface intersection as the point of both surfaces in the
leader's normal plane (what truck's subs converges to), by Newton's method.
Exact in our parameters: lines, polylines and whole splines on planes; a circle about the axis
of a cylinder, cone, sphere or torus (a u-line); a line along a cylinder's or cone's ruling (a
v-line). Each exact pcurve is checked by lifting it; everything else is the cubic B-spline fit
the kernel had, with its span and round caps. Curve2::eval moves here from the kernel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The parameter box, the projection of a point onto an evaluator surface (closed form on a
plane, damped Gauss-Newton elsewhere), unwrapping and the pole test work on geom's
EvalSurface alone. They move from the kernel's tessellator to solidcraft_geom::tess::uv so a
pcurve routine on plain B-rep records can use them without truck; the kernel re-exports them
and its tessellation is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
A test over every design the repository keeps (the action matrix's
corpus: old design files, examples, regressions, demo and typical
designs, the oracle recipes; plus the sample designs, the engine's
design fixtures and the older designs of the hole matrix, taper and
projection tests: 154 designs). Each is rebuilt and recorded in
tests/naming/<group>/<design>.txt: per body the names of its faces and
edges with whether each follows exact history, and every stored name
(feature face picks, fillet/chamfer/bend edges, sheet metal edges, the
face a sketch is attached to) with the face or edge it finds now. A
difference fails with the design and the changed lines; record an
intended change with NAMING_CORPUS_BLESS=1, as FEATURE_WALK_BLESS does.
Face and edge indices are left out on purpose. They are the kernel's
order: truck already gives a fillet's or a boolean's faces in another
order from one process to the next (oracle 07 and 37 changed on every
run), and SMLib will order them differently again, while what must not
change is the names and what references find. Names are listed sorted.
One design's names themselves change between runs on truck
(closed_sweep_torus_cut: level wall halves numbered #1/#2 in kernel
face order); it is listed in UNSTABLE with the reason and its
difference printed instead of failed on.
Records made on this branch (truck), macOS. The whole corpus builds in
about 20 s on 14 cores (designs run side by side).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The corpus naming test (next commit) records what every stored face and
edge name resolves to, and must find it exactly as a rebuild does. Three
lookups were crate-private: `locate_face_on_body` (a feature's face
points, tried on the body the feature names first), `blend_target` (the
body a fillet or chamfer rounds) and `Document::anchored_face_indices`
(the faces a face-attached sketch is on). Visibility only; no behaviour
changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
docs/native-brep.md describes schema 4: the records with their SMLib and
STEP names, regions and shells, the loop convention, every edge's uses,
the canonical order (a reversed face walked backwards), each curve and
surface kind's parameterisation, pcurve rules, validation, history, the
SCBREP05 envelope and what the truck adapter writes and refuses.
A test lifts every pcurve of a box, a filleted box with a bore and a
sphere onto its face and checks it is its own edge at both ends and the
middle; with a reversed face's pcurves left in truck's oriented order it
fails by 30 mm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Edges, vertices and the body carry the face tolerance (10^-6 of the
body's size, at least 10^-5 mm): truck's traced intersection edges sit
about 1e-6 mm off their surfaces, so the linear tolerance is too tight for
a pcurve to lift onto them. The box-and-fillet golden fixture is the
schema 4 envelope now (NATIVE_BREP_BLESS=1 rewrites it), and the adapter
tests cover a pair of lumps. The model cache's producer revision becomes
solidcraft-truck-adapter/4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The plain B-rep follows SMLib's flat form with STEP's names: regions
(infinite first), face/wire/vertex shells, oriented faces, faces with a
domain and edge or vertex loops, oriented edges with a pcurve each, edges
with an interval, a tolerance and their list of uses, vertices with a
tolerance. Curves and surfaces are unbounded; their bounds are the edge
interval and the face domain. Loops are relative to the surface's natural
normal. Schema 3's read-only intersection and revolution records go.
The validator keeps schema 3's caps and adds the structural rules:
references, loop shape and connectivity, regions and closed shells, the
pcurve interval, natural parameter ranges, every edge's uses (primary
first, upper = !forward) and the canonical order of shells, faces, edges
and vertices. A reversed face is walked backwards, as truck's edge_iter
walks it, so truck's face and edge indices are the canonical ones.
The truck adapter writes truck's absolute wires as the loops, numbers
everything as the kernel does, and builds regions from the shells (a
tessellated nesting test only when there are several closed shells).
Pcurves come from pcurve::pcurves only; until it supports a face, encode
fails and the body is a cache miss. The envelope signature becomes
SCBREP05.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
FaceOrigin, Change, FaceProv, Side, EdgeOrigin, EdgeProv and History move
unchanged (fields, serde derives) to solidcraft-geom, so a backend can
report history without depending on solidcraft-kernel. The pure methods
(composition, renumbering, count checks, mark_shared_split) move with
them and return brep::history::Failed, which converts to
KernelError::Failed with the same "history: …" message.
What needs a kernel body (History::identity, check, face_sources) stays
in the kernel as the BodyHistory trait; solidcraft_kernel::history
re-exports the types, so existing paths keep working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
A band of a cylinder bounded by two full circles and no seam cannot close
in (u, v): each loop's pcurve runs one period. The test records that such
loops come back continuous and a period open (the caller needs a seam
there), and that a hostile tolerance is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Each sample of an intersection curve is one of truck's double projections
(about 0.13 ms in a release build), and a fit evaluates the curve at every
sample and halfway between. Starting from four samples a span instead of
eight halves that; the checks stay at eight points a span, and the knots
at the curve's own breaks already carry the shape. The cylinder cut by a
tilted box now takes 70 ms at 1e-3 mm and 149 ms at 1e-5 mm for all its
faces (release), from 183 and 638 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Faces with seams as imported faces have them (the kernel's own
revolutions come in pieces): a cylinder whose seam line is used twice
between two full circles, each a closed edge on one vertex, and a sphere
bounded by one meridian used twice from pole to pole. Their seam uses come
out a period apart, in the base period. The checks now also require every
face's outer loop to turn counter-clockwise in (u, v) for a face along its
surface's normal and clockwise against it, holes the other way: the rule
the pole junctions rely on, which every test body follows.
With SOLIDCRAFT_PCURVE_SVG=<dir>, a test writes each test body's face
loops in (u, v) as SVG for review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Every edge not exact on a plane gets a cubic 2D B-spline on its curve's own
parameter: through the parameters of the curve's ends, least squares
through samples between (banded normal equations), with spans halved and
sampled twice as densely wherever the lift misses the curve by more than
half the tolerance, at the samples or halfway between them. Knots start
where the curve's parameterisation is less smooth: a spline's own knots
and the vertices of an intersection curve's leader polyline, with the
multiplicity a cubic needs there (without them the cylinder-minus-box
intersection curves took 400 spans at 1e-3 mm). Spans and rounds are
capped; a fit that does not converge, or a curve farther from the surface
than the tolerance allows, fails naming the wire, edge and distance.
Samples are projected with the kernel's own surface parameter search (the
evaluator's form, parameterised as truck parameterises it; truck's search
for surfaces it does not take), each from the one before, unwrapped along
periodic directions. Where the surface collapses (a pole) the undefined
parameter comes from the nearest sample that defines it, so a pcurve runs
straight into the pole. Each wire's uses are then moved by whole periods so
each starts where the one before it ends, starting after a pole junction
if there is one; at further pole junctions the offset that gives the loop
a boundary's turn is taken. The wire then moves into the base period. A
seam edge used twice is fitted once.
Truck places intersection curves on their surfaces to about 1e-6 mm, so
their pcurves need a tolerance of 1e-5 mm or more; the test records that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
A plane's parameters are affine in space, so the pcurve of a line or a
(rational) B-spline on a plane is the curve's own knots and weights with
its control points mapped into (u, v): no fit. Curved surfaces and
intersection curves still return "not supported yet". A test helper checks
every pcurve the way the schema validator does, its interval against the
curve's range, the lift error and the wire's closure in (u, v); the box's
line pcurves lift onto their lines' ends bit for bit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
The shared base for the schema 4 types and the truck pcurves: 2D B-spline
pcurves as SMLib keeps its UV trim curves, and the truck adapter's pcurve
function, not supported yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
Arabic and Hebrew are complete catalogs since #3768, so
every_complete_catalog_carries_every_english_message fails a pull request
that adds an English message without them. The localization guide's list of
languages a new label needs now says so.
Co-authored-by: rootarchitect <7781866+rootarchitect@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4771
* Move/Copy takes a browser body the same way Combine does (#4771)
A body row feeds the dialog's active input through the same pick as a
click in the view, and does not replace the selection (#4452). A group,
the Bodies folder, or a component is that pick for every body in the
set. M and the Move/Copy button also start from a selected body or from
a component picked in the tree.
* claim: #3457
* Hebrew interface language with right-to-left text (#3457)
Adds a complete Hebrew catalog (assets/locales/he.json, original
translations) and registers `he`.
egui 0.36 shapes each font run with harfrust, so a run of Hebrew letters
is drawn right to left, but it places the runs of a line left to right in
stored order. The new `bidi` module runs the Unicode Bidirectional
Algorithm (unicode-bidi, MIT OR Apache-2.0) over each message of a
right-to-left catalog once, giving egui the runs in display order with
brackets mirrored and placeholders kept whole, while leaving each Hebrew
run's letters in reading order for the shaper. A test lays messages out
with a real Hebrew font and checks where the glyphs land.
Desktop builds append a system Hebrew font (Arial Hebrew / SF Hebrew,
Segoe UI / Arial, Noto Sans Hebrew / DejaVu Sans). Layout mirroring,
wrapped multi-line RTL paragraphs and a web Hebrew font are follow-ups.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hebrew: translate the new Describe Command label; list he in the control protocol
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hebrew: catch up with main's catalog and the new ribbon (#3457)
- Translate the 258 messages main added since the branch was cut, drop
the 38 it removed (10 renamed ones keep their translation) and
retranslate the two whose English changed (toolbar → application bar).
- The timeline title is ציר הזמן, matching the glossary.
- Ribbon buttons split their labels into two lines from the start of
the string; Hebrew labels come in display order, so the first line
showed the end of the phrase ("חדשה / סקיצה"). For right-to-left
labels the first line now takes the words at the end of the string,
and a shortened line loses characters, and shows its ellipsis, on the
left. The word-boundary sweep checks Hebrew from the lines the galley
is built from, since epaint's right-to-left rows carry zero-width
glyphs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hebrew: after rebasing onto main, load the Hebrew face with the cached system fonts
main now searches the system fallback faces once (SystemFonts, on a thread);
the Hebrew face joins that search with its coverage check. Drop the three
camera-animation messages main removed (#4296) from the Hebrew catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Hebrew: translate the messages main added after the catch-up (#3457)
Translates the messages main added since the last catch-up: the history
bar's buttons and name toggles, the 3D mouse driver and axis mapping, the
thread size columns, the File view (home page), the redesigned Preferences, the new issue report
dialog, free orbit and the assistant's setup card. Drops the one message
main removed. The catalog has every English message again, so the
complete-catalog checks pass for he.
* Arabic interface language (right-to-left, on the Hebrew bidi path)
Adds an original Arabic translation of the whole English catalog
(assets/locales/ar.json, every message) and registers it. `ar` joins the
complete catalogs, so a message added to en.json later fails the build
until it is translated. It goes through the right-to-left path of #3458
unchanged: crate::bidi puts each message in display order and leaves
every Arabic word's letters in reading order for harfrust, which joins
them with the font's own tables. Ribbon labels use the right-to-left line
split from the same stack.
fonts.rs adds a system Arabic fallback after the Latin interface font,
which keeps spaces, digits and Latin text so each Arabic word stays one
shaped run: Segoe UI/Tahoma/Arial on Windows, SF Arabic/Geeza Pro/
Damascus/Tahoma on macOS, Noto Sans or Naskh Arabic/KACST One/DejaVu Sans
on Linux, or NotoSansArabic-Regular.ttf in CRAFT_FONTS_DIR. It is the first
fallback for an Arabic UI (Arial Unicode, a CJK candidate, has Arabic
letters without joining data) and the last otherwise; like Thai and
Hebrew it is never promoted for a CJK interface.
Tests: the catalog comes back in display order with no directional
controls, and Arabic laid out with a real font lands on screen word by word
in reading order.
* Hebrew: review the catch-up translations; restore the camera-animation messages (#3457)
- Review of the 533 messages translated in the previous commit: 21
corrected. Keys in the motion dialog are keyframes (נקודות מפתח, as
in Add Key), the Design workspace is תכן as in its label, a dropped
"(Direct Driver)" is restored, terms aligned with the rest of the
catalog (פורמט, סוכן קוד, רצועת הכלים), and a few calques reworded.
- Translate ui.animate_view_changes, ui.animation_speed and
ui.look_at_anim_desc, which main has again, so he stays complete.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Arabic and Hebrew: translate the messages main added since (#3457, #3762)
Both catalogs again carry every English message. The 760 messages main added
since the last rebase (render workspace, material editor, standard views,
sheet metal Path Flange, AI app connections, document-on-disk notices and
more) come from A-AL-ANAZI's translations on #3768; the five Perforate dialog
messages are new here. The earlier Hebrew review is kept on top of them, and
33 of the new Hebrew messages are corrected: "completed" was "perfect"
(מושלם → גמור), Regrade read as ranking (now תיקון צבע מחדש), shaders,
guarantee, the Mirror reflection mode, simulations and displacement.
Co-authored-by: Abdullah <alksa2025@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Arabic and Hebrew: attribution rows as a pending file
main now takes new ATTRIBUTION.md rows as one file per change
(attribution/pending/), so the he.json and ar.json rows move there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Arabic and Hebrew: translate the 23 messages main added since (#3457, #3762)
Both catalogs again carry every English message; the two theme-page messages
main removed are dropped. Twelve of the new messages (link updates while
loading, the agent chat's model choice, plugin search, render quality) come
from A-AL-ANAZI's translations on #3768; the sketch plane descriptions, the
rigid group and empty-boolean messages, free sketch points and the web update
note are new here.
Co-authored-by: Abdullah <alksa2025@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Arabic and Hebrew: translate the 12 messages main added since (#3457, #3762)
Eleven come from A-AL-ANAZI's #3768; "Turn Up Axis" in Hebrew follows the
catalog's existing up-axis wording, and the agent chat's no-tool-call message
is new in both.
Co-authored-by: Abdullah <alksa2025@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Arabic and Hebrew: translate the 22 messages main added since (#3457, #3762)
* Arabic and Hebrew: a label found by its English text comes back in display order too (#3457, #3762)
* Arabic and Hebrew: translate the 32 messages main added since (#3457, #3762)
* Arabic and Hebrew: translate the 34 messages main added since (#3457, #3762)
* Arabic and Hebrew: translate the 30 messages main added since (#3457, #3762)
* Arabic and Hebrew: translate the 7 messages main added since (#3457, #3762)
---------
Co-authored-by: rootarchitect <7781866+rootarchitect@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Abdullah alanazi <x1700@MacBook-Air-Abdullah-2.local>
* claim: #6943 free orbit Shift snapping
* fix(ui): snap free orbit direction and roll with Shift
* test(ui): align free orbit coverage and complete snap help translations
Evaluation and placement never read appearances, yet an appearance edit
re-evaluated the model and placed every occurrence again. A whole-document
proof that only appearances changed now takes the existing render-only refresh,
which keeps the model and retargets the world-placement cache.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Sketch Transform: scale, turn and move sketch entities in one command and one undo
sketch.transform scales about a centre, turns about it, then moves; the window and its
handles for existing sketch geometry build on it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Sketch Transform window: one box with handles to scale, move and turn what is picked
Select curves and points in a sketch and press G (or Transform in the Modify panel or the
context menu): the picks get a box with a handle at each corner to scale (the opposite corner
stays), the box to move and a knob above it to turn (Shift snaps to 15 degrees). Position,
Width, Height, Scale and Angle in the window are the same values, to type exactly; OK applies
it as one step.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Move the new message to a pending locale file
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch Transform: follow the sketch field of Sel::SketchCurve
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #5542
* Reopen the designs that were open at the last quit (#5542)
A launch with no file to open reopens the saved designs that were open
when SolidCraft was last closed, in tab order. Designs never saved and
files that are gone are skipped. Preferences > General > Reopen designs
turns it off; a launch driven over --control or with --sample starts fresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move the new messages to a pending locale file
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>
Each command that names a body (appearance, material, move, ...) mapped it to
its component's own name by working out every placed body's world name and
frame, so a command on a large assembly cost a walk of the whole design. The
list is now kept with the world-placement cache while it is current.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Naming a placed body scanned every occurrence and body of the design, so the
world state and every caller naming all placed bodies grew with bodies times
design size. WorldNames indexes the design once and names each body in time
proportional to its occurrence path; one naming routine serves both it and the
single-body scan. Names are unchanged (a test compares them with the scanning
implementation).
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Viewport: palette-indexed scene and incremental appearance updates
- Move triangle, line and instance colours and opacity into a per-scene
palette (an R32Uint texture, WebGL2-safe); appearance edits update
palette entries instead of vertex bytes.
- Build CPU body triangles as one canonical vertex stream per block with
opaque/glass/faded index lists; an opacity class change rewrites only
the index list.
- Cache resolved body appearances by a per-body key, so an edit resolves
only the bodies whose look changed.
- Split the model scene key from the session revision: selection and
view-only commands no longer rebuild or upload the model scene.
- Replace the O(n^2) section-index lookup and per-placement instance scan
with hash lookups.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Viewport: skip unchanged palette work when rebuilding
- Reuse each body's appearance key while the document is unchanged.
- Leave a palette slot alone when its value did not change, and allocate
every body's slots once before the build loop.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Viewport: measure scene rebuild cost by edit kind
An ignored scale test times the scene build after selection, appearance,
theme and one-body geometry edits at 200, 1,000 and 5,000 bodies.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Viewport: read faded grid alphas through the scene palette
The grid-fade tests now resolve line palette indices instead of reading the
line record's old colour byte.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Viewport: measure rebuild cost on few large meshes
Adds the 20/100-torus case where a block rebuild used to re-emit a large
part of the model.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Viewport: measure rebuild cost on an assembly
Unique detailed parts plus one component placed many times, timed for
selection, part and component colour, opacity, theme, hide and move.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Viewport: draw block classes as vertex runs, not index lists
A body's vertices are three per triangle in mesh order, so each draw class
is a few runs of the block's vertex buffer: all-opaque blocks need no index
buffer, and an opacity class change uploads nothing. A fragmented class
(more than 64 runs) falls back to one index buffer.
Each triangle is written as one record into a block reserved up front,
which takes the cold scene build of a 7.1M-triangle assembly from 97 ms
to 55 ms (main: 83 ms).
Also fix reindexing a block whose bodies are drawn elsewhere: it walked
the mesh's triangles from that body's start, past its own vertices.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Viewport: draw the highlight's own block, and light faded bodies without copies
The highlight scene keeps its feedback (a hovered face, a body copied on
the CPU) in a block of its own; since blocks became vertex runs, only the
model's blocks were drawn, so that feedback was uploaded and never shown.
Draw the highlight's blocks with its triangles.
Whole faded and see-through bodies (another component active, or an
appearance below full opacity) were lit by copying every triangle on the
CPU at each hover change: 85 ms for a design row of 812 bodies. They are
now drawn again from the model's own block vertices, in the highlight
palette with their slots set to the highlight colour, or as placements of
the model's shared triangles. The picture is the same as the copy's.
Adds a GPU frame benchmark for document-tree hover (ignored test).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Viewport: count scene uploads, and keep the sorted glass stream across colour edits
Renderer::uploads reports the bytes the model scene's last change uploaded
(geometry, placements, palette rows, planned colours, re-evaluated vertices).
Without order-independent transparency the global glass stream is kept while
the blocks' see-through triangles are the same, so a colour edit on a glass
body uploads palette rows only. Tests assert both on a GPU.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Viewport: keep palette slots and looks without rehashing every body
A rebuild found every body's palette slots by name and wrote them back,
allocating and hashing a new key per body, and copied every shown name into a
set to check the arena still belongs to the design. Slots are written back only
when a body's faces grew, and the looks cache is pruned only when it holds more
bodies than are shown. Adds the STEP-file benchmark (SOLIDCRAFT_SCALE_STEP) and
the colour command's own time to the assembly benchmark.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Viewport: round-pick construction lines through the scene palette
The axis and centre feedback for round picks (#4610) drew with a raw colour;
on the palette scene it is interned once like the other construction geometry.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Viewport: draw faded and see-through bodies from their plans with OIT
A body faded outside the active component, or see-through, left the GPU
tessellation path and was built triangle by triangle on the CPU, so working in
a component of a large assembly rebuilt every other body. Where the renderer
blends see-through surfaces order-independently (GpuTarget::oit, set by the
renderer), a body whose faces share one class is drawn from its plan: chunks
are split by class, opaque passes, AO and the section draw only opaque chunks,
and the OIT pass draws the rest. The sorted fallback keeps them on the CPU.
The model and its highlights decide through one rule (model_plan_class).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
* Move the design-link path helpers into one module
`component_link.rs`'s path rules for links to other design files (base_dir,
absolute, relative, stored, resolve, rebased) move, unchanged, into
`cmd/design_link.rs` with their test, so other kinds of link (#3891's drawing
files, linked parameters #5354, #5113's extract) share them instead of copying
or re-exporting them. No change in behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* design_link: keep base_dir and relative private
Only design_link.rs uses them; the module's surface is absolute, stored, resolve and rebased (review on #5420).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* component_link: import Document after the rebase
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Carel Theron <careltheron@Carels-MacBook-Pro.local>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #6861 eval scripts on input designs
* fix(cli): evaluate scripts on the input design (#6861)
* fix(cli): validate eval options before opening inputs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6608
* Canvas: keep value boxes clear of the command window (#6608)
* Canvas: describe how value boxes clear the command window (#6608)
* Canvas: keep the value boxes clear of the gizmo handles (#6608)
* ui-egui: format the value box regression test (#6608)
* gpu: the startup MSAA check skips when there is no GPU adapter
The check needs a real adapter to draw the multisampled view and to
resolve it, so on a machine with no software Vulkan adapter it is
skipped, with the reason printed, instead of failing the build.
SOLIDCRAFT_REQUIRE_GPU=1 still makes a missing adapter a failure, as in
the CI job that has a software adapter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A cut (or intersect) that names no bodies went through every body it met,
hidden ones too: a body hidden to keep it out of the cut was cut anyway. When
some of the active component's bodies are hidden, the cut now stores the shown
ones as its targets, so the hidden bodies stay as they are, and stay so once
shown again (the model never depends on what is shown). With nothing hidden it
goes through every body it meets, as before.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Until a connection had shown the token, its first line (up to 16 MiB)
was parsed whole into a JSON value. A line of [0,0,...] that size is
hundreds of MB once parsed, so 16 connections without a token could
make the app run out of memory and lose unsaved work. The token is now
read from the request object without building the rest of it: other
fields are only skipped, and anything but an object with a string
token is refused as before.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Canvas to Sketch: trace a canvas image into a sketch on it (#3931)
New command canvas.to_sketch, offered as Canvas to Sketch in the canvas
context menu. It traces the canvas image (automatic Otsu threshold, light
drawings on dark backgrounds inverted) with interpolated marching squares
and Douglas-Peucker, and adds lines and cubic Bezier splines through Insert
SVG's add_geometry to a new sketch on the canvas plane, with the canvas'
width, centre, rotation and flip. Holes stay holes; one undo step.
The image's declared width x height is checked before decoding (at most
40 megapixels) and the decoder runs under image::Limits, since canvases
are capped by file size, not pixel count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move the new message to a pending locale file
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>
* claim: #2605
* Insert SVG / DXF: place and scale the drawing before it is added (#2605, #3443)
After the file is picked, an Insert SVG (or DXF) window sets the
drawing's X, Y offset and scale, with a live preview; OK adds it in one
step and Cancel adds nothing. The insert commands already took `at`
and `scale`; the window is what was missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Insert scenario: the picked drawing goes through the placement window (#2605)
`sketch_toolbar_imports_use_file_picker` expected the SVG added as soon as
it was picked; it now checks the placement window first and adds it with
OK.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Insert SVG / DXF: a size in millimetres, an angle, and placement by the drawing's centre
sketch.insert_svg and sketch.insert_dxf take width or height (mm, aspect kept, winning over
scale), angle (degrees) and align (origin or center). The new sketch.drawing_bounds reads a
file's own size without inserting it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Insert SVG / DXF window: Width, Height and Angle, placed by the drawing's middle
Width and Height in millimetres follow the scale (aspect kept); Angle turns the drawing
about its middle, where X and Y now put it. Uses sketch.drawing_bounds for its own size.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Insert SVG / DXF: handles in the viewport to scale, move and turn the drawing
The preview gets its box with a handle at each corner (scales about the opposite corner,
aspect kept), the box itself (moves) and a knob above it (turns, Shift snaps to 15 degrees).
They edit the same X, Y, Scale and Angle the window shows.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Insert handles: the move that comes with the release counts
A quick drag could stop short of the pointer's last position because the final move arrives
in the same frame as the release.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Insert SVG / DXF: the Drawing Size command's label in the catalogs
`sketch.drawing_bounds` came without a catalog entry, which the i18n
test requires (and the complete ja and zh catalogs with it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move the new messages to a pending locale file
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Phanoob <phanoob@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Nathalie Schönwetter <nathalie@cfw-ag.ch>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
* claim: #2113
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Crash reports: capture native crashes on macOS (#2113)
A signal handler for SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGTRAP and SIGABRT
writes the same <journal>.native.json as the Windows exception filter: the
signal, the faulting module and offset, the thread and a frame-pointer walk.
The next start reports it instead of "an unclean exit". The signal then goes
on to the previous handler or the default action, so macOS still writes its
own crash report.
native_crash_macos.rs joins the crash-safety skill's list of files that may
use unsafe (no safe API can catch these signals).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5641
* Cone primitive: solid.cone with base and top radius (#5641)
A cone or truncated cone placed like the cylinder: base centre, axis,
base radius (or diameter), top radius (0, the default, is a full cone)
and height, with the new body / join / cut / intersect choice. It is
built from what the kernel already exposes, a revolve of its half
section about +Z through the origin, moved into place. Toolbar entry
after Torus, a Cone dialog that also places it on a picked face, Edit
Feature, a timeline icon, pending UI strings and attribution, and one
pending design-format change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #1932
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Press Pull on a fillet's round face edits the fillet (#1932)
Q on a fillet's round face opened Offset Face, which refuses a face
between two others it is tangent to ("a corner whose faces don't meet
the same way after the move"), so OK stayed disabled. The engine's
solid.press_pull already changes the fillet's size on such a face; the
window now does the same and reopens the fillet (or chamfer) to edit.
Other curved faces still open Offset Face, planar faces Extrude.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6718
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Claim bot: let an unassigned claimant /unclaim (#6718)
GitHub doesn't assign people who aren't collaborators, so many claims
have the `in progress` label and the bot's "Claimed by @user" reply but
no assignee, and `/unclaim` refused the claimant. When nobody is
assigned, `unclaim()` now also accepts the user named in the bot's
latest "Claimed by" reply on the timeline, unless the label was removed
after it. Maintainers and the owner rule are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add Open in Slicer for the selected bodies (#2595)
One command saves the selected bodies as a 3MF in millimetres (an STL if the 3MF can not be written) and opens it in the last slicer used. It is on the File menu, the Mesh tab and the body right-click menu. The first time, or when that slicer is gone, it asks which one with the same list Save as mesh uses. The Save as mesh dialog is unchanged.
* Send slicer files to a private per-user folder (#2595)
Files sent to a slicer used to go to a shared folder in the temp directory. They now go to the user cache folder (mode 0700 on Unix), under a name that stays the same for the same design and bodies, so sending again replaces the file and the slicer can reload it. The file is written to a temporary name and renamed into place. If the name can not be replaced, as on Windows while the slicer holds the file open, the copy gets a number and the status line says so. Files older than seven days are deleted at startup. The first send tells the user that sending again replaces the open file, and so does the tooltip of Open in Slicer. Send to slicer in Save as mesh uses the same path.
* Open files in a running slicer (#2595)
PrusaSlicer, SuperSlicer, Bambu Studio and Cura are started with --single-instance, so a slicer that is already open loads the file instead of opening a second window. OrcaSlicer has no such flag and is left alone, and macOS bundles are single-instance already. PrusaSlicer 3.0 ships a launcher entry that already carries --single-instance-on-url; it keeps that one and never gets a second flag. It is a preference, on by default, and scripts can pass single_instance. Preferences also gets a button to forget the slicer Open in Slicer remembers.
* List slicers the desktop entries say open 3MF or STL (#2595)
On Linux the scan of desktop entries now also lists a program that claims 3MF or STL in MimeType, even when it is not in our table of known slicers. The table still supplies names, ids, icons and flags for the ones it knows. Entries marked NoDisplay or Hidden are skipped, and so are terminal programs and SolidCraft itself. The existing size and count limits stay. The Windows scan now looks inside a Prusa3D folder, where PrusaSlicer installs itself.
* Only start slicers the user can see or chose (#2595)
file.save_mesh took a slicer parameter that could be any executable path, so a script or an MCP client could run any program. The slicer now has to be one of the detected slicers (by id or name) or a program the user already added under Custom, which the app hands to the engine from its settings. The same rule holds for file.open_in_slicer. The agent chat already asked before any file command; its question now also names the slicer.
* Review fixes for Open in Slicer (#2595)
The file name hash keeps all 64 bits instead of 32. When Open in Slicer has to fall back to an STL, the status line now says so. The engine test that sets the chosen programs clears them again. The unused "could not make the folder" message is gone from the catalogs, and the docs say plainly that finding slicers by file type is Linux only.
* Keep unsaved designs apart and hide Open in Slicer in the browser (#2595)
Two unsaved designs with the same name and body names got the same file in the slicer folder. Every document now has a uid that is new for each New, Open and Recover and stays the same for the life of the document, and the file name uses it (with the process id) until the design has a path. At the first Save As the name changes to the path-based one, so the slicer gets one new file; the old one goes with the seven-day sweep.
The browser can not start a program, so Open in Slicer is now disabled there: the engine command reports why, the File menu item and the body and component right-click items are greyed, and starting it anyway says so on the status line instead of opening a dialog. The Save as mesh dialog keeps its browser guard for the open flow too.
* Put the Session doc comment back on Session (#2595)
The new NEXT_UID static had landed between the doc comment and the struct it describes.
* List every install of a slicer, and test the first Save As (#2595)
Detection kept one entry per known slicer, so a system PrusaSlicer 2.9 and a Flatpak 3.0 beta showed up as one. Every distinct install is listed now. The same program found twice, through a desktop entry and through PATH, is still one. When a slicer has several installs, each gets an id made from where it was found (prusa#comprusa3dprusaslicer) that stays the same on every run, and a name worked out from its command and location without running anything: PrusaSlicer (system), PrusaSlicer (Flatpak, beta), PrusaSlicer (AppImage). A lone install keeps its plain id and name. The plain id or name still picks the first install, so a remembered "prusa" keeps working. Single-instance flags and the other per-slicer behaviour go by the id without the install part.
Also a test that the first Save As changes the file name once and later sends keep the name that comes from the saved path.
* Start PrusaSlicer 3.0 first and send the file once it is ready (#2595)
PrusaSlicer 3.0 starts through slic3r-app-launcher, which ignores a file given at a cold start whatever the flag, and loads it over D-Bus when an instance is already running. A small table in slicers.rs names such slicers by their command; today it holds that launcher, so PrusaSlicer 2.9 and OrcaSlicer are untouched. Ready means a name starting with com.prusa3d.prusaslicer.BroadcastListener. is on the session bus, read from busctl --user list with no new dependency. If busctl is missing or fails, nobody waits and the file is sent as before.
When the slicer is not ready, Open in Slicer starts it without the file and looks every 200 ms for up to 10 seconds. The wait runs on another thread, so the window stays live, and the status line says Starting PrusaSlicer. Once the name appears the file is sent with the same arguments as before. If the limit passes, the status line says the slicer did not respond, that the file is already saved, and to send again. A second click during the wait only repeats the Starting line, so there is one launch and one send. Scripts calling file.open_in_slicer wait inside the command by default, or pass wait: return or no.
* Always show the slicer picker for Open in Slicer (#2595)
File > Open in Slicer, the Mesh tab entry and the right-click entry now always open the picker, with the slicer used last time pre-selected, so the usual case is one click or Enter. Scripts and file.open_in_slicer with a slicer still send without a dialog. The preference button that forgot the remembered slicer is gone: it could no longer make anything ask again, and it did not clear the pre-selection either. The guide says how it works now.
* Leave Bambu Studio out of the single-instance list (#2595)
Open files in a running slicer now adds --single-instance for PrusaSlicer, SuperSlicer and Cura only. Bambu Studio is started as it is, so it may open a second window. A test pins that, for the detected slicer and for one the user picked as a custom program. The guide and the preference tooltip no longer name Bambu Studio as getting the flag.
* Sort the English catalog and leave features.md to the generator (#2595)
cargo xtask lists wanted one message moved in en.json after the rebase. docs/features.md is written by the Generated files workflow on main, so the row I had added by hand is gone from this branch; the workflow adds Open in Slicer when it lands.
* Drop the unused folder message from the Vietnamese catalog (#2595)
Vietnamese arrived on main after I removed ui.save_mesh_temp_failed from the other catalogs, so the check that every translated id exists in English failed on it. Nothing uses the message any more.
* Say so when a started slicer can not be found again (#2595)
After Open in Slicer had started a slicer on its own, a failure to look it up again to send the file just returned, so the status line stayed on Starting and then nothing. It now says the slicer was started, that it can not be found to send the file, and where the file is saved. A test covers it.
* Say why a failed busctl ends the wait (#2595)
If busctl fails partway through a wait, the wait stops and the file is sent, the same as when busctl can not be run at all. That is on purpose: a file sent too early is dropped quietly and costs one more send. A comment in wait_for_ready says so.
* Keep the readiness wait inside its limit (#2595)
wait_until asked the probe before checking the deadline, including right after a sleep, so a yes could be accepted after the limit, and busctl ran with no timeout, so one look could outlast the whole wait. The deadline is checked first now, the probe is told how much time is left, and busctl is given at most two seconds (less near the end of a wait) and is killed after that. A hung busctl counts as not telling, which sends the file as before. This covers the first look on the UI thread too. Tests cover a slow probe, the shrinking time left, and a fake busctl that hangs, answers and fails.
* Send the file that was already written once the slicer is ready (#2595)
When the wait for a started slicer ended, the UI ran file.open_in_slicer a second time. That exported the design again on the UI thread, from whatever the session looked like by then, and could write a numbered copy while the timeout message still named the first file. The wait now keeps the slicer it found and the answer of the first call, and opens the file that was written. A test changes the design during the wait and shows the same file is sent, is not written again, no command runs, and no second file appears.
* Send with the single-instance flag after starting a slicer (#2595)
After Open in Slicer has started a launcher that needs a start first, the file has to reach the instance that is now running, so the send that follows a start carries --single-instance whatever the preference says. A normal send, to a slicer that was already up, still follows the preference. A comment in both places says why. The test uses a plain launcher command with the preference off: the normal send has no flag and the send after a start has it.
* Find the remembered slicer again after an uninstall (#2595)
With two installs the remembered key is prusa#prusaslicer. Remove the other install and the lone one is plain prusa, which matched neither the exact key nor the plain-key fallback, so the picker fell back to the first slicer in the list, maybe a different one. The key is now also matched by the part before the #, so the same slicer is pre-selected. A test covers the lone install, a plain key with two installs, an exact key, and that another slicer is never picked.
* Say a second click did not send anything while a slicer starts (#2595)
While a started slicer is being waited for, another click showed Starting <the first slicer> even when it had picked another slicer or other bodies. It now says it is still waiting for that slicer and that nothing else was sent. The test checks the wording and that it names the slicer being waited for.
* Label macOS installs by their application (#2595)
An open -a command resolved its program to /usr/bin/open, so every application on a Mac was labelled system. Labels for open commands now come from the .app path: /Applications is system and a folder under home is shown as ~/Applications. A test with two PrusaSlicer.app bundles and a made-up home checks both labels and that the ids differ.
* Pick the same place for a program found twice every time (#2595)
When two desktop files, or a desktop file and PATH, pointed at one program, the first one read was kept and its file name went into the id, so the ids could change with the order the folders were read in even though nothing was installed or removed. The place that sorts first now speaks for the program. A test with two aliases and a second install lists them with the folders in both orders and gets the same ids and commands.
* Treat flatpak run spelled two ways as one install (#2595)
flatpak run ... and /path/to/flatpak run ... with the same arguments had different install keys, so one invocation could be listed twice. The launcher is now compared by its file name and the arguments are kept, so both give the same key, while a different branch is still its own install. The test uses a real launcher file so the full-path entry is not skipped as stale.
* Say plainly that a late yes is accepted on purpose (#2595)
The comment on wait_until said a slow probe could not accept a yes that came too late. That was not what the code does: no probe is started after the limit, but one already running may finish and its yes is taken, because the file should still go. The comment now says so, with the bound: the limit plus that last probe, which in production is given the time left and so ends by the limit. A test has a probe that runs past the limit and says yes, and checks it is accepted and the wait stays within limit plus the probe time, and that a probe that says no is not followed by another.
* Give reading busctl the same deadline as running it (#2595)
After busctl exited, the output was read with a join that could wait forever if a process it left behind kept the pipe open. The reader now hands the text back over a channel and the wait uses the time that is left of the same limit, and gives up with nothing otherwise. The polling sleep no longer overshoots the time left either. A test uses a fake busctl that backgrounds a sleep with its output inherited and exits at once: the call returns within its limit.
* Tell two different flatpak launchers apart (#2595)
Comparing only the file name of the launcher made /opt/tool-a/flatpak run ... and /usr/bin/flatpak run ... with the same arguments one install even when they are different programs. The launcher is now found the way any program is, through PATH and with links followed, and its name stands in only where it can not be found. The test has flatpak on PATH and the same file by its path as one install, a different executable of the same name as a second, a different branch as a third, and the fallback when there is no PATH.
* Keep waiting when busctl is slow, send only when it is missing or fails (#2595)
Since the busctl timeout, a slow busctl came back as "can not tell", and that was read as send now. One slow look, likely just when a cold PrusaSlicer 3.0 is starting under load, ended the wait and sent the file early, which gets dropped. The look now says why it has nothing: slow means not up yet, so the wait goes on to its limit and then reports that the slicer did not respond; a missing or failing busctl still means it can not be told and the file is sent. The comments name only those two cases. Tests use fake busctl programs: one that hangs keeps the wait going to the limit and times out, a missing one sends at once, one that says not up and then fails sends, and one that names the slicer is ready.
* Do not open an empty path when the answer had no file (#2595)
If the answer kept for a started slicer had no path, the send ran open with an empty string. The tick now says there is no file to send and starts nothing. The test gives a wait with no path, checks the error line, and that the fake slicer was never run.
* Drop the unused folder message from the Danish catalog (#2595)
Danish arrived on main with ui.save_mesh_temp_failed, which this branch removed from English because nothing uses it any more, so the check that every translated id exists in English failed. The entry is gone from the Danish catalog too.
* Always start the 3.0 launcher with the single-instance flag (#2595)
A hand run showed that starting the PrusaSlicer 3.0 launcher with no file while a 3.0 is already running forwards to it and exits when the flag is there, but opens a second full window when it is not. A slow first look at the bus can start the launcher twice, and with the preference off the flag was stripped. A slicer that has to be started first now keeps the flag on that start whatever the preference says, the same reasoning as the send after a start; when nothing runs the flag does no harm. A normal send with the preference off still has none. The test covers the launcher entry, a bare launcher command, a normal send and a slicer that is not on the table.
* Swap --single-instance-on-url where it does not hand the file over (#2595)
A hand run on the PrusaSlicer 2.9.6 Flatpak showed that its entry's --single-instance-on-url does not forward a file to the running instance: every send opened a new one, three after a cold start and two sends. With plain --single-instance the second process finds the first, forwards, and exits. So when a command carries --single-instance-on-url and the slicer is not on the wait table, that is, not the 3.0 launcher, it is replaced by --single-instance, with the file markers handled as before, when the preference is on, and stripped when it is off. On the 3.0 launcher the flag does forward and stays. The comment on the rule says why. A test uses the real 2.9.6 and 3.0 Flatpak command lines.
* Cut long slicer names to fit the picker (#2595)
With installs listed on their own, a name like PrusaSlicer (Flatpak, master) is wider than its row. The list painted it with no limit, so it ran out of the popup, and the closed selector wrapped it onto a second line. The list now lays the name out on one line that ends in an ellipsis when it does not fit, the selector truncates, and the full name is in the tooltip when it was cut (for a program the user added, ahead of the delete hint). A test checks that a long name is cut to one line within the width and a short one is left alone.
* Keep the 3.0 launcher's flag on a send whatever the preference says (#2595)
A hand run with the single-instance preference off showed the 3.0 launcher needs its flag on a send too, not only on a start: a cold start with a file and no flag loaded nothing, and a send to a running 3.0 left a launcher process that never forwarded. A slicer on the wait table now gets the flag on every launch, as it already did when started alone. The doc comment says why. The preference text, in English, Japanese and Chinese, and the guide now say that PrusaSlicer 3.0 is the exception to turning this off. The tests that took the flag away with the preference off now expect it, for the launcher entry, a bare launcher command and the 3.0 Flatpak line; the test of the send in the UI checks the first send carries it.
* Keep the install-flag swap to PrusaSlicer, found by what it is (#2595)
The swap of --single-instance-on-url for --single-instance ran for any program whose command had the flag, so another program with that flag would have been rewritten. It is now a PrusaSlicer rule: it applies to a slicer recognised as PrusaSlicer by its identity, its id or, for a program the user picked, the name it goes by, never by a version number. The single-instance table uses the same identity check. A test keeps the flag of a program that is not PrusaSlicer, strips it when the preference is off, and swaps it for a picked program named PrusaSlicer.
* Do not promise a hard bound in the wait comment (#2595)
The comment on wait_until said the production probe ends by the limit. Its clock starts after the process is spawned, and killing and reaping the process and scheduling are not bounded by it, so it is a polling and read budget with process overhead on top. The comment says that. Comment only.
* Tell PrusaSlicer 2 from 3 by the installed version (#2595)
PrusaSlicer 2 and 3 need different handling, and the rules had to guess from the command line. Each install of PrusaSlicer now carries its major version, read when it is detected from the release at the top of its appstream metadata and never by running the program: a Flatpak's own files (the branch the command names, else the current one), or the metainfo file beside a desktop entry. PrusaSlicer older than 3 is sent the file with --single-instance in place of --single-instance-on-url, which does not hand a file over there, and is not waited for. PrusaSlicer 3 and newer keeps the launcher's flag, is started on its own, waited for, and sent the file. When the version can not be read, the command decides as before: the slic3r-app-launcher launcher counts as 3, and any other entry is left as it is. A program that is not PrusaSlicer is untouched whatever its flags. Tests use metadata fixtures, no Flatpak: 2.9.6, 3.0.0-alpha10, a branch that is not installed, a system package, an unreadable version with and without the launcher, and a non-PrusaSlicer program with the same flag.
* Make the busctl wait tests steady under load (#2595)
One of them failed once in a full run at its first assertion and passed alone. A script that has only just been written can fail to start (text file busy) while another test forks, and that reads as a busctl that is missing. The fake programs are now started once, patiently, with a flag they answer at once, before a test relies on them; a hanging one uses exec so it is killed cleanly. The time bounds that could trip on a busy machine are several seconds now, and the checks are on outcomes: not ready and keeps waiting, send now, and the number of looks. Thirty runs alone, thirty of the whole slicers filter and thirty of the busctl tests under eight busy loops all pass.
* Say what the busctl reader thread leaves behind (#2595)
The thread that reads busctl output is left to end when the pipe closes. A busctl that answered but left a process holding its stdout reads as slow, and every poll of such a bus can leave one blocked reader behind until that process lets go. A comment next to the thread says so. Comment only.
* Make Save as mesh a plain export (#2595)
Save as mesh asked for the bodies and the format, and could also send the file to a slicer with a checkbox and a slicer list. Sending now has its own command, so the checkbox, the list and the Send label are gone from this dialog: bodies, format, a file dialog, Save. The two settings that only the checkbox used, save_mesh_send and save_mesh_slicer, are no longer kept; a settings file that still has them loads, and they are ignored. The strings only they used are gone from the catalogs. The slicer list stays in the dialog of Open in Slicer. file.save_mesh keeps its slicer parameter for scripts. The guide describes the two as separate.
* Rename Open in Slicer to Send to Slicer (#2595)
The command sends a file to a slicer rather than opening one, and a later Send to App can share the name. File menu, Mesh tab, right-click menus, the dialog button, status lines, the guide and the English, Japanese and Chinese strings now say Send to Slicer, and the command id file.open_in_slicer is file.send_to_slicer. It is new in this change, so there is no alias. The remembered slicer is stored as send_slicer, and the question the agent chat asks says send to. Module and type names stay as they are.
* Put the send targets in one table and one send pipeline (#2595)
The known slicers, the list of slicers with a single-instance flag and the rule for slicers that must be started and waited for were three separate things. They are one table of targets now: id, label, kind, names and program files, accepted formats and the default, how the file is handed over, the single-instance flag, the version from which PrusaSlicer's on-url flag hands a file over, and the wait-for-ready rule. Programs that are not in the table are slicers with the same defaults. The kind is slicer, the only one so far; its comment says it is where a later Send to App would plug in. The send itself is one function: write the bodies, find the target, start it and wait if its rule says so, hand the file over, report. Detection and resolution take a kind. Nothing changes for what already worked.
file.save_mesh keeps its slicer parameter, now through the same function, with its export arguments and result unchanged: a cold PrusaSlicer 3 is started and waited for before the file goes, and it accepts the program path of a detected slicer as file.slicers lists it (file.slicers gives it as program). A path is accepted only when it is the program of exactly one detected target, and that target's own command and arguments run; a launcher several entries share, such as /usr/bin/flatpak, is refused as ambiguous, and any path nobody listed or chose is refused. The parameter and the code say it may be deprecated later in favour of file.send_to_slicer or a Send to App workflow, which is a possibility and not a plan.
Tests go through file.save_mesh itself for the two reported failures, a listed AppImage path and a cold PrusaSlicer 3 that is waited for, with the slicers and the busctl made up; and they check that the result and the file are what an export without a slicer gives, and that a path that is not listed, or is shared by two entries, writes nothing. The pipeline has its own tests: a cold launcher waited for, one already up, wait return, and one that never comes up. A test of save_mesh that shared a file name with another is given its own.
* Keep an AppImage's id through an upgrade (#2595)
When a slicer has several installs, each gets an id made from where it was found. For an AppImage that was its file name, which carries the version, so upgrading it changed the id and the remembered choice missed. The id is now made of the slicer's id, the fact that it is an AppImage and a short hash of its folder, so a new version that replaces the old one in the same folder keeps the id, and the same AppImage in another folder is its own install. Two versions kept side by side in one folder are two installs: while both are there, their file names are added to their ids to tell them apart.
A remembered id is found again only when it names exactly one install. An id that was one AppImage and is now two versions, or a plain id that is now several installs, is treated as not remembered, so the picker starts from the first entry as on a first run and does not quietly pick another install; a single install left of the same slicer is still found, as before. Installs that are not AppImages are told apart as before. An id from before this change no longer matches. Tests cover an upgrade, two versions side by side, a second folder, and the picker's side of each case.
* Add Open with EasyPrint to Send to Slicer
EasyPrint is Printables' slicer in the browser. It's a plain text row in
the Send to Slicer dialog, backed by a new file.open_in_easyprint
command. The command exports the bodies as a 3MF (or STL), hands them to
the host to upload to slicer.printables.com/import, and the app opens the
link that comes back. The desktop app uploads with ureq and the browser
build with fetch, so this is the one send target that works on the web.
The model leaves the machine, so the command refuses without
consent: true, the dialog asks once and remembers, and an AI agent's
request asks first. A model over 100 MB is refused before anything is
sent, and a failed upload shows as an error with the reason.
The chat's privacy note said SolidCraft sends nothing anywhere, which
isn't true any more, so it now says that about the chat. New strings are
in every catalog.
* Place Send to Slicer on the Insert Files panel
* claim: #6170
* Move: after picking a body, go on to From, Point or Axis (#6170)
In Point to Point, Point to Position and Rotate, once a body is picked
and the next input is empty, the dialog goes on to it, so the next click
picks the point or axis instead of toggling another body. Free Move
stays on Bodies: its Pivot is optional.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6070
* Sketch: a changed length dimension keeps its start point, so a side grows from its first corner (#6070)
Changing a dimension's value re-solved its sketch as the smallest move from
the stored positions, which splits the change between both ends: a
rectangle side set from 40 to 60 mm grew 10 mm each way. parameters.change
now first solves a sketch whose length dimension (distance, horizontal or
vertical distance, line length) the parameter drives with that
dimension's first point held, and stores the result, so evaluation has
nothing left to move. Where holding it can't solve (a symmetry moves both
ends), evaluation solves as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5099
* Browser: a feature's eye shows or hides the bodies it made (#5099)
A row in the Browser's Features folder has an eye when the feature made
bodies that are in the design now (a patch, a box, a pattern's copies;
not a fillet, which changes bodies). It runs browser.visibility with the
new features: [id or name] (and path: for one placement of a component
placed more than once), which hides or shows those bodies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A path a design names (a linked design, a sketch text's font file) now
goes through one rule before any file call: a network location is
reached only once its server is allowed, a device path never. Update
Links and Open Linked Design ask first (Allow once / Always allow this
server / Cancel); Preferences > Design lists the allowed servers and
can allow every network location. component.links reports such a
link's found as unknown with a reason, without looking for it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A homemade 3D input device's driver app (MAKMOUSE) listens on a loopback
port; with Preferences > Navigation > "Navigate with a device app on this
computer" on (off by default), the desktop app connects, says hello and
reads JSON lines: axis motion (orbit, pan or zoom by mode), zoom steps and
standard views. The motion goes through navigation::device like a 3D
mouse's, only in the focused window; a HID device the app names is left
to it. Documented in docs/3d-mouse.md.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat: add personal Hole parameter presets
* feat(hole): prompt for preset names and recall changed selections
Ask for a name only on Save new or Rename in the kit confirmation. Keep Enter and Escape inside the prompt, preserve cancelled drafts, recall only changed selections, and retain independent recessed-hole bottom choices.
* feat(hole): preserve thread direction and partial depth in presets
Carry Left/Partial, explicit source units and Custom draft guards through personal preset capture and recall. Preserve legacy Right/Full/flat defaults and reject unknown options before changing the form. This production bridge depends on the handedness and graphical-depth changes inherited below it.
* docs(hole): describe supported preset thread options
* feat(ui): save modified hole presets in place
* fix(presets): retain reference placement and current locale contracts
* test(ui): borrow restored preset library during serialization
* test(presets): preserve modal focus with viewport focus restoration
* Use UI kit controls for hole preset selection
* Themes: Clay (light) and Clay Dark, warm paper and charcoal with a terracotta accent
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Clay: on-accent text a touch lighter for 4.5:1 on the accent
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6724
* Extrude: the picked region is the one extruded again (#6724)
Since #5530 (#5302) every index or point pick grew to the whole drawn
profile that reference geometry cuts, so a lens or crescent clicked in
Extrude, where the face's projected outline cuts a circle, extruded the
whole circle while the dialog highlighted and counted one region.
Dialogs now send exact_regions: true with their profile picks, and the
command keeps each pick to the region it names. Picks without the flag
(scripts, agents, the CLI) still take the whole drawn profile (#5302).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5345
* Select Other: list what is under the pointer front to back, one body at a time (#5345)
The list was sorted by kind first, so on stacked bodies it showed every
body's face, then every body, mixed across depth. Now each body's rows
stay together (vertices, edges, face, then the body), bodies ordered by
their nearest hit along the pick ray; an edge counts at its point under
the pointer. Sketch overlays stay first, profiles, planes and axes last.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4784
* Part properties: one table of keys, each with its scope, check and label (#4784)
SolidCraft's part-property keys (project, part number, revision, author,
organization, description, licence) were declared once but listed again
by hand in each command, error, dialog and window. They are now one
table, metadata::KEYS: each row has the key, where it may be set
(Scope: the design, a component or both), how its value is checked and
the message id of its label. A new key is one row.
Metadata::set is told whose metadata it changes (Owner::Design or
Owner::Component), and a key is taken where its scope covers that
owner. is_well_known, both set commands, document.metadata,
component.metadata, the refusals, the Design Properties dialog and the
Bill of Materials window read the table; the hand-written lists are
gone (WELL_KNOWN, the label match whose fallback named any unknown key
"License", the window's three labels). metadata::BOM_KEYS names, in
column order, the keys a bill of materials lists for each part:
component.metadata returns them, component.metadata.set takes each as a
parameter of its own and the window edits them; a test keeps each one a
row of the table that a component takes. Free properties use the keys'
text rule (metadata::check_text) instead of a copy of it. The params
docs point to document.metadata's properties instead of repeating the
keys.
Every key accepted before still is, on the design and on a component.
Two things change:
- a refused key or a full metadata on a component says "part property"
("`bogus` is not a part property: …", "part properties are limited
to …"), not "design property";
- changing the design's own properties no longer evaluates the model.
The fields that change no geometry are named once,
Document::with_part_properties_of: the design's metadata, and each
component's metadata and free properties. It compares them first and
copies the design only when one differs, as before.
The shortcut is for a session's own edits only. A command that gives
the session another design (doc.open, file.new, doc.recover: through
Session::replace_design, which now counts them) is refreshed in full,
however little the new design differs from the one before, so its
joints are solved and its occurrence state pruned. Before, opening a
design that differed from the open one only in render settings or
component properties skipped that; with the design's properties in the
list it would have for two revisions of one part too.
Tests: the table (one row per key, the dialog's order); the bill of
materials' keys (rows a component takes, in column order); each scope
on each owner, and refusals that name the owner (the empty key, an
unknown key, a 256-byte key shown cut); the text check at 4096 bytes,
line breaks, tabs and control characters; hostile keys and values
refused without a change; only-part-property changes in each scope,
and changes beside them (units, a component's name, an added, removed
or swapped component); document.metadata.set, remove, undo and redo
evaluate nothing; opening a revision that differs only in its author
solves its joint; the component wording, the shortcuts and the limits;
the dialog's and the window's labels from the table.
What the tests can't reach: every row is Scope::Both, so a known key
refused for its owner (the wrong-owner branch of check) can't happen
yet. It becomes live with the first single-scope row, bom_type, a
component-only key.
Left for later (not this topic):
- Metadata::others() ignores scope, so a component-only key stored on
the design would be neither a dialog field nor listed under other
tools' data. It matters from the first single-scope key (bom_type).
- assembly.bom's rows, its CSV columns and the window's grid headers
still name the three keys by hand; the bill of materials module's
rewrite will read BOM_KEYS.
- The bill of materials types (P1, #5107, into drawings) still use
WELL_KNOWN and the two-argument set: once this is on main, bom_type
is rebuilt on it as one KEYS row with Scope::Component, set through
Owner::Component.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5497
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Sketch pattern: a spacing arrow dragged past zero gives a negative spacing (#5497)
The drag arrow floored an unsigned value at a small positive number, and the
rectangular sketch pattern's two spacing arrows were unsigned, although the
command takes a negative spacing and typing one works. Both arrows are signed
now, like the Extrude, Move and offset ones.
The test drags each arrow back past its start: without the change the first
one stops at 0.001.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Drag arrows: the solid pattern spacing and one-sided sketch offset go negative too (#5497)
The solid rectangular pattern's Spacing arrow and the sketch Offset distance
arrow were unsigned, so a drag back past the start stopped at a small positive
value, although both commands take a negative value and typing one works. Both
are signed now. A symmetric offset uses only the distance's size, so its arrow
still stops at the curve.
Tests: the solid pattern arrow dragged 10 mm back gives Spacing -10 and the
copies on the negative side (without the change: 0.001); the offset arrow
dragged across its circle gives -1 mm and the same circle as typing -1 mm
(without the change: 0.001), and a symmetric offset stays positive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Release: sign the Windows binaries and MSI through SignPath Foundation
When the SIGNPATH_API_TOKEN secret exists, each Windows job sends the
three .exe files to SignPath before packaging, then the MSI built from
them, using the GitHub artifact flow from SignPath's docs. The policy
is test-signing unless the signpath_signing_policy dispatch input or
the SIGNPATH_SIGNING_POLICY repository variable says otherwise. Without
the secret the jobs build unsigned exactly as before.
package.ps1 gains -Phase stage|package so signing can sit between the
build and the MSI; the artifact configuration to paste into SignPath is
packaging/windows/signpath-artifact-configuration.xml.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* SignPath: drop the description attributes the Foundation plan doesn't allow
SignPath Foundation projects get an automatic signature description (its watermark);
the artifact configuration may not override it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Double-click a shown sketch dimension in the model view to edit its value (#7079)
A finished sketch's dimensions shown in the model view (Show Dimension) now take a
double-click: the same inline value box as sketch editing opens at the text, Enter
changes the dimension's own expression through parameters.change (one undo step), Esc
cancels. The sketch is not edited and the view does not move. Driven dimensions stay
read-only with a status hint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Control channel: ui.selection lists the shown dimensions of finished sketches
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test: a shown dimension that breaks a later fillet reports it, and Undo restores both
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #1457
* Agent chat: tool calls read as summaries, raw JSON behind Details (#1457)
Each tool-call card now says what the call did, built from the command's
translated label, its main parameters (lengths in the design's units) and the
bodies the result names: "Extrude Sketch1 by 10 mm", "Box 40 x 30 x 20 mm,
then Fillet 4 edges (r 4 mm)", "Measured Block: ...", "Saved block.solidcraft".
Calls it can't put in words keep the agent's title. The raw arguments (now
kept from the in-app client and ACP's rawInput) and the result sit under a
folded "Details".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Agent chat cards: show file names, never paths; temp files not named
ui.screenshot reads "Screenshot"; save/export/open and other tools' titles
show only the file name. The full text stays under Details and in the tooltip.
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>
* claim: #5681
* sketch: exact circular arcs on drawn 3D wires (#5681)
Add WireArc (centre, unit normal, radius, start/end) as an optional analytic
form on drawn 3D sketch wires, with arc_from_3_points, arc_sweep and
sagitta-bounded arc_polyline; add_drawn_arc_in stores the arc, its three
defining fit points and its samples; move_drawn_point re-derives the arc
atomically and refuses collinear points; validate_wires checks saved arcs.
* engine,ui: sketch.arc3d exact 3D arc command (#5681)
sketch.arc3d draws an exact circular arc through three world points in a 3D
sketch, stored analytically beside its fit points and samples; sketch.move3d
rebuilds it, sketch.inspect reports centre/radius/sweep; placement mapping
(map_wire, now shared with the engine's world state) transforms the arc.
One pending design-format file records the new field. The viewport's 3-Point
Arc tool draws a 3D arc when clicked off the sketch plane, with a live arc
preview; the SKETCH panel offers sketch.arc3d.
* ui,engine: 3D Arc button starts an interactive tool (#5681)
sketch.arc3d now maps to a Draw(3) tool handled by sketch3d: three world clicks (start, end, through - same order as the 2D arc tool) run the command, with the live arc preview and snapping. The command is only enabled while a 3D sketch is open, so the toolbar never offers a tool that could only fail. Locale labels for the command.
* ui: cargo fmt
---------
Co-authored-by: RayGalt <RayGalt@pm.me>
* claim: #1383
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport tests: the hover pixel test keeps its pictures when it fails (#1383)
The Metal hover pixel test failed once in a loaded full-suite run and has
not been reproduced since. On failure it now saves the plain and hovered
GPU and CPU frames and a mask of the pixels that do not light, and names
the folder in its message. The assertion is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4384
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: shared module, with Rest as the first tool
The Rest handles spike becomes a shared module, place_gizmo, so other features placed on a face can
use the same marker and knob. The mechanics (crosshair with its three states, the grab offset, the
footprint while dragging, the turn knob) live in place_gizmo.rs. The handles own the pointer through
canvas::owns_pointer, as the move triad does, so navigation and Select Other's press-and-hold leave
them alone. Each tool is one file under place_gizmo/ that gives a Spec: which dialogs it serves,
which commands' Position input also takes sketch points, where the place is, how the pointer
re-places it, its footprint and its turn, if any. The module lists the tools one per line, so tools
added on separate branches each add one `mod` line and one list line. Rest is the first.
The handles are always on; the preference the Rest spike had is gone. The POINTS accept flag on a
Position input is granted from the tools' Specs instead of a Rest-only arm in the plastic dialog's
start. Handles are published as `rest:position` and `rest:rotate` as before.
Seen straight down, the dialog's height arrow collapses onto the place and its value box sits just
right of it, where it took the press meant for a knob pointing that way. The knob now slides out
along its ray until it is clear of the box.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Placement handles: Boss
The marker moves a boss across the model, also from Edit Feature, and its Position input takes
sketch points while the handles are on, so a screw boss can be dragged onto the centre of a
circle drawn for it. A boss is round, so it gets no knob; the footprint while dragging is its
outer circle. Published as `boss:position`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Placement handles: Snap Fit
The marker moves a snap fit across the model and its Position input takes sketch points while
the handles are on, so the arm can be put where a sketch says (#3423). The knob points the
catch: it turns between the four directions the Catch Direction combo offers for the face, so
the knob and the combo always agree; the command keeps only those four, so there is no free
turn. The footprint while dragging is the arm's foot, its thickness behind the place and its
width across. Published as `snap_fit:position` and `snap_fit:rotate`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Placement handles: the look in one file
Everything that draws the handles (the crosshair and its three eased states, the knob's ball, the
cased lines, the angle label, the colours) moves from place_gizmo.rs into place_gizmo/look.rs, so a
restyle touches one file and the mechanics (hit radius, drags, knob geometry) stay where they are.
No change in behaviour.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: behind a Design preference, off by default
The handles sit behind Preferences > Design > Placement > Placement handles (`placement_handles`,
also through `ui.prefs`), off by default as the hole placement work in #2912 is: a Rest, Boss or
Snap Fit is then placed by its click alone, its Position input takes faces only, and no handle is
drawn or published. The tests switch the preference on; one checks the default.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: the tests hide the origin through set_origin_shown, as main does since #1849
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: the move marker as designed in the prototype, drawn with theme tokens
The marker is a dot with four arms to the screen cardinals: short and dotted at rest, longer with open arrowheads when pointed at, solid while dragging, each state growing out of the previous one (360 ms). Sizes are the prototype's. Its core is the icon highlight and its casing the marker halo, the tokens main's hole handle uses, so the literal allowance goes. The tip shows while pointing, not during a drag.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: the dialog's drag arrow stands above the move marker
With the handles on, the Height arrow started at the marker's centre and ran through its upper arm, head on arm tip. The arrow now keeps the marker's full reach and a gap free at the base, its shortest length grows by as much, and the base dot is left to the marker. Other dialogs are unchanged (clearance zero).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: a turn ring on the face replaces the knob
The ring lies on the face about the move marker's centre, drawn in perspective, a fixed 65 px on screen like the move triad's arrows (it is a control of something on the screen, not a measure of the model). At rest it is a short dotted arc with an open chevron grip just outside; pointed at, the dotted circle opens from the grip both ways round and the grip slides in and fills; while turning, a solid arc around the grip. Dragging the grip turns the feature as the knob did, through the same handle name, so the harness tests stand.
The floating value box yields: with a handle's centre under it, it moves to the other side of the arrow's tip or above it, since seen straight down the box sat exactly where the grip is. A handle the arrow's stem crosses keeps the pointer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: a compass while turning, and the turn pulled onto the face's compass points
Four ticks just outside the ring at the face's in-face world directions (the ones the Snap Fit combo names, +X, −Y, …) appear while turning and stay put, so you can see how far you have gone. A Rest's turn is pulled onto one of them within 8°, otherwise it steps by 5° as before; Alt or Ctrl frees it. The readout beside the grip names the point it sits on ("90° · +X") and keeps clear of the ring.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: the footprint in the preview's blue, the idle handle steps back
The footprint drawn while the marker drags is a filled shape in the preview's own blue with a thin darker edge, not a white outline, so white stays reserved for what can be grabbed. While one handle is dragged the other fades to a quarter and comes back when the drag ends.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: the ring in the preference text, the docs and the tests' wording
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: the look as tuned in the app
Marker: drawn to the redrawn design (dot r 3.9 pt, two dashes per arm, a filled head 8.7 × 7.1 pt whose base is where the outer dash began); pointed at, the outer dash becomes the head (its far end narrows to a point, its near end widens); dragging runs the inner dash on into the head as a shaft. Every casing is 0.4 pt outside the edge, for lines, heads and the dot alike.
Ring: the grip and the compass lie in the ring's plane like the circle, so the grip is never skewed against it; the grip is a thick chevron with a hole that flattens into the base as the grip slides onto the ring, and fills; the dotted circle stays while turning, with the solid arc laid over it; opening eases in and takes 0.3 s, leaving is a 0.15 s fade over the resting ring, not the sweep backwards; a drag keeps the angular gap of the press, so the grip does not jump onto the pointer; the compass ticks and the readout's point name are in their axis colours, and the grip takes the axis colour (eased) while aligned with one, with that point's tick left out; free turning keeps a hair of pull (0.5°) so an axis can still be hit; the readout wraps to a half turn.
Value box: with a ring it keeps out of the circle the ring, grip, compass and readout sweep, one place per view, nothing moves when a turn starts; also when the value does not evaluate and there is no arrow.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: the readout shows a half turn as 180°, not −180°
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: the motions in one place
The animations the handles grew (a factor eased in and out each way on its own time and easing, a handle stepping back while the other works, a colour that comes and goes from the last one it was on, the gap a press keeps for its drag, an offset that slides instead of jumping) move out of place_gizmo into motion.rs, so the next manipulator reuses them. The look's timings become Transitions in look.rs. No change in behaviour: the contact sheet before and after is identical, the harness tests stand.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Placement handles: the axis colours as main names them since #4430
The compass and the aligned grip take the one set of X, Y and Z colours (axis_x, axis_y, axis_z); the Move gizmo's own array is gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* claim: #3133
* Apply complete feature transactions off the window thread
Evaluate dependent history and prepare reference and GPU display caches before accepting a guarded result. Preserve the evaluated original model for safe editor cancellation and journal only accepted commands.
* fix(ui): preserve original marker across sketch-to-feature edits
* test(ui): verify edit-sketch cleanup and guard Unix link fixture
* test(ui): fix texture disposal and unreachable assertion in feature tests
* fix(build): compile CLI link inspection only on Unix
While an agent's batch computes off the window thread (#1989), the chip reads
N/M · <command>. It is centred on the navigation bar, just above it, and moves up
past any toast or panel it would cover.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Axes and points from circular edges and round faces
In 3D, a round edge or face stands for its circle wherever a command
takes an axis or a point.
Revolve, Surface Revolve, both Circular Pattern dialogs, Plane at Angle
and Move -> Rotate take a circular edge (through its centre, along its
normal) or a cylinder, cone or torus face (its axis). The features keep
the pick as a model reference, so they follow the model when a parameter
moves it. Straight edges work as before; flat faces are refused.
Every 3D point input takes a circle's centre: construction points (which
follow the circle), To Object, patch points, hole placement, and Move's
pivot and points. Hovering shows the axis as a dashed construction line
or the centre as a star, and the dialog's chip names what the pick
stands for ("centre of edge 10", "axis of face 3").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format the round-pick hover check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Selection chips: name a round pick as a circle centre or axis
The chip has room for little more than the body and a word or two:
"Shaft · circle centre", "Shaft · circle axis", "Shaft · face 2 axis",
and "Shaft · point" for a point of the model.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move: a circle's centre in a placed component is where the component shows it
Move's pivot and points read a picked circle from the model in its
component's own frame, while the pick is in the world. They now read it
from the world state (and, editing a Move, from the state before it,
placed the same way), as the viewport and the other round picks do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Plane at Angle on a planar face about a model edge: test that it follows both
Upstream #1956 made a planar face a base for Plane at Angle; this branch makes a model edge an
axis. The rebase keeps both in one PlaneRef::AtAngle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #1397
* External STEP corpus: nightly import checks on real-world files (#1397)
tests/external-corpus.toml pins 20 real-world NIST PMI STEP files (AP203
geometry-only and AP242 editions from several vendor exporters, public
domain) by archive and per-file SHA-256 with the licence recorded.
cargo xtask external-corpus downloads each source once into the gitignored
corpus/external/, verifies every hash, imports each file headless
(file.insert_step + inspect.measure) and checks body count, total volume
(0.1 %) and area (0.5 %) within tolerances and time and memory budgets.
A file whose upstream hash changed is reported as 'source changed', never
as our failure. --bless "why" re-records expectations in the manifest with
a dated reason. The External corpus workflow runs it nightly.
assets: skip corpus/ when scanning for attribution (downloaded case data
is never committed).
---------
Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
The blue, green, red scale's lightness rose and then fell (L* 44 up to
72 and back to 50), so a flat face and a tight radius looked alike with
hue lost, and its upper part merged for a red-green loss. The stops are
now dark blue, a middle grey-gold and light yellow in one straight
blend, as measured in the issue: L* rises the whole way and every
quarter step stays clear for normal, protan, deutan and tritan vision.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Frames: a Weldments ribbon tab (#4003)
- WELDMENTS joins the ribbon after SHEET METAL and is built the same way:
Sketch (New Sketch), Create (Frame Member) and Configure (Parameters,
Configurations). Frame Member's home moves there from MODEL > Create.
- Always shown, as a frame starts from a sketch of its lines. It selects
itself and takes the contextual style when the selected body (or the last
body of the active component) is a frame member; sheet metal comes first.
- It uses the default accent until the palette has a colour for it.
- The tab label is in every catalog; docs/ribbon-layout.md, docs/frames.md
and the tab list in docs/control-protocol.md follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frames: Weldments keeps the ordinary tab look (#4003)
Weldments still selects itself when a frame member is the active body, but
no longer takes the solid contextual fill or the 2 px accent line under the
tab row: selected, it looks like a selected Model tab. Only Sketch and Sheet
Metal (on a sheet body) are contextual.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Expressions: a number and its unit are one value (10 mm / 2 mm is 5)
A unit written after a number was multiplied in at the precedence of `*` and
`/`, so `10 mm / 2 mm` was (10 mm / 2) mm = 5 mm² and `90 deg / 1 rad` a value
in angle², reported as a unit error. The number now takes the unit after it
before `*` and `/` apply; a fraction of numbers before a unit stays one value
(`1/2 in` is half an inch), and an exponent stays a plain number (`2^3 mm`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Expressions: after a division, a fraction before a unit only when closed up
`width / 10 / 2 mm` read the `10 / 2 mm` as the fraction 5 mm and gave
width / (5 mm) = 8 instead of (width / 10) / 2 mm = 2. After a `/` the
fraction rule now needs the fraction written without spaces (`1 in / 1/2 in`
is still 2); elsewhere `1/2 in` and `10 / 2 mm` are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
?open= now also accepts an absolute path on the page's own origin, fetched
with the page's scheme, so a self-hosted setup that serves SolidCraft and
other tools under one origin can link a design on a LAN without a public
HTTPS address. Absolute URLs keep the HTTPS-only rule (a URL on the page's
own origin is also accepted), redirects must end on HTTPS or the same
origin, and protocol-relative, dot-segment, backslash and control-character
values are refused.
* Patterns: one before the feature it copies follows that feature's parameters (#6726)
A pattern or mirror of features made with the timeline rolled back comes
before its sources and replays them from their definitions. Its
fingerprint chained only the features before it and its own inputs, so a
change of a source's parameter left its result in place: the box grew,
its copy kept the old length, also after rolling forward.
The chain fingerprint of a pattern or mirror of features now also takes
the fingerprint of each feature it replays (and of their sources, for a
pattern of a pattern), wherever they are in the timeline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Patterns: a test for a later feature taking a deleted source's name (#6726)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Patterns: fingerprint the replayed sources with the document, as fingerprint now takes it (#6726)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3538
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Updated AppImage takes the release's file name (#3538)
A versioned AppImage name (solidcraft-<version>-linux-<arch>.AppImage) is
replaced by the new release's name, so zsync-based tools keep finding the
update data. Other names are kept.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
A sketch dimension read Ø40.00, with nothing to tell millimetres from
inches, while the value inputs and the pointer's coordinates show the
unit. Length labels now end with the design's unit (Ø40.00 mm, 1.000 in,
(25.00 mm) for a driven one); angles keep their degree sign.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
AGENTS.md drops from 16.8k to 3.7k characters: the hard rules, each naming the
check that enforces it, and a table of what to read for each kind of work.
The rest moves into eleven new skills; the five existing ones stay.
cargo xtask skills fails when AGENTS.md or a SKILL.md cites a path, an xtask
command or a skill that does not exist, when the router table and skills/
disagree, or when the router or a new skill grows past its size cap. It runs
in cargo xtask ci and the repository checks job.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With a section shown, the marking pass decided where the cap shows (the stencil) and wrote the
depth everything after it tests against. #1768 gave it the faces' FACE_BIAS for the second job,
so edges draw over their own faces (#1434). For the first job its slope part is unfair: next to an
edge where a back face seen through the cut meets a front face, the front face won a band about a
pixel wide although the back face is nearer. The cap skipped that band, and the edge line showed
through the cut.
#6363 marked those samples again from the back faces, pulled toward the eye as far as FACE_BIAS
pushes. That closes the gaps beside the edges, but the pull also lets back faces win wherever they
lie within the push of a front face, along every body's far silhouettes, and the cap hatches that
band. On bherbruck's block, the band away from the cut went from 58 px orthographic and 534 px
perspective before #6363 to 618 and 780 with it. In the app, his filleted hub showed it in every
frame of a 36-step orbit in both projections.
This replaces that pass. While a section is shown the opaque model now draws in four steps (`Cut`):
1. mark: the faces by their true depth (no push): colour, the stencil and depth;
2. a triangle over the viewport sets the depth back to the far plane, colour and stencil kept;
3. push: the faces again with FACE_BIAS, depth only, so the depth is what unsectioned frames have
and edges draw over their faces as before;
4. cap: depth only, as #6728 made it; the mark pass has drawn the cap's colour and hatch, as fs_solid
draws a back face seen through the cut.
The window pass's depth reset and this one are one helper, `depth_reset_pipeline`, for either
depth direction, and its passes are built by #6728's `solid_writes`. Unsectioned frames are unchanged. The CPU rasterizer already chose the cap by
true depth.
Tests (crates/gpu/tests/depth.rs), with the device's float depth and the 24-bit fallback:
- a_cut_hides_the_edges_behind_it (new): bherbruck's block cut at y = -16, home view, orthographic
and perspective. No edge shows through the cap, and away from the cut the picture is a box of
just the kept part's: 0 px orthographic and 1 perspective (main: 618 and 780).
- bherbruck's pocket and hub tests from #6363 are kept and pass (pocket 0 px; hub 1 and 2 px over
its twelve views, as with #6363's pass).
- an_edge_on_a_face_draws_whole_at_any_angle and
an_edge_just_behind_a_face_stays_hidden_on_a_large_design also run with a section shown.
In the app (safe mode, WARP), with 625 patterned cylinders cut by a section, the frame time is
that of main (#6363): 33.5 against 34.3 ms at the home view, 34.1 against 35.2 ms with the model
filling the view.
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6720
* Face pick by point: linear work budget, so bodies with many faces take a sketch (#6720)
A face picked by its point alone charged the whole mesh once per face, and
face_contains scanned every triangle for each candidate. A block with a
modelled thread (about 5 000 faces) went past the 2 000 000 limit. The
triangles are grouped by face once, each face's test reads only its own,
and the budget charges triangles + faces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #3812
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
* Sketch Dimension: value box in the design's units (#3812)
A dimension added without a value restated what it measured as
"<value> mm", whatever the design's units. In an inch design the
Dimension tool's value box therefore opened at "25.4 mm" next to a
dimension whose text read 1.000 (inches).
Document::design_length writes a measured length in the design's units
("1 in"), with enough decimals to keep it to the micrometre, so a
dimension that restates the geometry still moves nothing. The Dimension
tool, AutoConstrain's dimensions and Toggle Driven (making a driven
dimension driving) use it. Millimetre designs are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
* Move the design-units dimension tests into sketch_ux_tests (review)
The #3812 tests go in the existing sketch dimension test file rather than
a new file of their own (AGENTS.md, #3226).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The evaluation cache's 64 MB budget charged every cached feature result the whole model, though a
state shares every body it did not change with the state before it. A design whose model is a few
MB kept only its last few dozen results, so setting a parameter back recomputed what had dropped
out. Each result is now charged for the bodies and sketches it does not share with the state it
started from.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6550
* Crash reports: keep journaling through file.new and doc.open (#6550)
`Session::replace_design` swapped in the fresh session without its
`crash_journal` flag, and the app turns it back on only at its next frame.
So `file.new`/`doc.open` never recorded how they ended, the commands sent
after them in the same frame (a control-channel or MCP batch) were not
journaled, and a crash report then named `file.new` as the command in
flight. `replace_design` now keeps the flag, like the other session-wide
fields it keeps; only the design on screen has it set.
Test: `commands_right_after_a_new_design_are_journaled` (crash_tests.rs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6917
* Tests: the six timing assertions do not depend on the wall clock (#6917)
Six tests asserted on the wall clock. Their result depended on how loaded
the machine was and not only on the code under test: they passed on an idle
machine and failed when the test binaries ran beside each other, which is how
the suite runs (`cargo xtask test` starts every test binary at once).
CONTRIBUTING.md:196 says it: "Tests run beside each other on a busy machine,
so a test must not depend on wall-clock timing."
The change is confined to the six test files the issue names:
- geom: the tangent chain of a helix is a ratio (750 pieces against 3 000)
instead of a limit of 200 ms.
- io: the placement budget is spent inside the reader, so the clock is gone.
- ui-egui: the classification of many faces is a ratio (900 against 3 600)
instead of a limit of 10 s.
- engine: the clearance measurement drops its 30 s limit; the boolean that
gives up reports itself.
- engine: the fillet beside a modelled thread is a ratio (a 1 mm thread
against an 8 mm one) instead of a limit of 30 s.
- ui-egui: the window frame budget goes from 250 ms to 20 s, the limit #6072
gave its own probe.
The helper is the one #649 wrote, copied into the crates that need it, with
the noise of a run as a parameter.
* claim: #6558
* Shortcuts: Ctrl+Shift+S opens Save As in the workspace (#6558)
The Home page already opened Save As with Ctrl+Shift+S, but the design workspace matched Ctrl+S, so the chord saved over the current file instead of offering another one. The workspace now routes the chord to Save As, and the File menu shows that shortcut next to Save As, taken from the command registry.
* claim: #6600
Co-authored-by: Cursor <cursoragent@cursor.com>
* Save an imported mesh that only fits after compression (#6600)
The design file limit was applied to the mesh before Deflate, so a mesh
that had already loaded could not be saved or autosaved.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: validate and version large mesh saves (#6600)
---------
Co-authored-by: Colin Faulkingham <colinfaulkingham@Colins-Mac-mini.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
* claim: #5679
* Top-level cut and join reach bodies in sub-components (#5679)
An extrude, revolve or press-pull from a sketch on a face of a body in a
component inside the active one now cuts or joins that body, moved or
turned, in its own component, so every placement shows the change. The
automatic operation probes that body where it is shown. A linked part, or
a face on a further placement of a part placed more than once, is refused
with the reason instead of a silent no-op (#5678).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6423
* test: a copied body must keep the look of the body it came from
solid.move with copy: true leaves the copy without the appearance, the
material or the face looks of the source body, so it renders with the
default look. The tests fail for that reason: the copy has no entry in
appearances.bodies, in materials or in appearances.faces.
* fix: a copied body keeps the look of the body it came from (#6423)
A move with `copy: true` pushed a new body but left it with the default look, so the copy came out a different colour than the part it came from. The copy now carries the body's appearance and its material, as the component copy already did.
* test: keep the copy regression to the body look and the material (#6423)
* test: each copied body keeps the look and the material of its own source (#6423)
Two bodies with different appearances and different materials, copied in one solid.move: each copy must keep the look and the material of the body it came from.
Scope of the fix: the look of the BODY and its material are carried over to the copy. The per-face looks are NOT carried over: a face look is keyed by the point on the face, so the copy's faces would have to be matched and their points translated, which is a separate feature.
A drag that isn't live walks to the cursor in steps, each a held solve
on its own budget seeded on the last solved state. Past a failed step
the walk keeps trying every step (a stretch the geometry can't follow
can be followed by one it can), and each failing solve runs the
numerical solver to its limit: one drag in a 65-command sketch took
7.7-9 s, most of it in failing solves.
A failed step leaves the sketch as it was, so the steps after it all
start from the same state and none changes what the next one sees.
They are now solved eight at once on all cores; those that fail in a
row are taken as tried and the walk goes on at the first that doesn't,
as it would one by one. The drag above takes 3.7-4.3 s and every
command's reply is unchanged. Live frames, whose steps share one
frame's budget, are left as they are.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6569
* Sketch: an offset slot can still be moved by the dimension that places it (#6569)
An offset arc is drawn round its source's own centre, and its ends are
held while the sketch solves (the offset is rebuilt from its source
afterwards). Its arc condition, both ends as far from the centre, then
only tied that shared centre, and the source arc with it, to the middle
of the held ends: a slot offset by 1 mm lost a degree of freedom, and
the length that places it was refused as conflicting.
The solver and the constructive reading now leave out the arc condition
of an offset arc round its source's centre (Sketch::offset_arc_follows_source),
as #4466 did for the Parallel between an offset line and its source. The
source's own arc keeps the centre; nothing changes without an offset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: a test for a rounded rectangle with an offset round it (#6569)
kaievns's case from the review: the offset's corner arcs tied each corner
centre to a 45 degree line, so the height followed the width and every
width change was refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6841
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Sheet metal: a pattern of a cut made in the flat is kept like the cut (#6841)
A rectangular, circular or path pattern of an extrude cut through sheet metal was refused
("this sheet machining cannot be preserved in its flat material"), though the cut itself is
kept as a cut-out of the flat pattern. The pattern replayed its copies through the generic
boolean, whose sheet check refuses any change the sheet model can't keep; the cut goes through
the sheet's flat machining first.
Each copy of an untapered extrude cut is now machined into the sheets the same way, in its
moved sketch plane (a moved copy keeps the extrude's prism certificate), and then cut from the
other targets. A copy that meets a folded bend is refused as the cut is there. Mirrored copies
(the sketch plane turns over) and patterns of an intersect (which cut with the union of their
copies) keep the generic path.
Fixes#6841.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5098
* Sketch: deleting part of a link breaks off the rest instead of rebuilding it (#5098)
Deleting some of a link's curves left the rest linked. The next refresh saw a
different layout and rebuilt the whole link as normal geometry: the deleted
curve came back and the others lost their construction flag (a projected arc
returning as a solid arc, a projected face's lines turning solid).
sketch.delete now breaks the link for what remains, as Break Link does: those
curves stay where they are and keep their flags. Deleting a whole link, and
text links, are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FN5eEMrqWWXqi65FcK8GXq
* Move the #5098 tests up in sketch_project_tests.rs
They sat at the end of the file, where main has since added other
tests; moving them next to the break-link test avoids the merge conflict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #5853
* Batch: refuse a step that would fail at its turn before the first step runs (#5853)
engine.batch and the MCP batch tool checked only a batch's shape up
front. A misspelled command id in a late step was found at its turn,
after every earlier step had run, and then all of it was rolled back: a
160-step batch with a typo as its last step ran for a minute and
changed nothing.
solidcraft_engine::batch_preflight::check finds, before anything runs,
the first step the batch would certainly refuse at its turn: a step
without a command, an unknown command id (the error names the closest
ids, as at the step's turn) or, with strict parameter checking, a call
that breaks its command's typed contract. Anything that depends on the
design is left to the step's turn.
- engine.batch (control channel) checks against the app's registry and
its session's level; view.pick, which the app answers itself, passes.
- The MCP batch tool checks when the commands run in its own process
(headless, with --param-check from the backend). A running app may be
another version with other commands and checks the batch itself; its
reply's not_run is passed on.
- The refusal is the failing step's reply with completed 0, undone 0,
not_run listing every step and a note.
Tests that used an unknown command only to make a late step fail now
use a step that fails at its turn (a bad extrude distance), so they
still test rollback, timings and file.new as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FN5eEMrqWWXqi65FcK8GXq
---------
Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6211
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Joints: a loop whose axes are a hair out of parallel still moves (#6211)
Holes picked on parts are rarely exactly in line. A closed loop through
axes a few hundredths of a microradian apart (a lever, link and a rod on
a cylindrical axle) can't close exactly: it closes to nanometres, and the
gap grows as it turns. The loop solver wanted 1e-7 mm, so it threw such a
closure away (the loop stayed open, its parts at 0 dof) or stopped a drive
or drag after a fraction of a degree.
The conflict check is now 1e-4 mm / 1e-6 rad (it was below what its
message can show), and joint.drive / motion and joint.reach close the
loop at each step as at the start: to the tight tolerance where they get
there, otherwise within the conflict check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2175
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Body rename: an imported assembly's other bodies keep their names and components (#2175)
An Import keeps its body names by the file's body index, and builds no body for the
file's later instances of a repeated part (`skip`). Renaming a body stored the names by
the body's place in the model instead, so every name after the first skipped instance
landed one slot early (and the new name could land on a skipped slot). The rename looked
right until the design was rebuilt; then those bodies took other names, dropped out of
their components and showed up loose in the import's top component.
`Feature::body_name_slots` maps the bodies a feature builds to their stored slots, and
`Feature::pin_body_names` stores current names through it. The rename, the pinning of
later features' names (rename and suppress) and `Document::body_rename_from` use them.
Fixes#2175
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6348
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: constrain to an edge of any body, a mirrored copy too (#6348)
A constraint tool ignored a click on a body edge, so only edges already in
the sketch (the face it sits on) could be constrained to; a mirrored copy's
edges never were. Constraint commands now take a model edge or vertex
({edge|vertex: [x,y,z], body?}) for an operand and project it first, in the
same undo step, and the pick tools pass a clicked edge or vertex that way.
The link keeps the edge's persistent name, so the constraint follows rebuilds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6646
* Control channel: document.revision, a design revision and a document id (#6646)
A program that follows the open design asks document.revision and exports
only when the answer moves: document_id names the open design (kept through
edits, saves and renames, another for each design started or opened, never
shared between two tabs or two runs) and revision moves whenever the design,
what is hidden or an uncaptured component position changes. It stays put on
reads, selections and view changes, unlike the session's view revision.
The revision is read lazily: the session remembers the design it last
reported (held weakly, so Arc::make_mut can never change it in place and its
address can't be reused) and takes a new number from one counter for the
whole run when the design is another one. The control method answers at once,
also while a slow command computes; engine.execute reaches the same command
for scripts and MCP.
* document.revision: hash the run's id prefix with the process's random keys
A clock set before 1970 gave every run the prefix 0, so a restarted app could
answer a new design with the id and revision of the old one. The docs no
longer promise an answer at once: a screenshot waiting for an opened design to
be meshed still holds the requests behind it.
* document.revision: its label in the complete catalogs (ja, zh)
* claim: #6985
* Control channel: control.subscribe and design.changed events (#6985)
A connection that sends control.subscribe gets the active design's id and
revision in the reply (as document.revision answers), then a
{"event":"design.changed","params":{document_id, revision}} line whenever
either changes, so a live link need not poll.
The window publishes the design at the end of each frame's logic and
drawing while a connection is subscribed (one atomic load otherwise). Each
subscribed connection waits on that from a thread of its own and writes
the latest after a 100 ms gap, so a burst of changes is one line. Replies
and events share one lock per connection, each written whole (#6390).
* control.subscribe: the reply before any event, a second one refused
The events thread started before the subscribe reply was written, so a
change right after the window's answer could reach the client before the
reply that named the older revision. The subscription now starts holding
the connection's line lock and writes the reply under it.
A second control.subscribe on a subscribed connection is refused on the
connection itself: its answer could arrive behind a newer event. When no
thread or socket is left for the events, the reply says it did not
subscribe instead of ok.
* control.subscribe: a second one does not lose a finished late reply
The already-subscribed answer came before the busy check, so a request that timed out and had finished since lost its reply: control.pending then said the app ended it without one. It now comes after the busy check and drops a finished late reply as every other request does.
* control_server: declare the subscribe guard at the top of the loop
Clear of the lines #6643 changes (its token check moves to where the declaration was).
* control_server: the subscription's items beside serve
Away from the lines next to send, where #6643 adds token_of.
* control test: New on a blank design reuses its tab, so redo the box first
---------
Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
* claim: #617
* fix(sketch): manual Horizontal/Vertical between a circle centre and an edge (#617)
The manual H/V tool silently did nothing when a point and a straight line
were picked or selected together (only two points or a lone line worked),
while auto-constrain inference applied the alignment. A point picked or
selected with a straight line now aligns the point with the line's
midpoint, in the mode the line's dominant axis suggests, through the
existing midpoint alignment in sketch.constraint.horizontal_vertical.
- tools: constraint_preselection consumes [point, line] selections; the
interactive tool applies the alignment on the second pick (a line).
- context menu: Horizontal/Vertical Constraint items for point + line.
- engine + ui regression tests pin centre-edge alignment and the
edge-to-centre dimension from #617.
Written by Qwen.
* style: formatting cleanup for #617 files
---------
Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
* Extend: join a stop that is already another curve's end
sketch.extend refused a target that coincided with another curve's end
("the extension target coincides with existing geometry") instead of
extending onto that point. The stop is now joined: a new line ends on the
existing point, and a free end grows onto it held by a Coincident. The
preview shows that stop too, so the line it draws is the line the click adds.
A stop at the end of a spline or conic joins that end the same way, and
extending an infinite line now refuses instead of moving one of its two
points.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* Extend: close an arc, take an equal stop, preview with the command's search
An arc with nothing on its path now grows all the way to its own far end and
closes into a whole circle instead of reporting that there is nothing to
extend to.
Two curves stopping the extension equally far are both on the way, so the first
stop is taken instead of refusing to choose between them.
The line preview and the command now share one stop search (`line_aim`), so the
preview can no longer draw a line the click refuses.
---------
Co-authored-by: n0tfr3shm1lk <n0tfr3shm1lk@ukr.net>
Co-authored-by: Codebuff <noreply@codebuff.com>
* claim: #1994
* plastic.snap_fit: base_height and new_body options (#1994)
Base Height extends the snap fit's base below the picked face; New Body
makes the snap fit its own body so it can be patterned or moved. Both
optional, serde-defaulted, old designs replay identically.
* ui: snap_fit length-field defaults list gains the optional base height
---------
Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com>
* Browser: a feature row edits its profile sketch, suppresses and rolls back
A sketch button on feature rows with a profile sketch, and the feature menu gains
Edit Profile Sketch, Suppress/Unsuppress and Roll Back Here. Features behind the
history marker or suppressed are drawn faint, as in the timeline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Browser: the sketch button sits right after the feature's name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6545
* cli_link: the Unix-only link code and its test build on Unix only, so the UI crate builds on Windows (#6545)
slot() is used only by link_in's Unix version, and links_only_where_it_may
makes its own link with std::os::unix::fs::symlink; gate both like link_in.
On Windows the test failed to compile (E0433) and clippy -D warnings flagged
slot as dead code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6755
* Frame rate readout stays beside a right-side pane, not under it (#6755)
The readout and its details sat in the viewport's bottom-right corner even under the
floating Assistant. They now move just left of any pane over that corner and come back
when it closes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every view attached to an API-agent message stayed in the history as a
base64 data URL, and the whole history goes with every request (up to 24
per message). Requests grew by one screenshot per message until the
endpoint refused them with HTTP 413, after which every message failed.
Keep the newest two views as pictures and swap older ones for a short note;
the text sent with them and every other message stay. HTTP 413 now says the
conversation is too large for the endpoint and to start a new chat.
Fixes#2298
Main now speaks the Anthropic protocol too, where a view is an `image`
block rather than an OpenAI `image_url` part: drop_old_views trims both,
before trim_history. ui.ai_http_too_large is in every catalog that has the
other ai_http_* messages.
Co-authored-by: Framewrk CI <ci@framewrklabs.ai>
* claim: #6396
* Sketch: a regular line on a construction line is picked before it (#6396)
Coincident sketch curves tied on pick distance and the one drawn first
won, so a construction line drawn first hid the regular line on part of
it. Construction and centre lines now get half a screen point added to
their pick distance: on a tie the regular curve is hovered and picked,
and a construction line clearly nearer the cursor still wins. Clicking
again and Select Other still reach the construction line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FN5eEMrqWWXqi65FcK8GXq
---------
Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
wgpu's default uncaptured-error handler panics ("wgpu error: Out of Memory").
The headless picture devices already report instead; the window's device had
no handler. Errors the code expects stay under error scopes.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
A line tangent to an arc at the arc's end had no sense: the tangency held
with the line on either side of its touch point, so pinning a line's far
end across the corner left the line cutting back through its fillet, and
when the solver found the mirrored corner the arc kept its stored
direction and became its three-quarter complement, inside out.
The solver now keeps each fillet an arc of its corner. A line tangent at an
arc's end keeps its sense there (it leaves the arc as it did) and the arc
its side of its chord, compared with the positions before an interactive
move or the stored ones. A corner that mirrored gets its arc on the
mirrored side: its ends swapped where the solve ran the arc round the
other way with the lines following, or the arc reflected across the line
that stayed and solved again where a line cut back through. A solve that
would loop the arc the other way round, or fold a line onto it, is refused
as a fold is. A drag's cursor-following steps keep the fillets too, so a
line dragged through its own fillet stops at the arc.
The kit slider adds a real `egui::Slider`, lets egui paint it, then paints an
opaque rectangle over it and draws the kit's track and thumb on top. That mask
was exactly `slider_width` wide, and egui places its handle so the circle's
right edge lands on `rect.right()` at the maximum, so the fill stopped flush
with the handle and its last column survived as a light sliver beside the
value box. The kit's own thumb cannot hide it either: its radius is
`t.space_8` while egui's handle radius is `input_radius`, about two pixels
larger.
Extend the mask one pixel past its right edge. The gap before the editor is
`item_spacing.x`, so nothing else is covered, and only the maximum was
affected: at the minimum the handle's left edge coincides with the mask's left
edge, which a fill does cover.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* claim: #6524
* Sketch display: each sketch samples within its own budget (#6524)
After a large DXF was inserted, lines drawn in a later sketch vanished
once finished: only their end points showed and could be picked, and
hiding the DXF sketch brought them back.
The display cache had one sampling budget for every shown sketch (4096
curves, 131 072 vertices per model state and view). Sketches are drawn
and picked in timeline order, so a large imported drawing used it all
and every later curve came back empty (BudgetLimited): not drawn, not
pickable, while the points, drawn separately, stayed.
Each sketch now has its own budget with the same limits (curves,
profiles, vertices and nodes). The cache as a whole is capped at 65 536
curves and 16 384 profiles, so memory stays bounded however many
sketches are shown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FN5eEMrqWWXqi65FcK8GXq
---------
Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6657
* Context menu: the Appearance flyout closes when the pointer moves to another row (#6657)
Hovering Appearance opened its quick-choice flyout, but resting on a plain
row of the same menu never closed it: only another submenu row replaced it.
Now a flyout closes once the pointer moves onto another row and rests there
for 0.3 s, so a diagonal path to the flyout across a neighbour keeps it open.
A flyout opened with the keys stays open while the pointer is still.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A feature's face points are moved onto their named faces before it is
evaluated, so a To Object extrude follows its target face when an earlier
feature moves it. A pattern's or mirror's copies were built from the
feature as stored, with the face where it was picked: after a gear's disc
was made thicker, the tooth reached the new top but its pattern copies
kept the old height. The copies now use the same moved points; the move
is one helper used in both places.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Test: sketch Intersect keeps its exact cuts, crosses curved faces on the surface, cuts on a top face
Intersect's cut of a box, a cylinder, a sphere and a tilted cylinder, as it is on main, pinned.
Two cases where it is not exact: a plane through a cylinder's rim cuts the mesh's chord (its sides
come out at 6.3159 and 6.3085, not the square root of 40; a hole's walls at 25.0122), and a plane
on a body's top face finds nothing ("projects to nothing on this sketch plane").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
* Sketch Intersect: crossings on curved faces lie on the surface; a plane on a top face cuts it
Problem: the plane cut that sketch Intersect makes of a body's mesh (project::section) took a
crossing on an edge where a curved face meets another (a cylinder's rim on its cap) from the
merged normals, which are undefined at the crease, so it fell on the chord: a cylinder's sides came
out 0.01 mm off and uneven. A plane through a vertex or on a face counted the vertices on it as
below, so a plane on a body's top face crossed no edge and found nothing.
Solution: the crossings are found by section_segments, which section fits as before: an edge where
a curved face meets another takes the curved face's normals, so the crossing lies on its surface;
a vertex within 1e-9 x the mesh's size of the plane is on it, on one side for every edge (below
when the mesh lies on or above the plane, else above), and an edge ending there crosses at that
vertex, which is an exact point for the fit. Each segment keeps its B-rep face, for a section that
groups them by face. Cuts that were exact are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The first test in each GPU path shard compiles the path pipeline cold
on lavapipe (335-502 s on 16 vCPU, 665 s on 8, against the 600 s setup
budget); the later ones read Mesa's disk cache in about 30 s. Put that
cache on a sticky disk that main commits, so the first test reads it
too until the shaders change.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A change computed off the window thread ran here with only the results of the evaluation
ahead's last evaluate primed. A parameter change evaluates twice (before and after its sketch
stores the new solution), so its first evaluate here found nothing to take, and on a design whose
states pass the evaluation cache's budget it recomputed the features on the window thread: the
window froze for seconds after Computing ended. The run now also takes what the evaluation ahead
kept (KeptResults, as an agent's control requests already do).
The catalogue entry Quantity served both the pattern dialogs' number of
instances and the motion study's plotted quantity. Nine locales had
translated the second meaning, so the rectangular pattern dialog showed
e.g. "Größe" above "Anzahl 2" in German.
The motion study's row now reads motion.plot_quantity (a pending file
that keeps every locale's previous word there). Quantity takes each
catalogue's own ui.quantity text, which the "Quantity 2" row already
shows; nothing is newly translated. The glossary lists Quantity under
words with more than one meaning.
Fixes#6538
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
On 8 vCPU the first merge-queue run after #7066 failed: one gpu path
test went past the 600 s pipeline-setup budget (665 s; 204 s for the
same test on the PR run) and shard 2 took 35 minutes. lavapipe compiles
on every core, so the shards move to 16 vCPU, and four count partitions
(27 tests each) replace three hash ones (41/40/27).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The 108 gpu path:: tests run one at a time (gpu-path test group) and took
about 36 of Native Linux tests' 45 minutes in a merge-queue run. They now
run in a GPU path tests job, three hash partitions on 8 vCPU, beside the
other tests; Native Linux tests leaves exactly them out. A `gpu` output of
Select checks drives both the job's `if` and the `ci` check.
The Windows jobs now run in a merge group only when its diff reaches what
they exercise (the pull request patterns and the Cargo.lock graphics-stack
check), not on pushes to main, and still nightly and by hand.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The README and docs/downloads.md ship a universal macOS disk image, but the
contributing guide listed only Windows and Linux.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: on Windows, give claude mcp add the CLI's full path (#5895)
The Windows installer puts solidcraft-cli.exe in C:\Program Files\SolidCraft but not on PATH,
so the README's and docs/mcp.md's `claude mcp add solidcraft -- solidcraft-cli mcp` registers a
server that can't start there. Say so next to the command and give the full-path form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: word the Windows full-path note so it holds once the installer sets PATH (#5895)
#5898 makes the MSI add its folder to PATH. Say "if a terminal doesn't find solidcraft-cli
(older Windows installers don't put it on PATH)" instead of stating that the installer never
does, so the note stays true whichever change lands first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: the Windows CLI path is the default, not the only place (#5895)
The MSI installs to ProgramFiles6432Folder and lets people choose the folder
(WixUI_InstallDir), so C:\Program Files\SolidCraft\ is only the x64 installer's default; the
x86 MSI on 64-bit Windows lands in C:\Program Files (x86)\SolidCraft\. Say "by default" in
the README and docs/mcp.md (review on #5896).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
SolidCraft runs through XWayland whenever an X display is available. A
compositor that doesn't let X11 apps scale themselves draws the window at
100 % and stretches it. docs/nix.md said so only for fractional scaling,
but GNOME 47 (RHEL and Rocky Linux 10) does it at any scale: the report
shows 1.05 physical pixels per point at 200 %.
The troubleshooting page gets a section with the cause, the Window system
preference, SOLIDCRAFT_WAYLAND=1 for 0.21.0 and earlier, and the file drop
trade-off; docs/nix.md links to it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The component colour tests no longer assume a design always carries a list of
pending format changes; once the release folds them, there is none.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #4214
* Viewport: the grid fades with the distance instead of cutting off (#4214)
The ground grid stopped at a fixed distance from the view, so its edge showed
as a cut across the picture. It now fades out with the distance from the
middle of the view, and it is gone before either of the two lines that used to
cut it: its own edge, and the picture's far plane.
Each grid line is drawn in pieces, and each piece is as faint as the fade
makes it at the distance where it lies. The 'under' batch is drawn by the
overlay pipeline with alpha blending, so the colour's alpha reaches the
frame.
* The headless grid fades with the distance too (#4214)
The headless path (solidcraft-cli snapshot, the home-page thumbnail, the MCP
screenshot) drew the grid from the engine's palette, where a line had no
alpha: the grid still ended in a cut, and at a low camera angle the cut was
in the picture.
A SceneLine now carries an alpha, the CPU rasterizer blends with it and the
GPU capture passes it on, so the engine's grid can fade. The fade itself
moves to the engine, next to the grid's other geometry, and both paths use
it: the headless grid now reaches as far on the ground as the view is wide
(the ground stretches as it is seen at a slant) and dissolves into the
distance instead of ending in a line.
* Fix the headless/viewport grid fade divergence (#4214)
The engine stretched the grid's size with the slant of the plane it is seen at, the viewport stretched it only on the ground, so on a sketch's plane the picture's grid reached further than the window's (153.42 mm against 120.00 mm). Both now share one grid_ext, which the picture and the window draw the same grid from.
* The grid fades out before the picture's far plane cuts the ground (#4214)
The window's grid faded against a far plane of its own: the scene's radius
clipped by the projection's camera (260 mm away at 400 mm of distance), while
the picture itself cuts the ground at the far plane of its own projection
(223.7 mm at the same distance, because the projection frames the scene's
bounds and not its radius). The grid was still a third of its strength where
the picture cut the ground, so the cut showed as a straight line across the
view.
The fade now ends at the far plane the picture's projection uses, and the
grid is gone before that plane cuts the ground.
The test that watched the fade now measures the ink of each row of the
picture, and checks that the grid is already gone in the ten rows above the
row where the far plane cuts the ground: the point it compared before lay
beyond that plane, where both renders draw nothing at all, so the comparison
was always true.
* claim: #5734
* Docs: don't offer winget install before the package is in winget-pkgs (#5734)
The README and docs/downloads.md said Windows users could run
`winget install bherbruck.SolidCraft`, but winget-pkgs has no manifests for
bherbruck.SolidCraft (manifests/b/bherbruck doesn't exist), so winget finds
no package. winget.yml only updates a package that is already listed, and
does nothing until then. Drop the command from both pages, and say in
docs/releasing.md that the first version has to be submitted once by hand
and the command added back when it's listed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Releasing: a first WinGet submission is already open; take it up (#5734)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): finish the open sketch when a modeling feature starts
* fix(ui): wait for the marker roll before starting a feature from a sketch
* fix(ui): start the feature once the marker is back
* test(ui): keep the sketch open in the palette clearance test
Saving into Documents fails with the Windows dialog's "File not found" when
Controlled folder access blocks the app, and the message gives no hint.
Refs #4479.
* claim: #4640
* Docs: AppImageLauncher 2.x can't start the AppImage; update to 3.0 (#4640)
AppImageLauncher 2.x intercepts every AppImage through binfmt_misc and
can't start static-runtime AppImages such as SolidCraft's: it fails with
"execv error: No such file or directory" after a FUSE message. 3.0 starts
it. One paragraph in the Linux AppImage section says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: AppImageLauncher 3.0 is so far only published as betas (#4640)
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
GitHub Pages reports the base URL as http:// for the custom domain, so the
.flatpakrepo file, the remote-add line on the repository page and the mirror
of the previous repository all used plain HTTP. Force https:// once where the
base URL is read.
* docs: two notes for agents and contributors (the gate's working tree, and linking a picture)
* docs: how to show a picture without attaching it
The reviewer asked for the pictures to be attached to the PR (or in a gist),
not hosted on a branch of a fork, so the note no longer teaches the branch
trick.
* docs: say how to attach a picture, and what a gist is for
The review of this pull request pointed at two things in the note on
evidence:
- Attaching works in the browser for anyone who can comment: drag the
file into the comment. The limit is the command-line agent, whose
upload needs write access to this repository. Say that, instead of
claiming that a contributor without write access cannot attach a file.
- A gist suits a log or another text file; it does not render a picture
inline. Say what it is for, so nobody hits that dead end.
The note in AGENTS.md about the gate reading the working tree is
unchanged.
* claim: #4851
* User guide: local models with the Assistant (#4851)
A page on choosing and setting up a local model (Ollama, LM Studio, or any
OpenAI-compatible server) for the Assistant: tool support, VRAM, permissions,
the measured results from the standard test in #4851, and troubleshooting
for the common failures.
* Guide: local models table with the full set of measured results
Point to the `<file>.bak` copy every save keeps, the design.solidcraft in crash
reports, the recovery folder for unsaved designs, and how to attach the file
to an issue.
"Start every session here" sent readers to plan/STATUS.md,
plan/execution-plan.md, plan/architecture.md and
plan/adr/0001-geometry-kernel.md. plan/ is gitignored, so nobody who
clones the repository has those files.
Point the steps at what ships instead: ROADMAP.md, CONTRIBUTING.md's
"Where help is wanted most", "How the code is laid out" and "Kernel
work is paused" sections, docs/architecture.md, the help wanted, good
first issue and needs-triage labels, and the licence rules with
deny.toml and cargo xtask licences --check.
Signed-off-by: Ray <raykholo@users.noreply.github.com>
* Roadmap: a short user-facing ROADMAP.md, developer detail moves to docs/roadmap-detail.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Roadmap: feature milestones with progress counts, daily releases through the in-app updater
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Roadmap: count issues as pieces, show work built on the drawings and render branches
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Joint dialog kept a round edge (a hole's rim), a straight edge and a
corner as plain points where they were picked. When the part got thicker,
a hole moved or the part got longer, the origin stayed where it was and
the jointed part overlapped the other one. An edit that left the picked
spot where it was (lengthening an arm away from the jointed hole) looked
fine, which hid it. Faces and hole walls were already found by name.
The dialog now sends a round edge as its circle's centre with its z, and a
straight edge or corner as an edge pick with its z. The engine keeps them
by name (the round end by its wall and the face across its rim, the edge
and corner by their edge and face names), with the frame the pick showed;
where it can't (no planar face beside the edge, no names), it keeps the
point as before. Edge origins get an optional axis (along the edge, or a
fixed direction) so a hinge on an edge keeps turning about it.
Tests: every joint type and as-built, for each way the dialog picks an
origin, with the arm made thicker, its hole moved and the arm lengthened,
inside one design and through Update Links.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Move and Align on placed components: picks and vectors where they are shown
A body in a component placed away from its own frame (an imported part
set upright) moved the wrong way: Move's vector, axis and pivot were
taken in the root's frame, so moving the upright plate up moved it along
its own frame's z. They now turn with the frame of the bodies moved.
Align mapped each pick into the frame of the body under it, so aligning
two separately placed parts mixed two frames (the plate landed at right
angles), and its snap points were never mapped at all. Align now finds
its picks, face normals and snaps where the view shows them and takes
both ends into the moved bodies' frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Preview: a feature's tool is drawn where its component is shown
Extrude, revolve and the primitives preview their tool on its own. That
preview was built in the model's frame, so on a component placed away
from it (an imported part set upright, edited with its component
active) the tool showed at right angles to the picked face, though the
feature itself came out right.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Follow main's lazy body frames and first-part grounding in the placed-component work
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>
* Cache assembly browser data between unchanged frames
Extend #1762's cached component membership, placements and viewport hover
feedback with cached occurrence labels, folder entries, row keys and
tooltips. Skip offscreen row work and reuse browser hover buffers while
reading selection, visibility and locking live. Cache #2681's standard-part
configuration labels with the document snapshot instead of formatting
them on every redraw. Preserve #3009's live selection and hover marks
with highlight keys owned once by cached entries, and retain #2431's
feature rows in the reusable drawing order.
Related to:
- tangency-labs/solidcraft#662
- tangency-labs/solidcraft#1762
- tangency-labs/solidcraft#2681
- tangency-labs/solidcraft#3009
- tangency-labs/solidcraft#2431
Co-authored-by: Codex <noreply@openai.com>
* Browser cache: lock state moved to the session visibility
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Browser cache: reveal a placement's body by its row, and drop a needless borrow
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Pipe: an Operation row, so a pipe can join, cut or intersect (#2820)
The engine's solid.pipe takes an operation, but the Pipe window had no way to
choose one, so a pipe always made a new body. The window now has the same
Operation row as Loft and the primitives, and editing a pipe keeps its
operation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fit the Pipe operation row to main: sketch-qualified pick, length_input pattern
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>
* claim: #4846
* Assistant: say so when a turn ends without using SolidCraft's tools
When tools were offered and a completed turn made no tool call and no
change, the chat panel shows a short notice pointing to tool-capable
models: always for an empty reply, and for a text-only reply to an
explicit edit request. Plain answers to questions stay quiet. The
control channel exposes it as turnWarning.
* Fix the Anthropic loop test for the tools event; add ja and zh for the no-tool-call notice
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>
* Every new command comes with input and output schemas
A test fails for a command without a contract or an output type, unless it is a planned!
placeholder or listed in schema/unconverted.txt, which holds the 400 commands that predate
the rule and only shrinks: the test also fails when a listed command has its schemas, when
the list names a command that doesn't exist, or when it isn't sorted.
`checked!` now takes only the command's id and checks the contract that id publishes
(`run_checked_as`), so the contract enforced is always the one described. Another test runs
every command that has a contract in strict with an unknown key, so a contract is never
published without `checked!`, under the command's own id, enforcing it.
A new book page, Command schemas, explains how to write a contract and a reply type, the
tests to add, the shared field types and how to add one, with a checklist; the failure
messages point to it. "Adding a command", CONTRIBUTING.md and AGENTS.md now include the
schemas, and the MCP tool description and docs/mcp.md no longer list the converted commands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* List the commands added on main since: view.layout, view.history_layout, view.normal_to
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Command schemas guide: what participants and targets do, checking the evaluation, warning-free replies
- `participants` changes the result for some features and nothing for others; where it and
another key override each other, or where it has no effect, say so in that key's
description or in the summary.
- The example primitive's `targets` default to every body in the feature's component, not the
bodies it meets; they're ignored when `participants` lists bodies.
- Check what each value does where the feature is evaluated, not only how the handler reads it.
- A reply test runs every form in Warn and checks for no `param_warnings` (Strict for a reply
that isn't an object).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* List the 43 commands added on main since: frame, component, render and others
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* List the 11 commands added on main since: browser folding, standard views, parameter sweep, path flange
browser.collapse_all, browser.expand_all, inspect.parameter_sweep, sheet.path_flange and the
seven view.* standard views (back, bottom, front, isometric, left, right, top) landed on main
without schemas; the gate flags them, as intended. 457 commands listed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Schema rule warns instead of failing for now
A command without input and output schemas prints a warning listing it
(test output and a CI annotation). SCHEMA_RULE_STRICT in coverage_tests.rs
(or SOLIDCRAFT_SCHEMA_RULE_STRICT=1) makes it fail again. Docs say "should,
and will become required".
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>
* claim: #3965
* Parameters: a click can correct an invalid name (#3965)
Refusing the new row was putting focus back on the field being left, so the correction was typed into the expression.
* Parameters: the invalid-name test types its expression draft (#3965)
A new row starts with a blank expression, so the regression test types 10 mm and checks that correcting the name leaves that draft in place.
* claim: #4146
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport theme: a light 3D view in a dark window, and the reverse (#4146)
Preferences › Interface › Theme gets a Viewport row under the theme tiles:
Follow theme (the default, as before), Light or Dark. Light or Dark draws
the 3D view in the built-in Light or Dark theme's colours whatever the
window uses, so a dark window can show the model on a light view and the
other way round; it works with every theme, theme files included.
The view takes the background and grid, bodies, selection and hover, the
origin, previews, sketches with their dimensions and glyphs, the navigation
cube and triads, box selection and the analysis colours. Panels, dialogs,
menus and the chips over the view keep the window's. Text and warnings drawn
straight onto the view (the sketch status line, the loose-end and midpoint
markers, the section's cut outlines, the 3D sketch tool and the sketch tools'
previews) take the view's side, so they read on its background.
theme::ViewportTheme is the setting (saved as `viewport_theme`: theme, light
or dark; an unknown saved value reads as Follow theme, as ColorVision's
does). Tokens::get() takes the view's colours from the other side only when
the view's dark or light differs from the window's; Tokens::view() is the
view's side for colours drawn onto it. The ground shadow follows the view
(the scene and highlight caches already key on the colours themselves),
ui.render draws in the view's colours, and ui.prefs takes `viewport_theme`
and reports `viewport_dark`. An older release ignores the setting and draws
everything in the theme.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Viewport theme: follow the single set of axis colours
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>
* claim: #754
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
* Sketch Dimension: arc length from an arc and its end point (#754)
The arc-length dimension existed (sketch.dimension type arc_length) but
the Dimension tool could only give an arc its radius. An arc and one of
its own end points, in either order, now dimension the arc's length:
click the arc, then its end. The pair was refused before ("cannot
dimension that combination"), so no existing result changes. A point
where the arc's end is (a coincident line end) counts as that end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBN24y2TcqRcm2zn8Gkj27
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: hovering a circle's edge wakes its centre
A circle's or arc's centre was hard to take with Line or Dimension: it is a small
point in the middle of empty space, taken only within 8 points of the pointer, and
nothing showed where it was. Hovering a circle or arc of the sketch being edited
now wakes its centre: drawn (a cross in a ring) and taken from 16 points away, so
the pointer can go from the edge across the inside to it. Hovering another curve
or point puts it back to sleep, as does leaving the sketch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* A woken centre's extra reach yields to a curve under the pointer
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>
* Browser: surface bodies get the surface icon in the Bodies folder (#4165)
Every row in the Bodies folder had the solid body icon, so a surface (an
open shell, such as a Surface Revolve of an open curve) looked like a
solid. A body whose shell does not close now shows the existing surface
icon; solids keep the body icon.
Body::is_surface walks every shell's edges, so ModelBody caches it once per
body (shared by clones, like outline_bounds) instead of the browser
checking it every frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add the surface icon to the requested-icons manifest
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>
* Look At in the viewport context menu for a planar face or plane
Fixes#4105
* Look At tests: turn the camera without the animation
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>
* claim: #4846
* Warn when Ollama models lack tool support (#4846)
Check Ollama model capabilities in the background and cache results per
endpoint and model. Show an advisory warning for models without tools,
expose it through ui.agent_chat, and document the behavior.
Cover capability parsing, warning selection, background probing, caching,
and selection changes with synthetic regression tests.
* Keep the Ollama capability probe out of the web build
The panel never calls it there, so it would only be dead code.
* Wrap the model warning instead of cutting it off
In a narrow panel the end of the sentence, the part that says what to do, was truncated.
* Fix AgentConfig default after rebase onto main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move the Ollama no-tools warning into a pending locale file with ja and zh
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>
solid.loft already closes to a point (a section {sketch, point}), but the Loft
window's Profiles input took profiles only, so a pyramid or cone tip couldn't
be made from the UI. The input now also takes a finished sketch's point, sent
as an apex section in pick order; profiles of one sketch still make one
section.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #2348
* Open linked designs from assembly browser
* claim: #2348
* Test linked design action with multiple components selected
* Fix linked design lookup in browser storage and translate its menu action
* Add Turn Up Axis to complete UI catalogs
* Keep browser DXF download after linked-design checks
* feat: add colored components inspection toggle
* feat: persist component colors and add cycling controls
* feat: assign component colors in creation order
* fix: update component color menu integration and translations
* fix: proper description for pending format change.
* fix: deleted duplicated format change description from docs/pending.
* Compare the component colour counter in the features-only check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Vadim Kalinsky <vadim@kalinsky.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Sketch: a click picks the sketch's own curve over a projected one under it
* Select Other tests: follow origin visibility rename
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Own-curve pick test: click away from the lines' midpoints, which are picks of their own now
* Use the select-other test helpers main already has
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>
* claim: #5543
* Extrude: a profile drawn twice is cut once, not as two coincident tools (#5543)
A circle drawn twice at one place is two profiles of one region. Extruded
together they were two coincident tools: a cut through a 121-hole grid with
four such circles retried the boolean with the tool nudged (four times) and
took 25 s instead of 0.5 s.
merge_regions, which every extrude, revolve and surface feature runs on the
regions it builds from, now leaves out a region whose boundary repeats an
earlier one (within 1e-7 of its size; corners and segment middles on the
other loop both ways, holes alike). The selection itself keeps its
multiplicities, as profile_refs' tests require.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* claim: #6186
* Timeline: a reorder evaluates the design once, from the cache it already has (#6186)
timeline.reorder checked the move on a copy of the model, then dropped it,
so setting the reordered design evaluated the moved part of the timeline
again; reorder_many (and moving features into a group) checked on an empty
model, recomputing the whole design even for a move near the end. The
check now starts from the session's own model, and the session keeps the
model it was checked on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Marker is Copy: drop the clone
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>
Search Console lists docs.solidcraft.io/, /introduction.html (the same page) and /guide/mcp.html as duplicates without a user-selected canonical. The Pages build now adds a canonical link to every book page (index.html and introduction.html both name /), skips the redirect stubs that already have one, marks 404 and the toc frame noindex, and writes a robots.txt pointing at a generated sitemap.xml.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:35:56 -04:00
2559 changed files with 248217 additions and 96166 deletions
Thanks for taking the time to report this! Inside the app, **Report an Issue** fills most of this in for you, with the command, its settings and your environment.
Before reporting a boolean or modelling failure, please try to reproduce it on the latest nightly or a build from current `main`, and say which build you used.
# Documentation, packaging, the other workflows and the GitHub bots' scripts select nothing:
# no native job runs for them, on pull requests or on main.
# Tests are built and run in one job on a big runner, slowest first, starting beside the repository
# checks (no archive to upload and download). A few heavy tests (see .config/nextest.toml) are
# skipped on pull requests and run on pushes to main and nightly. The Windows jobs (D3D12 shaders
# and app startup), the reference tests and the doc tests run on main and nightly only, not on
# pull requests (the fast lane, in Select checks); a break there is fixed forward on main. Only
# main saves the build caches; pull requests restore them. sccache is on a Blacksmith sticky disk that
# only main commits.
# checks (no archive to upload and download). The GPU path tracer's tests run one at a time
# (.config/nextest.toml), so they have a job of their own in four shards (GPU path tests). A few
# heavy tests (see .config/nextest.toml) are skipped on pull requests and run on pushes to main and
# nightly. The reference tests and the doc tests run in the merge queue, on main and nightly, not
# on pull requests (the fast lane, in Select checks); a break there is fixed forward on main. The
# Windows jobs (D3D12 shaders and app startup) run nightly, and in the merge queue only for a group
# that changes what they exercise. Only main saves the build caches; pull requests restore them.
# sccache is on a Blacksmith sticky disk that only main commits.
# A push to main runs only the repository checks when a passing merge-queue run tested that very
# commit, or when nothing a build reads changed since the last push to main whose run passed.
# Otherwise it runs everything.
@@ -30,7 +32,7 @@
# reads `ci` from the newest run of a commit, so a run without it would block a PR that passed.
# Cross-compilation is opt-in through Run workflow; release builds remain in release.yml.
# Every Linux job runs on Blacksmith's Ubuntu 24.04 runners (blacksmith-<N>vcpu-ubuntu-2404), sized
# per job: 32 vCPU for the tests, 16 for Clippy, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
# per job: 32 vCPU for the tests, 16 for Clippy, the GPU path tests, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
# The Windows shader job runs on Blacksmith's Windows Server 2025 runners (blacksmith-8vcpu-windows-2025);
# the Windows app startup smoke stays on windows-latest, which has a desktop session that presents frames.
# Pull requests, forks included, the merge queue, pushes to main and the nightly run all use them:
@@ -46,10 +48,14 @@ on:
# only (other labels are skipped, see the top of this file).
SolidCraft is a clean-room, open-source, pure-Rust parametric 3D CAD application:
sketches with a constraint solver, a parametric timeline of features, a B-rep
kernel, and a desktop UI that agents can drive. Projects published by
[storytold](https://github.com/storytold) can serve as references for implementation conventions.
Any code adapted from other projects (MIT OR Apache-2.0) is noted in ATTRIBUTION.md.
SolidCraft is a clean-room, pure-Rust parametric 3D CAD app (sketch solver, feature timeline, B-rep kernel, an egui UI that agents drive). This file is a router: the hard rules, then the skill to read for your task. Humans: CONTRIBUTING.md.
## Product name
SolidCraft everywhere: crates `solidcraft-*`, binaries `solidcraft` and `solidcraft-cli`,
`.solidcraft` designs, the `solidcraft/N` format tag, `SOLIDCRAFT_*` environment variables,
settings and recovery folders, the MCP server name, docs, CI and packaging. The old name
survives only in the explicit compatibility shims listed in `docs/compatibility.md`; don't add
new uses of it anywhere else. Public docs/web/Flatpak deployments use project Pages
`/solidcraft/`; do not add permanent old-name Pages aliases.
## Start here
Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelled `help wanted`, `good first issue`, `needs-triage`; architecture in `docs/architecture.md`. Act when the code, docs and rules settle a question; ask only when it changes what users see. Local `plan/` notes are optional.
## Start every session here
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md`
and the relevant `plan/architecture.md` section.
2. Kernel and library decisions: `plan/adr/0001-geometry-kernel.md` (what we may depend on, what
is rejected for licence reasons).
3. Follow the autonomous operation protocol (`plan/execution-plan.md` §6). Don't stop to ask
unless it lists the decision as the owner's.
## Hard rules (what enforces each)
- Independent: never copy another product's code, icons, text or templates; never drive or take data from commercial CAD. `skills/solidcraft-ground-rules/SKILL.md`.
- No GPL/LGPL/AGPL code, not even reading it (OpenCASCADE, FreeCAD, SolveSpace, CadQuery, LibreCAD, QCAD): `cargo xtask licences --check`.
- Every asset is original or openly licensed with a row in ATTRIBUTION.md; read `assets/AGENTS.md` before icon work: `cargo xtask assets`, `cargo xtask icons`.
- Never crash: no unwrap/expect/panic/unsafe in non-test code (unsafe allowlist in crash-safety), errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
- Everything is a command and programmatic calls never open dialogs: `skills/solidcraft-commands/SKILL.md`.
- One outer UI container, one camera path (`navigation::handle`), colours from theme tokens: `cargo xtask ui-containers`.
- Application code is Rust only and builds for wasm: `cargo xtask wasm`, `cargo xtask web-offline-test`.
- The product name is SolidCraft; the old name lives only in `docs/compatibility.md`.
- Sorted lists (message catalogs, `mod` lines, languages) stay sorted: `cargo xtask lists`. Never edit generated files (`docs/features.md`, `THIRD-PARTY-LICENSES.txt`) in a PR.
- A design-format change is ONE new file in `crates/doc/src/format/pending/`: `scripts/test-format-pending.sh`.
- A command has input and output schemas (will become required; `schema/unconverted.txt` only shrinks): `skills/solidcraft-commands/SKILL.md`.
- New UI strings go in `assets/locales/pending/<slug>.json`, ATTRIBUTION rows in `attribution/pending/<slug>.md`: `cargo xtask lists`.
- A changed default keeps the old behaviour behind a preference and fills the PR's `Changes a default:` line, and no second PR for an issue a lower-numbered one closes: `pr-checks.yml`.
- A regression test goes in the area's existing test file, never a per-bug file (#3226).
- Look at open PRs first, claim with a draft PR (lowest number wins), one topic per PR, never force-push `main`, no pushes just to catch up with `main`: `skills/solidcraft-contributing/SKILL.md`.
- Show observable changes with real screenshots or a recording, captured yourself.
- Run `cargo xtask ci` before every commit, without switching branches while it runs. `cargo xtask skills` keeps this file and `skills/` honest.
`plan/` is gitignored (local only).
## Never crash
People trust SolidCraft with their designs; a crash loses their work. **This outranks feature work.**
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`). The exceptions, in the app crate and in
`crates/threedx-client` (which deny it instead), have no safe API: the macOS-only
`crates/threedx-client/src/macos.rs` (loads the 3Dconnexion driver's client framework at run time
and calls through its function pointers), two functions in
`apps/solidcraft/src/open_documents.rs` (macOS hands Finder files only to an Objective-C
method), the Windows-only `apps/solidcraft/src/native_crash.rs` (the exception filter that
records crashes inside native code such as graphics drivers), the Windows-only
`apps/solidcraft/src/native_frame.rs` (the window procedure that answers the resize-border hit
test of the window without OS decorations, and gives its class the icon the taskbar falls back
on) and the Windows-only
`apps/solidcraft/src/space_mouse/windows_hid.rs` (asks Plug and Play which HID interfaces are 3D
mice, so no other device is sent anything).
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
returns an error ("not supported yet: …"); it never panics.
- **Input-derived numbers are hostile** (command parameters, design files, control-channel
input): `get()` not `[i]`, finite checks, caps on sizes and loop counts.
- **Third-party kernel calls are guarded:** everything that calls `truck` goes through
`solidcraft_kernel::guard`, which turns a panic into `KernelError::Internal`.
- **Last-resort guard:** `Session::execute` runs every command under `catch_unwind`; an escaped
panic restores the design and reports `EngineError::Internal`.
- **Crash reports:** the desktop app writes every panic and every unclean exit to a local crash folder,
with the design and the commands that reproduce it (`solidcraft_engine::crash`, docs/crash-reports.md).
When someone says the app crashed, look there first: `solidcraft-cli crashes`, `crashes show <id>`,
artwork from commercial CAD products — ever.** Command icons are the original SVG masters in
`assets/icons/ui/design/svg/`, compiled through `crates/ui-egui/src/icon_svg.rs`.
**Before any icon, icon assignment or media edit, read `assets/AGENTS.md`,
`assets/icons/ui/AGENTS.md` and `assets/icons/ui/STYLE_GUIDE.md`.** Use the canonical
bases and named finished references there; never invent a parallel icon style. Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
(`cargo xtask assets` enforces it). Screenshots of commercial CAD software are never committed.
Breaking this rule is the most serious mistake you can make in this repo.
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*`
(`CommandSpec`: id = our own dotted, lower-case id `<area>.<thing>[.<variant>]` such as
`solid.extrude` or `sketch.rectangle.two_point`, never another product's command id; label;
toolbar tab and panel; icon; shortcut; params doc; `enabled`; JSON `run`) + tests. The toolbar,
dialogs, sketch tools, palette, scripts, CLI and control channel all reach the same commands.
- **Programmatic calls never open dialogs.** `engine.execute` runs the JSON form; only toolbar
clicks (`ui.start`) start interactive tools and dialogs, which then run commands.
- **Layering** is enforced by `cargo xtask layers`: L0 `geom` → L1 `sketch`, `kernel`, `render`
@@ -101,6 +101,24 @@ Editable drawing code, native projects and shipped exports are collected under `
| tests/step-conformance/tiny_pin.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/torus_ring_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/step-conformance/washer.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
| tests/kernel-cases/import/1721-closed-bspline-tube-face/input.step | step-conformance contributors | copy of tests/step-conformance/cylinder_nurbs.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#1721 | CC0-1.0 |
| tests/kernel-cases/import/2043-outer-bound-opposing-surface-sense/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#2043 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/258-cone-point-vertex/input.step | step-conformance contributors | copy of tests/step-conformance/cone_point_vertex.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
| tests/kernel-cases/import/258-cone-to-point/input.step | step-conformance contributors | copy of tests/step-conformance/cone_point.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
| tests/kernel-cases/import/258-tiny-pin/input.step | step-conformance contributors | copy of tests/step-conformance/tiny_pin.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
| tests/kernel-cases/import/3098-bspline-edges-over-tiny-knot-span/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3098 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/3208-extruded-ellipse-face-over-parameter-seam/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3208 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/3352-vertex-off-its-edge-curve/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3352 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/3730-void-oriented-shell-false/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3730 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/3871-cone-base-circle-with-vertex-loop/input.step | step-conformance contributors | copy of tests/step-conformance/cone_point_vertex.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#3871 | CC0-1.0 |
| tests/kernel-cases/import/3871-cone-bounded-only-by-its-base-circle/input.step | step-conformance contributors | copy of tests/step-conformance/cone_point.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#3871 | CC0-1.0 |
| tests/kernel-cases/import/3880-cylinder-seam-edge/input.step | step-conformance contributors | copy of tests/step-conformance/cylinder_seam.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#3880 | CC0-1.0 |
| tests/kernel-cases/import/3880-nurbs-hemisphere-dome/input.step | step-conformance contributors | copy of tests/step-conformance/dome_nurbs.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#3880 | CC0-1.0 |
| tests/kernel-cases/import/3963-closed-spline-edge-vertex-past-start/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3963 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/4005-cylinder-written-as-near-zero-cone/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#4005 | MIT OR Apache-2.0 |
| tests/kernel-cases/import/4006-plane-written-as-cone-of-nearly-right-angle/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#4006 | MIT OR Apache-2.0 |
| tests/ssi-corpus/cases/*.json.gz, tests/ssi-corpus/LICENSE-MIT, tests/ssi-corpus/LICENSE-APACHE | ssi-corpus contributors (jyviko) | 119 surface/surface intersection cases with exact ground truth from https://github.com/jyviko/ssi-corpus (cases/data, b04c8a6), gzip-compressed, unchanged; SSI scorer fixtures (tests/ssi-corpus/README.md) | MIT OR Apache-2.0 |
| tests/ssi-corpus/cases/y23_*.json.gz | Yang, Jia and Yan (paper authors: published surfaces, figures and topology); ssi-corpus contributors (reconstructions and truth) | 25 cases built from Yang, Jia & Yan 2023, "Topology Guaranteed B-Spline Surface/Surface Intersection", ACM Transactions on Graphics 42(6), Article 211; each cites the paper in its `source` field | CC-BY 4.0 (paper); MIT OR Apache-2.0 (case files) |
| tests/step-regressions/rod_doubled_edge.step | SolidCraft contributors | written by SolidCraft from the commands of #4598 (a bored rod moved 100 mm and exported); import regression fixture | MIT OR Apache-2.0 |
| tests/oracle/17-loft/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/17-loft/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/23-bracket-multifeature/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/23-bracket-multifeature/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
@@ -174,6 +192,10 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/platforms/macos/dmg/background.tiff | SolidCraft contributors | original disk image window background (blueprint sheet: grid, double border, circles, centre lines, leader arrow, title block) drawn by assets/platforms/macos/dmg/make_background.py in IBM Plex Sans/Mono; 1x and 2x representations | MIT OR Apache-2.0 |
| assets/platforms/macos/dmg/make_background.py | SolidCraft contributors | authoring script that draws the disk image background (Pillow); not part of any build | MIT OR Apache-2.0 |
| assets/platforms/macos/dmg/layout.json | SolidCraft contributors | disk image window size, icon size and icon positions shared by the background script and settings.py | MIT OR Apache-2.0 |
| assets/platforms/macos/dmg/settings.py | SolidCraft contributors | dmgbuild settings (MIT, https://github.com/dmgbuild/dmgbuild) for the SolidCraft disk image; original settings, no dmgbuild code copied | MIT OR Apache-2.0 |
@@ -284,6 +306,8 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/model/circle_tt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/circle_ttt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/coil.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/colored_components.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/colored_components_off.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/combine.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/component.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/compute.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -517,6 +541,13 @@ Editable drawing code, native projects and shipped exports are collected under `
| docs/book/src/og.png | SolidCraft contributors | copy of assets/branding/web/source/og.png staged by cargo xtask book (gitignored) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/path_flange.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| docs/book/src/generated/hem.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/hem.json`; gitignored), original sheet metal demonstration | MIT OR Apache-2.0 |
| docs/book/scripts/hem.json | SolidCraft contributors | original parametric recipe for an 80 × 50 mm sheet with an 8 mm returned edge, authored for the command guide | MIT OR Apache-2.0 |
| assets/styles/ui/themes/clay.json | SolidCraft contributors | original SolidCraft theme file (Clay, light): warm paper palette chosen for SolidCraft | MIT OR Apache-2.0 |
| assets/styles/ui/themes/clay-dark.json | SolidCraft contributors | original SolidCraft theme file (Clay Dark): warm charcoal palette chosen for SolidCraft | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/cone.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/locales/ar.json | Abdullah (A-AL-ANAZI) | Original Arabic translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/locales/he.json | SolidCraft contributors | Original Hebrew translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro; corner choice at vertices several intersection curves end on; reuse of an operand's meshes (vendor/README.md) |
| vendor/truck-geometry | truck-geometry 0.5.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | intersection curves divided from their neighbours' surface parameters; surface second derivatives (vendor/README.md) |
Agents: start from the router in [AGENTS.md](AGENTS.md), which points to a short skill in [`skills/`](skills/README.md) for each kind of work. This page stays the full human guide.
SolidCraft is an open-source parametric 3D CAD app written in pure Rust.
You sketch on a plane, constrain the sketch, turn profiles into solids, and
change a parameter to watch the timeline rebuild. It runs as a desktop app (Windows, Linux), in
change a parameter to watch the timeline rebuild. It runs as a desktop app (Windows, macOS, Linux), in
the browser (wasm + WebGPU/WebGL2), headless from a CLI, and as an MCP server that AI agents can
model with.
@@ -11,7 +13,7 @@ build it, how the code is laid out, the few rules that aren't negotiable, and wh
needed most.
Before choosing a task, check the [command inventory](docs/features.md) and
[roadmap](ROADMAP.md) for implemented capabilities and current priorities.
[roadmap](docs/roadmap-detail.md) for implemented capabilities and current priorities.
Questions, ideas or want to say what you're working on? Join the [Discord](https://discord.gg/FtU4NQNVDH).
@@ -130,7 +132,9 @@ The [architecture page](docs/architecture.md) has the layer table. The short ver
- Every call into truck goes through `solidcraft_kernel::guard`.
- Every crash fix comes with a regression test.
5. **Everything is a command, with a test.** A new feature means a `CommandSpec` in
`crates/engine/src/cmd/`, engine tests that measure the result (volume, area,
`crates/engine/src/cmd/` with input and output schemas (see
[Adding a command](docs/book/src/dev/commands.md#adding-a-command); a test warns about a
command without them, and will fail it once required), engine tests that measure the result (volume, area,
face/edge/vertex counts against a value you can derive), and for UI work a scenario (below).
6. **One set of camera controls.** Every page or canvas that shows a view (the modelling
viewport, the Render preview, and any new page such as a flat pattern or drawing) calls
@@ -157,6 +161,9 @@ the most complete description of the house style.
A geometry change that changes the robustness goldens re-records them in the same pull request:
`ROBUSTNESS_GOLDENS_BLESS="why it changed" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens::`
(see [docs/robustness.md](docs/robustness.md)); CI runs every seed on pull requests that touch geometry.
A change to the persistent names of faces and edges, or to what a stored name finds, re-records the
corpus's names in the same pull request, saying why:
`NAMING_CORPUS_BLESS=1 cargo test --profile ci -p solidcraft-cli --test naming_corpus` (`tests/naming/`).
[docs/mutation-testing.md](docs/mutation-testing.md) shows how to find kernel behaviour these
black-box suites miss.
@@ -190,9 +197,11 @@ in the PR. `cargo xtask test` builds the tests with it and then runs every test
shared bound on top-level test/side commands for `cargo xtask test` and `cargo xtask ci`.
For example, `SOLIDCRAFT_TEST_JOBS=2` allows two commands at once; the sequential wasm/doc
worker consumes one of those slots. Every binary and side check still runs after failures.
Without the variable the existing concurrency is unchanged. `RUST_TEST_THREADS`,
`ACTION_MATRIX_JOBS` and Cargo's build-job setting remain independent caller overrides;
this limit does not choose their values or change test filters, timeouts or baselines.
Without the variable the existing concurrency is unchanged. With it set, each test binary
inherits `RUST_TEST_THREADS` and `ACTION_MATRIX_JOBS` of `max(1, cpus / jobs)` — the machine's
parallelism shared between the binaries in flight — unless you set either one explicitly;
Status as of **2026-10-14**. SolidCraft supports parametric modelling (sketch →
features → parametric timeline) as an open, pure-Rust, agent-drivable application.
## At a glance
| Question | Answer |
|---|---|
| Where are we? | **M0 and M13 done; M1, M2, M5–M7, M9, M10, M14 in progress.** Sketch with solver and the full SKETCH create/modify set; extrude, revolve (two sides, symmetric, to object), fillet/chamfer (variable radius, chord length, a radius per edge, corner patches), holes, threads (cosmetic and modelled), patterns, mirror, shell (inside/outside/both), draft, loft, sweep, coil, split body and split face, delete face, construction planes, axes and points; persistent naming of faces and edges; parameters and configurations; components, joints, motion studies, exploded views; sheet metal and a plastic enclosure subset; STEP/IGES/3MF/STL/OBJ/DXF; FreeCAD/Rhino import; autosave and crash recovery; desktop and browser (wasm) app, CLI, MCP server. |
| Features | Every registered command by area — [docs/features.md](docs/features.md) (`cargo xtask features`); registry breadth does not establish workflow or geometric depth |
| Reference tests | [Current case count and results](docs/oracle.md): public cases in `tests/oracle/` with analytic or FreeCAD-computed expected values, checked by the reference-test job in [CI](.github/workflows/ci.yml). |
| Tests | Solver, profiles, kernel booleans/blends/measures, expressions, timeline, persistent naming, file formats and migrations, assemblies, sheet metal, engine end-to-end, hostile-input fuzz over every command and headless UI scenarios. `cargo xtask ci` reports the current passing counts. |
| Time to a useful alpha (M0–M6) | ≈ **82 agent hours** remain (sum of the M1–M6 estimates below) |
Optional GPU viewport ambient occlusion is available in View settings and Graphics preferences,
with persistent settings, Performance/Quality graphics presets (Quality combines AO, Fine tessellation and adaptive 2× supersampling; manual combinations show Custom), and the
`view.ambient_occlusion` and `view.graphics_preset` commands. See [display settings](docs/ambient-occlusion.md).
## Scope (owner decision, 2026-10-07)
The issue batch adds preservation of blind pockets in through cuts, associative projected
offsets and plastic face placements, XZ (+Y normal) with a v6 migration preserving older
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, Shift-picked line midpoints in Horizontal/Vertical using persisted points and ordinary constraints (#2087), driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces, at distances from reference edges, and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, Shift-picked line midpoints in Horizontal/Vertical using persisted points and ordinary constraints (#2087), drawing snaps to where two lines, circles or arcs cross (#4967), driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
| M3 | General fillets and chamfers (curved edges, chains, vertex blends); saddle rolling-ball surfaces now use continuous seam sections and arc-length fitting with shared contact boundaries and bounded accuracy checks; tiny rim edges remain selected instead of nearby wall seams when filleting (#2498) | in progress | 30 |
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), FreeCAD/Rhino solids through cadmpeg (done, desktop, browser and CLI), 3MF export and 3MF/STL import as mesh bodies (done), 3MF with B-rep face tags ([docs/formats/3mf-face-tags.md](docs/formats/3mf-face-tags.md), done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
| M5 | Persistent naming of faces and edges (docs/book/src/dev/naming.md): faces named from their exact kernel-reported history (sketch curve sides, caps, revolve sections, primitive sides, blends, shell inner walls, pattern/mirror copies; split pieces told apart by the faces around them, lumps of a cut body named after their faces), edges by their faces; fillets/chamfers, sketches on faces, shell/draft/offset/replace faces, hole placements, joint origins and face appearances resolve by name first and warn when the entity split or vanished: done. sheet metal edges by base sketch line and flange side, shell inner faces, sweep and loft roles: done. kernel-reported face history: done (merged faces answer to every name; operations without history yet fall back to geometric names, flagged unverified; format 14 keeps older designs' geometric names). Left: history through sheet metal, plastic, surface and emboss features; FaceSource certificates through history | in progress | 20 |
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; responsive feature history with width-driven icons/names/title, snapping width resize, bottom/left/browser-stack docking, shared-column collapse, rollback bar, multi-selection and persistent folders), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator, extrude taper), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; Browser Expand All / Collapse All (`browser.expand_all`, `browser.collapse_all`) on the design and component menus; preferences | in progress | 6 |
| M7 | Thin extrude (walls along open or closed curves: centre, inside, outside or two sides; mitred corners, crossing walls joined; #721), sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
| M8 | Surface workspace (#772). Done: Patch (sketch regions; edge loops filled G0, plus `crown`), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane, a face or sketch curves swept onto the surface, keeping a side or the picked regions; mutual trim of two surfaces, extended to meet and stitched, #767), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis, Surface Extrude/Revolve/Sweep/Loft from open and closed curves, Offset, Unstitch and Reverse Normal; the continuity-driven core (`kernel/src/bridge.rs`, G0–G3 Hermite rows) with **Blend** (`surface.blend`, #764: edge chains along one or several faces, closed rims paired seam to seam or from picked seams, a dialog), **Match** (`surface.match`), **Boundary** (`surface.boundary`, #763 — two sketch curves exact with G0–G3, three or four as the flat patch of their loop, and curve networks: Coons and Gordon surfaces through sketch, 3D-sketch or edge curves, a point for a three-sided side, G1/G2 against the faces along one direction's edges, joined into their body on request; G1/G2 on two sides that meet at a corner waits for the Gregory-type corner), **Fair** (`surface.fair`, CV fairing), **continuity analysis** (`inspect.continuity`, #770; a toolbar tool with edge markers and a legend, and curvature combs along face isocurves) and a multi-section **loft** with per-end continuity and crown, and guide rails on solid and surface lofts (one or two, scaling or stretching the sections between; #201). Also in the Class-A pass: G1…G4 blend curves in the sketch, chord and variable-radius fillets on curved edges (with honest refusals where the fit cannot hold), and a G2 corner patch where filleted edges meet (kernel; the command refuses until the patch's sections can be sewn to the blends' contacts). Left: bias/form-factor fillet sections (design recorded), a Gregory-type corner, surface Fillet between separate surface bodies (#769), Ruled (#762), Untrim (#761), curved-face Extend (#760), and every remaining #757–#772 item | deferred | — |
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, closed kinematic loops (four-bar, slider-crank: the loop's other joints follow the driven one on its branch, and driving stops where a loop can no longer close), cams (`joint.cam`: a revolute cam drives a slider or revolute follower along a closed sketch profile or the cam's edges, knife, roller or flat followers, worked out again when the profile changes; drives, studies, closed loops, contact sets), As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), motion analysis (joint values and a point's path charted over a study as value, velocity and acceleration; the parts that collide along a study, with the steps where they do), Exploded views (automatic or by hand, shown without changing placements, and exported as an animation coming apart and back together); interference, per-component physical properties; STEP with assembly structure; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link, Cam and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
| M11 | 2D drawings | deferred | — |
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. macOS Developer ID signing and notarization are in the release workflow (#5749; they switch on when the Apple secrets exist). Windows installer signing and broader performance work remain | in progress | 16 |
| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (config.configure, config.table, config.column/row/activate). Per-row body materials and appearances (library looks, custom colour/opacity and clearing overrides), undoable activation and save/reopen: done. Ordered typed configuration rules, atomic validation and structured rule editing: done. Component appearance and certified semantic face style columns, atomic preview/apply and independent-copy remapping: done. Exact constructed sketch-side and immutable imported-face lineage now preserve legacy names and reject retargeting. Left: ambiguous shifted/two-sided caps | in progress | 4 |
| M13 | Never lose work: atomic saves (temporary file, flush, rename; a crash at any moment leaves the old file or the new one) with the replaced version kept as `<file>.bak`; autosave of unsaved changes to the recovery folder every 5 minutes (preference) and after big operations, only when something changed; "Recover unsaved design?" on the next launch (`doc.recovery_list`, `doc.recover`, `doc.recovery_discard`); a kill-mid-save test (process killed while saving a 0.5 MB design 25 times). File format: `"format": "solidcraft/12"`, older formats upgraded step by step (`doc::format`), newer ones refused with a message, out-of-range numbers and broken placements refused; a corpus of designs saved by M0, components-era, joints-era and current builds (apps/solidcraft-cli/tests/corpus) that must open and evaluate as saved; damaged and hostile variants (truncation, bit flips, hostile values, removed keys, repeated features, deep nesting) never panic | done | 6 |
| M15 | Frames and weldments (#4003): structural members along 2D and 3D sketch lines, butt and mitre corners, trim to faces and bodies, end caps, gussets, cosmetic welds and a cut list. Done: the profile library (`frame.profile.list`, `frame.profile.properties`, `frame.profile.insert`): bar, round bar, rectangular and round tube, angle, channel and I-beam, each SolidCraft's own parametric definition with exact fillet arcs and exact section properties (area, centroid, second moments), tested against hand-computed values; members (`frame.member`): one body per straight line of a 2D or 3D frame sketch, following parameter edits, with a built-in profile (its dimensions parameter expressions) or a profile sketch (its drawn material by alternating nested loops), anchor, angle and mirror; each member's name kept with its line. Left: corners, trims, end caps, gussets, welds, cut list, FRAME tab and dialogs | in progress | 60 |
M6 navigation preferences include **Touchpad**: Shift + pointer movement pans, Alt/Option +
pointer movement orbits without a mouse button, and vertical scrolling zooms. Other presets
retain their saved two-finger scroll preference; camera motion is available through
`view.navigate` and modifier-aware `ui.move` automation.
With the **Fusion** preset and two-finger pan enabled, two fingers pan and Shift + two fingers
orbit freely, with pinch zoom and mouse bindings unchanged. The shared navigation handler
applies the same gestures to model and Render views and blocks orbit on flat pages.
editable, and Automatic size returns to the face's suggestion. Cosmetic/modelled and
full/partial-length options remain available. This does not add tolerance classes or
left-hand threads.
- New Body extrusion and face-offset previews show completed intermediate geometry while a
drag continues, then catch up to the exact final value. Changes to the selected geometry,
operation or feature still discard obsolete results. Distance arrows and their value boxes
follow the current frame's input.
- Large fine modelled threads use pitch-based helix resolution and tighter faceted
reconstruction fallbacks, preserving thin crests and closed shells on large shafts.
## Physical material data
- Physical material card validation (#923/#922) provides typed SI mechanical and
thermal data, per-property sources/conditions and isotropic/orthotropic stability
checks through read-only `material.properties.validate`. It does not assign or
persist properties, provide a solver or enable rendering; see
[the contract and staged follow-ups](docs/physical-material-cards.md).
- Directional elasticity (#994) queries the validated card in material-local axes
through read-only `material.properties.directional`, with source/condition review.
It does not assign body frames, predict structural behavior or enable a solver.
## Library foundations (#785, #786, #787)
- Local scalar libraries (#846) inspect/export bounded portable catalogs and copy exact
selected appearance/material revisions into existing design definitions in one undo step.
Appearance offers local loading/search/export; physical materials use existing commands.
This slice is independent of the render stack and does not change the document format.
See [the contract and limits](docs/book/src/dev/local-libraries.md).
- Proposed phases cover offline bundled/project/user packs, physical materials and
appearances, editable wood mapping, and parameterized component/assembly families.
Stable identity/revision, copied design definitions, provenance and resource limits
apply from the first implementation. Existing material, appearance and standard-part
commands remain the application paths. See [the proposal](docs/book/src/dev/libraries.md)
for source/license review and acceptance gates. This is planning only: pack loading,
texture rendering and expanded component families are not implemented by this entry.
## Render foundation
- Original starter appearances (#1722): 34 opaque recipes through existing commands, with six explicitly unavailable glass recipes and preserved legacy looks; see [the contract](docs/render-starter-appearances.md).
- Bounded linear film accumulation (#1696) supplies caller-admitted mean/variance storage and complete-pass views; actual progressive consumers remain separate. See [the film contract](docs/render-film.md).
- Definition-local visual mapping (#1214) supplies a bounded numerical frame, physical repeat coordinates and a deterministic one-time grain-axis suggestion. It adds no saved record or texture consumer; see [the mapping contract](docs/render-visual-mapping.md).
- Explicit texture normalization (#1218) produces completed float RGBA with
role-aware linear color/data semantics, bounded matrix/TRC ICC conversion and
opt-in EXIF orientation. It retains high-precision raw samples and records lossy
gamut clipping only when explicitly requested. It is not a renderer/resource
consumer; see [the supported policy and limits](docs/render-texture-normalization.md).
- Opt-in opaque PBR (#1046) connects persisted surface parameters to the integrated
product viewport and CPU PNG, using a fixed direct-light studio. Geometry and mass
stay unchanged. HDRI, transmission and textures remain separate gates.
See [the consumer and limitations](docs/render-opaque-pbr.md).
- Combined integration is tracked in #842, with independent scalar libraries in
#863 and richer PBR/material-card transport in #1086. See [phase boundaries and validation gates](docs/render-integration.md).
Render/library consumers use the merged asynchronous picker (#1127). Material-card
UI (#1094), texture mapping and HDRI lighting remain explicit follow-ups.
- R4a adds bounded CPU product-image jobs, cancellation and atomic PNG export through
engine commands, the Render panel, CLI and persistent MCP. Immutable captures and
results stay outside persistence/undo; native workers and cooperative browser tasks
share the same work protocol. See [jobs, limits and export](docs/render-jobs.md).
- R3 adds an overlay-free product scene and the integrated Render workspace, with
runtime GPU host capabilities, per-document drafts and named camera/background
controls. Design keeps its camera and drawing policy. Raster transparency remains
approximate; PBR and environments are later slices. See
[workspace and scene contracts](docs/render-workspace.md).
- R2 adds bounded named render setups in document format 21 and the read/save
and selected occurrence frames; cancel and successful edits preserve one-step undo history.
Placed feature edits preview through the edited timeline entry, and cylindrical Emboss guides
use the sketch's actual target, author or uniquely placed third-component frame.
Revolve rotators follow the actual signed axis and support symmetric and two-sided extents;
dragging a To Object extent converts its derived value to an explicit angle. Legacy reversed-axis
symmetric geometry is preserved through format 11's explicit legacy convention. New revolves
use the sketch-normal direction for each selected profile, including To Object and curved
target trimming, independently of the selected axis sign.
- Cylindrical emboss/deboss wraps bounded orthogonal line profiles, including holes, concave
outlines and nested islands, onto complete exterior cylinders or coaxial sleeves with exact
radial walls. New features infer a unique eligible cylinder when the face field is empty;
ambiguous targets require an explicit face. Saved target
and author occurrence paths retain the chosen placements; missing/split faces refuse rather
than retarget. General free-form profiles, seams, stepped cylinders and other curved surfaces
remain unsupported. STEP round-trip and independent radial/volume checks cover this subset.
- Shell and offset faces work on bodies of planes, cylinders, cones, spheres and tori (convex or not:
filleted boxes, bosses, domes, countersinks; corners where four or more planes meet, such as a
floor falling to a drain, as long as the corner's edges are all convex or all concave); draft on convex planar bodies and on walls (planes, cylinders → cones) between caps; lofts through three or more sections are smooth (B-spline sides through every section), two
sections ruled. Circular-meridian barrels retain curved inner walls through either axial
opening (#2421). Solid lofts preserve holes through sections with matching hole counts
(#2435), including inner walls and caps with openings; a hole cannot appear or disappear
between sections, and sections with holes cannot collapse to a point.
- Sketch corner tools: fillet and all three chamfer modes pick a connected corner even when
its endpoint overlaps the fixed origin or a loose point. Ordinary drawing still snaps to
the origin; clicked UI regressions verify geometry, closed profiles and undo.
islands share the same boundaries in flat preview, DXF and unfolded solids. Detached material
pieces survive unfold/refold as one logical sheet group, including split flanged sheets.
Cut-outs made in the flat pattern cross bends at any angle or curved: their edges fold to
helices and curves on the bend, their walls square to the sheet. Free-form bases use bounded
sampling.
Parallel prismatic partial-depth cuts retain pocket side and depth through transfer and
unfold/refold, with separate pocket contours in preview and DXF. Planar Intersect preserves
exact holes and retained islands, including the union of multiple selected profiles, and
records complementary depth layers as pockets while retaining the stock gauge. Unsupported
tapered or bend-crossing machining refuses atomically instead of restoring material later.
Sketches attach to a sheet's flat faces (base and flanges, either side) by panel, in the
flat pattern's coordinates, and follow it through rebuilds and unfold/refold.
A flange on another flange's far edge (a return) follows that flange's closed end and keeps
clear of the flange beside it, and the returns of one feature are mitred toward each other
with the rule's gap where their flanges meet at a corner; returns of separate features, and
flanges on returns, are not closed toward each other.
- Rolling-ball fillet accuracy: independent offset-cylinder tee references check surface
distance, actual trimmed tessellation, supporting-cylinder contacts and tangency at two
scales. The measured trimmed-part error is below 0.0001 mm; this is an analytic geometry
check. Under-resolved fits return an error.
- Contact driving checks the trajectory between clear endpoints, including linked joints,
playback and sparse motion exports. Checks use bounded tessellation samples, not exact
continuous collision detection; local thin walls and small details remain limitations.
See [contact-set limits and recovery](docs/contact-sets.md).
## Assembly dragging
The design's first part is grounded when it is made, and a left-button drag repositions a free part (`occurrence.drag`: one undo step, exact occurrence paths including repeated jointed definitions, any floating-chain member carries the chain, grounded parts and joints hold where present, Escape cancels).
## 3D sketch bridges
3D sketch drawing snaps to points on other visible sketch planes without flattening them onto
the active plane. Bridge endpoints capture their source positions; associative endpoint links,
construction-plane selection of 3D wires and loft guide rails remain separate work.
Feature dialog header chevrons collapse and restore the form without discarding selections
or edited values, including the Appearance panel.
## Construction on 3D paths
Plane Along Path has a viewport path selector, fractional position, live preview and timeline
editing. Construction planes and points resolve drawn 3D wires without applying their sketch
plane a second time. Curved paths use sampled segments; multi-curve chains, absolute distance
and reference-object placement remain separate work, as do loft rails and centerlines.
## Design properties (#1015)
- Designs carry project, part number, revision, author, organization, description and licence
in an optional `metadata` object (File → Design Properties…, `document.metadata.*`,
`inspect_design`). STEP and IGES headers and 3MF model metadata take the fields their format
has a place for. Other tools keep their own data under a reverse-DNS key, kept as it is
through open, edit and save and never exported. See [design properties](docs/book/src/guide/files.md#design-properties).
Per-component properties (a part number per component) can follow with the same type.
## Document-opening performance
- Desktop STEP import prepares consecutive independent solids in bounded batches with up
to eight workers, retaining serial traversal, result order and persistent source identities.
The web path remains serial. Original multi-solid fixtures show 64–81% lower complete
import times; single-solid assembly parts are not scheduled across nodes. See
[measurements and limits](docs/perf.md#parallel-step-body-preparation-2026-10-12).
- Design containers (#303, phase one) separate and deduplicate STEP, raw images and
lossless binary mesh arrays while keeping plain JSON as a supported Save As/export
format. Generated-mesh storage measurements reduce serialization and decoding work;
a seekable reader defers original blobs until after provisional display.
See [container format](docs/design-container.md) and [performance measurements](docs/perf.md).
- Warm saves with explicit `cache: true` can include a versioned native B-rep cache through SolidCraft-owned topology
and geometry records, with a separate truck adapter and bounded binary reader. Exact
face-source certificates and operation history survive codec round trips. A provisional
picture can be generated before original blobs are read; editing and measurement wait
for authoritative reconstruction. Cached metadata binds preview and reconstruction to
the same document despite concurrent file writes. Default saves omit this cache and never force cold
evaluation or meshing; explicit `preview: true` remains a separate warm-mesh option.
Keys, resource budgets and document/revision guards reject incompatible, hostile or
stale results. A second modelling kernel still requires its own tested adapter.
- Warm saves with explicit `cache: true` can also carry a bounded PNG thumbnail from already cached meshes.
Home reads it without rebuilding the design when its local picture is missing;
mismatched or malformed thumbnails fall back to normal generation. The image is
kernel-independent and saving never forces cold meshing to produce it.
- Parameters with units and expressions, and configurations.
- Faces and edges keep their names through edits, so references survive changes.
- Components, assemblies, joints, motion studies and exploded views.
- Sheet metal with flat patterns and DXF export, and a plastic-enclosure subset.
- A Render workspace with GPU rendering, studio lighting, materials and image export
(first release in 0.22.0).
- Import and export of STEP, IGES, 3MF, STL, OBJ and DXF, and FreeCAD and Rhino files.
- Autosave and crash recovery, and the browser build of the same app.
The full list of commands is in [docs/features.md](docs/features.md). Release notes for each
version are on the [releases page](https://github.com/tangency-labs/solidcraft/releases).
## In progress now
Each feature below is a [milestone](https://github.com/tangency-labs/solidcraft/milestones) holding
the issues for it, so the count shows how far along it is. Counts are as of 2026-10-25, one per
feature or request. Work merged on a feature branch (`drawings`, `render`) counts as built but
reaches the app only when that branch merges. They are rough, not a promise.
- **[Rendering & materials](https://github.com/tangency-labs/solidcraft/milestone/17)**: in progress, 9 of 34 pieces done, 3 of them built only on the `render` branch and in the app when it merges. The basic Render workspace is already in releases (0.22.0); the interface rework (#6281) and the material and light libraries are what is left.
- **[Drawings](https://github.com/tangency-labs/solidcraft/milestone/8)**: in progress, 3 of 21 pieces built (2 in releases, 1 on the `drawings` branch), and 3 more partly built there (sheets and borders, bill-of-materials types, the paper scene). Not in the app yet: it lands when the `drawings` branch merges. Today you can export the current view as a DXF.
- **[Sheet metal](https://github.com/tangency-labs/solidcraft/milestone/15)**: in progress, 1 of 30 requests done. Flanges, hems, unfold and flat patterns already work; the 30 are extensions, mostly the per-edge flange options (segments, miters, partial flanges, reliefs) and bend allowance.
- **[Sketching](https://github.com/tangency-labs/solidcraft/milestone/14)**: in progress, 12 of 50 pieces done. Dimension types, selection and snapping options.
- **[Solid modeling](https://github.com/tangency-labs/solidcraft/milestone/2)**: in progress, 9 of 50 pieces done. Blends, holes, threads and patterns.
- **[Assemblies & joints](https://github.com/tangency-labs/solidcraft/milestone/3)**: in progress, 4 of 19 pieces done. Joints, motion studies and exploded views already work; placement rules, linked sub-assemblies, fastener stacks and weldment corners are the pieces left.
- **[Import & export](https://github.com/tangency-labs/solidcraft/milestone/5)**: in progress, 7 of 39 pieces done. FreeCAD, SolidWorks and point-cloud import, and mesh export options.
- **[AI agents & MCP](https://github.com/tangency-labs/solidcraft/milestone/18)**: in progress, 2 of 8 pieces done. Agent chat and the MCP server.
- **[Interface, web app & plugins](https://github.com/tangency-labs/solidcraft/milestone/4)**: in progress, 31 of 96 pieces done. Ribbon redesign, navigation, themes, the web app and plugins.
- **[SMLib kernel migration](https://github.com/tangency-labs/solidcraft/milestone/1)**: in progress, 1 tracking issue open. The kernel is moving from truck to NVIDIA's SMLib (usd-brep, Apache-2.0) on the `kernel-smlib` branch; it is not in releases yet. Tracked in [#4012](https://github.com/tangency-labs/solidcraft/issues/4012).
## Next
- **[Surfacing](https://github.com/tangency-labs/solidcraft/milestone/16)**: next, 6 of 14 pieces done. Patch, trim, blend and match exist; the rest of the surface toolset is planned.
- **[Manufacturing / CAM](https://github.com/tangency-labs/solidcraft/milestone/9)**: next, 1 pieces tracked, none done. There is no CAM in SolidCraft yet. Send to Slicer and a first toolpath slice are in review; proposals are [#1153](https://github.com/tangency-labs/solidcraft/issues/1153) and [#1621](https://github.com/tangency-labs/solidcraft/issues/1621).
Features are not tied to a release or a date. Work lands in the daily releases as it is finished.
Anything without a milestone, such as bug fixes, is not a feature and is not listed here.
## How releases work
- Releases ship daily, as numbered versions (v0.22.0, v0.22.1) with nightly pre-releases in
between. They are delivered through the in-app updater.
- SolidCraft tells you when a newer stable release exists and never updates without asking.
Where it can replace itself safely (AppImage, Windows installer and portable builds) there is
an **Update now** button; downloads are checked against the release's SHA-256 list. See
[updates and privacy](docs/book/src/guide/updates.md).
- Design files carry a format number. A new version opens designs from older versions, but an
older version refuses a design saved by a newer one (0.22.0 writes format 35, and 0.21.0 and
earlier will not open it). Each release's notes say when the format changed. Keep a copy of
an important design before you move to a new version.
## Licence
SolidCraft is licensed MIT OR Apache-2.0, at your option; see [LICENSE-MIT](LICENSE-MIT) and
[LICENSE-APACHE](LICENSE-APACHE). Versions already released stay available under those licences
constUSAGE_RUN: &str=" solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
constUSAGE_RUN: &str=" solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--profile-out trace.json] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n (--profile also records where the time goes, SMLib's calls, kernel operations, features, commands and import stages,\n and prints the table to stderr: count, total, self time and longest per span; --profile-out FILE writes the spans\n as a Chrome trace for chrome://tracing or ui.perfetto.dev; SOLIDCRAFT_PROFILE=1 does the same for any command)\n";
/// `eval`'s lines in the usage.
constUSAGE_EVAL: &str=" solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect; or --in FILE)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
constUSAGE_EVAL: &str=" solidcraft-cli eval <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--json-out FILE] [--profile] [--profile-out trace.json] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.solidcraft|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n (--profile and --profile-out as for run)\n";
/// `script`'s lines in the usage.
constUSAGE_SCRIPT: &str=" solidcraft-cli script <design.solidcraft|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
/// `bench-open`'s lines in the usage.
constUSAGE_BENCH_OPEN: &str=" solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
constUSAGE_BENCH_OPEN: &str=" solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] [--profile] [--profile-out trace.json]\n (--profile and --profile-out as for run)\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
constUSAGE_KERNEL_CASES: &str=" solidcraft-cli kernel-cases <case-dir>... [--json] [--dumps=DIR]\n (the kernel regression rig, tests/kernel-cases, on SMLib; --dumps: each case's\n failing SMLib calls leave their inputs for replay in DIR/<area>-<case>)\n";
/// Designs whose names differ from one run to the next today: (design, why). Their records are
/// kept and their differences printed, not failed on, until the cause is fixed.
constUNSTABLE: [(&str,&str);1]=[(
"regressions/closed_sweep_torus_cut",
"the halves of each swept wall lie level along every axis, so which is `#1` and which `#2` follows the order the kernel gives the faces in, which changes from one process to the next",
problems.push(format!("{}: a record without a design",file.path().display()));
}
}
}
}
assert!(
problems.is_empty(),
"{} design(s) rebuild with other names than tests/naming records (if that is intended, record it with NAMING_CORPUS_BLESS=1 and say why in the pull request; - recorded, + now):\n{}",
"ui.agent_chat_privacy":"Nic se nikam neodesílá, jen agentovi, kterého spouštíte pod vlastním účtem.",
"ui.agent_chat_privacy_note":"SolidCraft nic nikam neodesílá: agent, kterého spouštíte, komunikuje se svou službou pod vaším účtem. SolidCraft nikdy nevidí ani neukládá klíč či heslo.",
"ui.agent_chat_privacy_note":"Chat sám nic nikam neodesílá: agent, kterého spouštíte, komunikuje se svou službou pod vaším účtem. SolidCraft nikdy nevidí ani neukládá klíč či heslo.",
"ui.agent_chat_queue":"Zařadit",
"ui.agent_chat_queue_full":"Čeká příliš mnoho zpráv: některou odeberte nebo počkejte na agenta.",
"ui.agent_chat_queued":"Ve frontě",
@@ -1556,6 +1559,7 @@
"ui.ai_http_busy":"endpoint je zaneprázdněný nebo překročil limit; zkuste to později",
"ui.ai_http_key":"endpoint odmítl klíč: zkontrolujte jej v Předvolby > AI",
"ui.ai_http_not_found":"takový endpoint nebo model neexistuje: zkontrolujte základní URL a název modelu",
"ui.ai_http_too_large":"konverzace je pro tento endpoint příliš velká; začněte nový chat",
"ui.ai_key":"API klíč",
"ui.ai_key_hint":"volitelné; uloženo v systémové klíčence",
"ui.ai_key_needs_https":"Klíč se odesílá pouze přes HTTPS nebo na tento počítač.",
@@ -1676,6 +1680,7 @@
"ui.command_done":"{command} hotovo",
"ui.command_palette_hint":"Zadejte příkaz, např. {example} — nebo stiskněte S pro hledání",
"ui.concentric":"Soustřednost",
"ui.cone":"Kužel",
"ui.conic_curve":"Kuželosečka",
"ui.constant":"Konstantní",
"ui.constraints_conflict":"Konflikt vazeb",
@@ -1734,6 +1739,11 @@
"ui.draft":"Úkos",
"ui.drawing":"Výkres",
"ui.drill_point":"Špička vrtáku",
"ui.easyprint_consent":"EasyPrint je slicer od Printables v prohlížeči. Aby se v něm model otevřel, SolidCraft jej nahraje na servery Printables. Odkaz vyprší po 30 minutách a musíte být přihlášeni k Printables. Pokračovat?",
"ui.easyprint_failed":"Nahrání do EasyPrint se nezdařilo: {error}",
"ui.easyprint_opened":"Otevřeno v EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, v prohlížeči)",
"ui.easyprint_uploading":"Nahrávám na Printables…",
"ui.agent_chat_privacy":"Intet sendes andre steder end til den agent, du kører, under din egen konto.",
"ui.agent_chat_privacy_note":"SolidCraft sender intet nogen steder: Agenten, du kører, kommunikerer med sin egen tjeneste under din egen konto. SolidCraft ser eller gemmer aldrig en nøgle eller adgangskode.",
"ui.agent_chat_privacy_note":"Chatten sender selv intet nogen steder: Agenten, du kører, kommunikerer med sin egen tjeneste under din egen konto. SolidCraft ser eller gemmer aldrig en nøgle eller adgangskode.",
"ui.agent_chat_queue":"Sæt i kø",
"ui.agent_chat_queue_full":"For mange beskeder venter: Fjern en, eller vent på agenten.",
"ui.agent_chat_queued":"I kø",
@@ -1737,6 +1740,7 @@
"ui.ai_http_busy":"endepunktet er optaget eller har nået sin grænse; prøv igen senere",
"ui.ai_http_key":"endepunktet afviste nøglen: Kontrollér den i Indstillinger > AI",
"ui.ai_http_not_found":"endepunktet eller modellen findes ikke: Kontrollér basis-URL og modelnavn",
"ui.ai_http_too_large":"samtalen er for stor til dette endpoint; start en ny chat",
"ui.ai_key":"API-nøgle",
"ui.ai_key_hint":"valgfri; gemmes i systemets nøglering",
"ui.ai_key_needs_https":"En nøgle sendes kun via HTTPS eller til denne computer.",
@@ -1861,6 +1865,7 @@
"ui.command_done":"{command} udført",
"ui.command_palette_hint":"Skriv en kommando, f.eks. {example} — eller tryk S for at søge",
"ui.concentric":"Koncentrisk",
"ui.cone":"Kegle",
"ui.conic_curve":"Keglesnitskurve",
"ui.constant":"Konstant",
"ui.constraints_conflict":"Bindinger er i konflikt",
@@ -1919,6 +1924,11 @@
"ui.draft":"Slipvinkel",
"ui.drawing":"Tegning",
"ui.drill_point":"Borspids",
"ui.easyprint_consent":"EasyPrint er Printables' slicer i browseren. For at åbne modellen der uploader SolidCraft den til Printables' servere. Linket udløber efter 30 minutter, og du skal være logget ind på Printables. Fortsæt?",
"ui.easyprint_failed":"Upload til EasyPrint mislykkedes: {error}",
"ui.easyprint_opened":"Åbnet i EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, i browseren)",
"ui.easyprint_uploading":"Uploader til Printables…",
"ui.save_mesh_no_slicer":"Ingen slicer fundet: Vælg Brugerdefineret…, og vælg dit slicerprogram, eller slå Send til slicer fra",
"ui.save_mesh_pick_bodies":"Vælg de emner, der skal gemmes",
"ui.save_mesh_temp_failed":"Kunne ikke oprette {folder}: {error}",
"ui.saved_to":"Gemt: {path}",
"ui.sbox_pin":"Fastgør øverst i S-menuen",
"ui.sbox_unpin":"Fjern fastgørelse",
@@ -2246,7 +2256,6 @@
"ui.selection_shift_add_only":"Shift+klik tilføjer uden at fjerne",
"ui.selection_shift_add_only_tip":"Slået til: Shift tilføjer til markeringen. Ctrl/Cmd tilføjer eller fjerner. Slået fra (standard): Både Shift og Ctrl/Cmd tilføjer eller fjerner.",
"ui.send":"Send",
"ui.send_to_slicer":"Send til slicer",
"ui.sent_to_slicer":"Sendt til {slicer}",
"ui.sheet_gap":"Mellemrum",
"ui.shell":"Udhulning",
@@ -2344,6 +2353,7 @@
"ui.untitled":"Uden titel",
"ui.up_axis_note":"Hvilken akse der peger opad i visningerne (aksevisning, Hjem, Se på). Designs ændres ikke.",
"ui.updateToCurrentView":"Opdatér til aktuel visning",
"ui.upload":"Upload",
"ui.view_centre":"Visningens centrum",
"ui.visual_shaded_hidden_edges":"Skyggelagt med skjulte kanter",
"ui.visual_shaded_with_edges":"Skyggelagt med kanter",
"motion.key_here_hint":"Ein Schlüsselbild bei diesem Schritt mit dem aktuellen Gelenkwert hinzufügen",
"motion.no_collisions":"Keine Kollisionen in der Untersuchung",
"motion.plot_quantity":"Größe",
"motion.step_back":"Schritt zurück",
"motion.step_forward":"Schritt vor",
"motion.to_end":"Zum Ende",
@@ -1113,9 +1118,11 @@
"nav.blender.help":"Mittlere Maustaste dreht, Umschalt+Mitte verschiebt, Strg+Mitte zoomt; rechtes Ziehen lässt die Kamera unverändert",
"nav.free_orbit.help":"Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollneigung bei. Das Einrasten mit Umschalt ist deaktiviert.",
"nav.free_orbit.label":"Freie Drehung",
"nav.free_orbit.snap_help":"Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollausrichtung bei. Halten Sie beim Ziehen zum Drehen die Umschalttaste gedrückt, um Blickrichtung und Rollausrichtung einrasten zu lassen.",
"nav.openscad.help":"Linksziehen auf leerem Hintergrund dreht, Umschalt+Linksziehen zieht ein Auswahlrechteck auf, Skizzengeometrie wird durch Ziehen bearbeitet; rechtes oder mittleres Ziehen verschiebt, Umschalt+rechtes oder Umschalt+mittleres Ziehen zoomt",
"nav.perspective.sketch":"Beim Bearbeiten einer Skizze wird die orthografische Ansicht verwendet. Beenden Sie die Skizze, um die Perspektive umzuschalten.",
"nav.tinkercad.help":"Rechte Maustaste dreht, mittlere oder Umschalt+rechte Maustaste verschiebt",
"nav.touchpad.help":"Umschalt gedrückt halten und den Zeiger zum Verschieben bewegen; mit Alt/Option drehen. Keine Maustaste nötig. Vertikales Scrollen zoomt.",
@@ -1577,7 +1584,7 @@
"ui.agent_chat_pop_out":"In eigenem Fenster öffnen, das Sie auf einen anderen Bildschirm verschieben können",
"ui.agent_chat_privacy":"Nichts geht irgendwohin außer an den Agenten, den Sie unter Ihrem eigenen Konto ausführen.",
"ui.agent_chat_privacy_note":"SolidCraft sendet nichts irgendwohin: Der Agent, den Sie starten, spricht unter Ihrem eigenen Konto mit seinem eigenen Dienst. SolidCraft sieht und speichert nie einen Schlüssel oder ein Passwort.",
"ui.agent_chat_privacy_note":"Der Chat sendet von sich aus nichts irgendwohin: Der Agent, den Sie starten, spricht unter Ihrem eigenen Konto mit seinem eigenen Dienst. SolidCraft sieht und speichert nie einen Schlüssel oder ein Passwort.",
"ui.agent_chat_queue":"Einreihen",
"ui.agent_chat_queue_full":"Zu viele Nachrichten warten: Entfernen Sie eine oder warten Sie auf den Agenten.",
"ui.agent_chat_queued":"Eingereiht",
@@ -1641,6 +1648,7 @@
"ui.ai_http_busy":"der Endpunkt ist ausgelastet oder am Limit; versuchen Sie es später",
"ui.ai_http_key":"der Endpunkt hat den Schlüssel abgelehnt: prüfen Sie ihn unter Einstellungen > KI",
"ui.ai_http_not_found":"Endpunkt oder Modell nicht gefunden: prüfen Sie Basis-URL und Modellnamen",
"ui.ai_http_too_large":"die Unterhaltung ist für diesen Endpunkt zu groß; beginnen Sie einen neuen Chat",
"ui.ai_key":"API-Schlüssel",
"ui.ai_key_hint":"optional; im Schlüsselbund des Systems gespeichert",
"ui.ai_key_needs_https":"Ein Schlüssel wird nur über HTTPS oder an diesen Computer gesendet.",
@@ -1761,6 +1769,7 @@
"ui.command_done":"{command} fertig",
"ui.command_palette_hint":"Befehl eingeben, z. B. {example} — oder S drücken, um zu suchen",
"ui.easyprint_consent":"EasyPrint ist der Slicer von Printables im Browser. Um das Modell dort zu öffnen, lädt SolidCraft es auf die Server von Printables hoch. Der Link läuft nach 30 Minuten ab, und Sie müssen bei Printables angemeldet sein. Fortfahren?",
"ui.easyprint_failed":"Upload zu EasyPrint fehlgeschlagen: {error}",
"ui.easyprint_opened":"In EasyPrint geöffnet",
"ui.easyprint_row":"EasyPrint (Printables, im Browser)",
"ui.easyprint_uploading":"Wird zu Printables hochgeladen …",
"Double-click to rename":"Double-click to rename",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing":"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing",
"Drag to move the boss across the model. It snaps to sketch points on the face.":"Drag to move the boss across the model. It snaps to sketch points on the face.",
"Drag to move the hole; click to remove it":"Drag to move the hole; click to remove it",
"Drag to move the rest across the model. It snaps to sketch points on the face.":"Drag to move the rest across the model. It snaps to sketch points on the face.",
"Drag to move the snap fit across the model. It snaps to sketch points on the face.":"Drag to move the snap fit across the model. It snaps to sketch points on the face.",
"Drag to point the catch in another direction.":"Drag to point the catch in another direction.",
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.":"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.",
"Drive Joint":"Drive Joint",
"Duration":"Duration",
"Edges":"Edges",
@@ -425,6 +430,8 @@
"Remove this point":"Remove this point",
"Rename Feature":"Rename Feature",
"Rename Folder":"Rename Folder",
"Reopen designs":"Reopen designs",
"Reopen the designs that were open when SolidCraft was last closed":"Reopen the designs that were open when SolidCraft was last closed",
"color.gamma":"Showing color values in 0-255 gamma space",
@@ -1133,6 +1160,7 @@
"dxf.progress.write":"Writing DXF (5/5)",
"dxf.tolerance":"Curve tolerance",
"dxf.tolerance_help":"Visible bodies and shown sketches only. Curves use straight segments. Smaller tolerance gives finer curves and uses more time and memory.",
"file.open.drawing_filter":"Designs, CAD files, meshes and drawings",
"file_load.built":"Built so far: {built} of {total} features.",
"file_load.converting":"Converting solid {done} of {total}",
"file_load.inserting":"Inserting geometry",
@@ -1174,6 +1202,26 @@
"help.solid.shell":"Hollows out a solid, leaving walls of the thickness you set, and can open the faces you pick. Use it for boxes, covers and other thin-walled parts.",
"help.solid.sweep":"Drags a sketch profile along a path to make a solid. Use it for pipes, rails, handles and any part of constant section that follows a curve.",
"help.third_party":"Third-party licences",
"hole_presets.confirm_delete":"Confirm delete",
"hole_presets.delete":"Delete",
"hole_presets.delete_prompt":"Delete this personal preset? Existing holes are unchanged.",
"hole_presets.full":"The personal library already contains 64 presets.",
"hole_presets.invalid_name":"Enter a short name without line breaks or control characters.",
"hole_presets.invalid_parameters":"Complete the hole parameters using valid expressions of reasonable length.",
"hole_presets.missing":"The selected preset is no longer available.",
"hole_presets.name":"Preset name",
"hole_presets.name_exists":"A preset with this name already exists. Use a different name.",
"hole_presets.name_hint":"A unique name",
"hole_presets.overwrite":"Save changes to selected preset",
"hole_presets.preset":"Preset",
"hole_presets.recall":"Recall",
"hole_presets.rename":"Rename",
"hole_presets.save":"Save new",
"hole_presets.scope":"Saved in your profile for all projects.",
"hole_presets.select":"Select a preset",
"hole_presets.title":"Personal presets",
"hole_presets.unsupported_options":"This preset uses thread options that this version cannot recall.",
"hole_presets.unsupported_overwrite":"This preset contains options that cannot be overwritten safely.",
"home.back":"Back to {name}",
"home.crashes":"Crash reports",
"home.design_workspace":"Design",
@@ -1322,6 +1370,7 @@
"motion.key_here":"Key here",
"motion.key_here_hint":"Add a key at this step with the joint's value now",
"motion.no_collisions":"No collisions over the study",
"motion.plot_quantity":"Quantity",
"motion.step_back":"Step back",
"motion.step_forward":"Step on",
"motion.to_end":"To the end",
@@ -1331,9 +1380,11 @@
"nav.blender.help":"Middle drag orbits, Shift+middle pans, Ctrl+middle zooms; right drag leaves the camera alone",
"nav.free_orbit.help":"Mouse and 3D mouse rotate through both poles and preserve roll. Shift axis snapping is disabled.",
"nav.free_orbit.label":"Free orbit",
"nav.free_orbit.snap_help":"Mouse and 3D mouse rotate through both poles and preserve roll. Hold Shift during an orbit drag to snap the view direction and roll.",
"nav.fusion.help":"Middle drag pans, Shift+middle orbits, right drag orbits",
"nav.inventor.help":"Middle drag pans, Shift+middle orbits, Ctrl+middle zooms, right drag orbits",
"nav.openscad.help":"Left drag on empty space orbits, Shift+left drag box-selects, sketch geometry drags edit; right or middle drag pans, Shift+right or Shift+middle drag zooms",
"nav.perspective.sketch":"Sketch editing uses orthographic view. Finish the sketch to switch perspective.",
"nav.solidworks.help":"Middle drag orbits, Ctrl+middle pans, Shift+middle zooms, right drag orbits",
"nav.tinkercad.help":"Right drag orbits, middle or Shift+right drag pans",
"nav.touchpad.help":"Hold Shift and move the pointer to pan; hold Alt/Option and move it to orbit. No mouse button is needed. Vertical scroll zooms.",
@@ -1343,6 +1394,10 @@
"nav.touchpad.scroll":"Touchpad uses vertical scroll to zoom. Your two-finger pan setting is kept for other presets.",
"nav.touchpad.zoom":"Zoom (vertical scroll or pinch)",
"needs two joints":"needs two joints",
"network.allow_once":"Allow once",
"network.always_allow":"Always allow this server",
"network.prompt_detail":"This design links to a file on network location {host}. Opening it connects to that computer and may send your Windows sign-in to it. Only allow servers you trust.",
"network.prompt_title":"Network location",
"new configuration":"new configuration",
"new parameter":"new parameter",
"no hole":"no hole",
@@ -1371,6 +1426,7 @@
"pick the path":"pick the path",
"prefs.add_item":"Add the item",
"prefs.alt_frees_manipulators":"Alt / Option also frees manipulator arrows",
"prefs.anti_aliasing_amd_dx12":"Anti-aliasing uses 1× because the startup check drew nothing with multisampling and passed at 1×. You can choose another setting above; restart to apply it if the check passes.",
"prefs.design_note":"What new designs start with. Open designs keep their own.",
"prefs.devices":"3D mouse",
"prefs.dialog_buttons":"Dialog buttons",
"prefs.display":"Display",
"prefs.edges":"Edges and shading",
"prefs.extend":"Extend",
@@ -1421,6 +1478,10 @@
"prefs.graphics_note":"Trade speed for smoothness. Each choice shows its effect on a sample.",
"prefs.graphics_presets_note":"Presets set the choices below together.",
"prefs.history":"History",
"prefs.history_sketches":"Sketches",
"prefs.history_sketches_in_order":"In order",
"prefs.history_sketches_note":"With Sketches set to Under their feature, the History at the left lists each sketch one level in, under the first feature built from it, whenever it is wide enough for names. The timeline at the bottom always keeps the creation order.",
"prefs.history_sketches_under_feature":"Under their feature",
"prefs.hold_stop_snapping":"Hold to stop snapping",
"prefs.icons_only":"Icons only",
"prefs.imported_file":"Imported file",
@@ -1443,12 +1504,17 @@
"prefs.mouse_mapping":"Mouse mapping",
"prefs.navigation":"Navigation",
"prefs.navigation_note":"How the mouse, trackpad and 3D mouse move the view.",
"prefs.network_host_remove":"Remove",
"prefs.network_locations":"Allow network locations in designs",
"prefs.network_locations_note":"Off: a design's linked files on another computer (\\\\server\\share) are opened only from the servers listed here, or after you allow one when asked.",
"prefs.sketch_exit_view_note":"Where the view goes after Finish Sketch, Cancel or a double Esc.",
"prefs.sketch_selection":"Sketch & selection",
"prefs.sketch_selection_heading":"Sketch and selection",
"prefs.sketch_selection_note":"How sketches snap and how clicks select.",
@@ -1487,12 +1558,15 @@
"prefs.timeline_quarter_hour":"−15",
"prefs.toggle_item":"Add the item, or take it out again",
"prefs.toolbar":"Toolbar",
"prefs.top":"Top",
"prefs.translucent":"Translucent",
"prefs.under_browser":"Under browser",
"prefs.under_cursor":"Under cursor",
"prefs.units_precision":"Units and precision",
"prefs.updates":"Updates",
"prefs.view":"View behavior",
"prefs.view_confirm_corner":"OK and Cancel in the view's corner",
"prefs.view_confirm_corner_note":"While a dialog or a sketch is open, a check and a cross at the top right of the view do what the dialog's OK and Cancel do, or finish and cancel the sketch. The view navigator moves down below them.",
"prefs.viewport":"Viewport",
"prefs.wheel":"Wheel",
"prefs.wheel_trackpad":"Wheel and trackpad",
@@ -2184,8 +2258,11 @@
"thread.select_size":"Select size",
"timeline.delete_after_marker_one":"Delete 1 Feature After History Marker",
"timeline.delete_after_marker_other":"Delete {count} Features After History Marker",
"timeline.delete_folders_tip":"Keep their features and nested folders in the history",
"timeline.feature_hint":"Double-click to edit · drag to reorder · right-click for more",
"timeline.feature_warning":"Warning: {message}",
"timeline.group":"New Feature Folder",
"timeline.move_many":"Move Timeline Items",
"timeline.move_to_group":"Move Features to Folder",
@@ -2382,10 +2459,12 @@
"ui.agent_chat_key_in_env":"key set",
"ui.agent_chat_left":"Left",
"ui.agent_chat_limit":"The agent reached the limit of {limit} changes for one message. Send another message to let it go on.",
"ui.agent_chat_login_found":"login found",
"ui.agent_chat_minimize":"Minimise to a strip",
"ui.agent_chat_mode_all":"All",
"ui.agent_chat_mode_ask":"Ask",
"ui.agent_chat_mode_edits":"Edits",
"ui.agent_chat_model_no_tools":"{model} can't use SolidCraft's tools, so it can only chat. Pick a model with tool support.",
"ui.agent_chat_model_tip":"The model the agent uses. Your choice is remembered for this agent.",
"ui.agent_chat_new":"New chat",
"ui.agent_chat_new_confirm":"Start a new chat? The current one is cleared.",
@@ -2394,6 +2473,7 @@
"ui.agent_chat_no_control":"The agent chat's endpoint did not start, so the agent can't reach the design.",
"ui.agent_chat_no_images":"This agent doesn't take pictures",
"ui.agent_chat_no_picture":"There is no picture on the clipboard",
"ui.agent_chat_no_tool_call":"The model didn't use SolidCraft's tools, so the design wasn't changed. Choose a model that supports tool calling; see Local models in the agent chat guide.",
"ui.agent_chat_none_found":"No coding agent found. Install one of these, then press Refresh:",
"ui.agent_chat_privacy":"Nothing goes anywhere except to the agent you run, under your own account.",
"ui.agent_chat_privacy_note":"SolidCraft sends nothing anywhere: the agent you run talks to its own service under your own account. SolidCraft never sees or stores a key or password.",
"ui.agent_chat_privacy_note":"The chat itself sends nothing anywhere: the agent you run talks to its own service under your own account. SolidCraft never sees or stores a key or password.",
"ui.agent_chat_provider_models":"{count} models",
"ui.agent_chat_provider_other":"Other",
"ui.agent_chat_provider_tip":"The provider whose models the next list shows.",
@@ -2482,6 +2562,7 @@
"ui.ai_http_busy":"the endpoint is busy or over its limit; try again later",
"ui.ai_http_key":"the endpoint refused the key: check it in Preferences > AI",
"ui.ai_http_not_found":"no such endpoint or model: check the base URL and the model name",
"ui.ai_http_too_large":"the conversation is too large for this endpoint; start a new chat",
"ui.ai_key":"API key",
"ui.ai_key_hint":"optional; kept in the system keychain",
"ui.ai_key_needs_https":"A key is only sent over HTTPS, or to this computer.",
@@ -2528,9 +2609,27 @@
"ui.ai_test_no_model":"Reached: {count} models, but not {model}",
"ui.ai_tool_sum_more":"{steps}, and {count} more",
"ui.ai_tool_sum_radius":"r {value}",
"ui.ai_tool_sum_saved":"Saved {file}",
"ui.ai_tool_sum_screenshot":"Screenshot",
"ui.ai_tool_sum_then":"{first}, then {next}",
"ui.ai_tool_unfold":"Click to show what it returned",
"ui.ai_tool_waiting":"waiting",
"ui.ai_toolbar_button":"Show Assistant button in application bar",
@@ -2609,6 +2708,7 @@
"ui.command_done":"{command} done",
"ui.command_palette_hint":"Type a command, e.g. {example} — or press S to search",
"ui.concentric":"Concentric",
"ui.cone":"Cone",
"ui.conic_curve":"Conic Curve",
"ui.constant":"Constant",
"ui.constraints_conflict":"Constraints conflict",
@@ -2669,6 +2769,11 @@
"ui.draft":"Draft",
"ui.drawing":"Drawing",
"ui.drill_point":"Drill Point",
"ui.easyprint_consent":"EasyPrint is Printables' slicer in the browser. To open the model there, SolidCraft uploads it to Printables' servers. The link expires after 30 minutes, and you'll need to be signed in to Printables. Continue?",
"ui.history_sketch_listed_under":"Listed under {feature}",
"ui.history_title":"History",
"ui.history_toggle_names":"Show or hide feature names",
"ui.hole":"Hole",
@@ -2876,6 +2984,11 @@
"ui.move":"Move",
"ui.multiple":"Multiple",
"ui.named_views":"Named Views",
"ui.nav_bridge":"Navigate with a device app on this computer",
"ui.nav_bridge_connected":"Connected",
"ui.nav_bridge_port":"Port",
"ui.nav_bridge_tip":"A 3D input device's own program (for example a homemade 3D mouse's driver) moves the view over a local connection, so the mouse pointer stays yours. SolidCraft connects to it on this port on this computer only.",
"ui.nav_bridge_waiting":"Waiting for the device app",
"ui.navigation":"Navigation",
"ui.needs_design":"{command}: open a design first",
"ui.newSketchGroup":"New Sketch Group",
@@ -2894,6 +3007,7 @@
"ui.one_side":"One Side",
"ui.open":"Open",
"ui.open_ends_tip":"Touches nothing",
"ui.open_linked_design":"Open Linked Design",
"ui.open_preferences":"Open Preferences",
"ui.open_recent":"Open Recent",
"ui.operation":"Operation",
@@ -2924,6 +3038,8 @@
"ui.pinToToolbar":"Pin to Toolbar",
"ui.pipe":"Pipe",
"ui.placement":"Placement",
"ui.placement_handles":"Placement handles",
"ui.placement_handles_tip":"Handles on the preview of a Rest, Boss or Snap Fit: drag the marker to move it across the model, turn it by its ring",
"ui.plane":"Plane",
"ui.plane_at_angle":"Plane at Angle",
"ui.plugins":"Plugins",
@@ -2990,9 +3106,8 @@
"ui.right":"Right",
"ui.save":"Save",
"ui.save_as":"Save As",
"ui.save_mesh_no_slicer":"No slicer found: choose Custom… and pick your slicer program, or turn off Send to slicer",
"ui.save_mesh_no_slicer_open":"No slicer found: choose Custom… and pick your slicer program",
"ui.save_mesh_pick_bodies":"Select the bodies to save",
"ui.save_mesh_temp_failed":"Couldn't make {folder}: {error}",
"ui.saved_to":"Saved {path}",
"ui.sbox_pin":"Pin to the top of the S box",
"ui.sbox_unpin":"Unpin",
@@ -3007,8 +3122,9 @@
"ui.selection_shift_add_only":"Shift+click adds without removing",
"ui.selection_shift_add_only_tip":"On: Shift adds items, Ctrl/Cmd toggles. Off (default): Shift and Ctrl/Cmd both toggle.",
"ui.send":"Send",
"ui.send_to_slicer":"Send to slicer",
"ui.send_to_slicer_unavailable":"Send to Slicer needs the desktop app: a browser can't start a program",
"ui.sent_to_slicer":"Sent to {slicer}",
"ui.sent_to_slicer_copy":"Sent to {slicer} as {file}, because the first file is still open there",
"ui.sheet_gap":"Gap",
"ui.shell":"Shell",
"ui.shortcut_capture_add_key":"Add one ordinary key. Escape cancels.",
@@ -3043,6 +3159,15 @@
"ui.slice":"Slice",
"ui.slicer":"Slicer",
"ui.slicer_delete_tip":"{program}\nPress Delete to remove it from the list",
"ui.slicer_lost":"{slicer} was started but can't be found again to send it the file. The file is saved at {path}: open it from there.",
"ui.slicer_no_file":"{slicer} is ready, but there is no file to send to it.",
"ui.slicer_no_response":"{slicer} didn't respond in time. The file is already saved at {path}: send it again once the slicer has started.",
"ui.slicer_resend_hint":"Sending again replaces the file the slicer has open.",
"ui.slicer_single_instance":"Open files in a slicer that is already running",
"ui.slicer_single_instance_tip":"PrusaSlicer, SuperSlicer and Cura load the file in the window that is open instead of starting another. Other slicers are started as they are. PrusaSlicer 3.0 always does this: its launcher can't take a file any other way.",
"ui.slicer_starting":"Starting {slicer}…",
"ui.slicer_still_starting":"Still waiting for {slicer} to start. Nothing else was sent.",
"ui.slicer_stl_fallback":"The 3MF could not be written, so this is an STL.",
"ui.slot":"Slot",
"ui.smooth":"Smooth",
"ui.smooth_wheel_zoom":"Smooth wheel zoom",
@@ -3094,6 +3219,7 @@
"ui.total_angle":"Total angle",
"ui.trackpad_pan_tip":"Two-finger scroll pans; in Fusion, Shift + two fingers orbits. Off: scroll zooms. Mouse wheel and pinch zoom; thumb wheel pans sideways.",
"ui.agent_chat_privacy":"Solo se envían datos al agente que ejecutes, con tu propia cuenta.",
"ui.agent_chat_privacy_note":"SolidCraft no envía nada a otros destinos: el agente que ejecutas se comunica con su propio servicio utilizando tu cuenta. SolidCraft nunca ve ni almacena claves o contraseñas.",
"ui.agent_chat_privacy_note":"El chat no envía nada por sí mismo a otros destinos: el agente que ejecutas se comunica con su propio servicio utilizando tu cuenta. SolidCraft nunca ve ni almacena claves o contraseñas.",
"ui.agent_chat_queue":"Poner en cola",
"ui.agent_chat_queue_full":"Hay demasiados mensajes en espera: elimina uno o espera a que termine el agente.",
"ui.agent_chat_queued":"En cola",
@@ -2037,6 +2043,7 @@
"ui.ai_http_busy":"el servicio está ocupado o ha superado su límite; inténtalo más tarde",
"ui.ai_http_key":"el servicio ha rechazado la clave: compruébala en Preferencias > IA",
"ui.ai_http_not_found":"no existe ese servicio o modelo: comprueba la URL base y el nombre del modelo",
"ui.ai_http_too_large":"la conversación es demasiado grande para este endpoint; empieza un chat nuevo",
"ui.ai_key":"Clave de API",
"ui.ai_key_hint":"opcional; se guarda en el llavero del sistema",
"ui.ai_key_needs_https":"La clave solo se envía mediante HTTPS o a este ordenador.",
@@ -2157,6 +2164,7 @@
"ui.command_done":"{command} completado",
"ui.command_palette_hint":"Escribe un comando, p. ej., {example}, o pulsa S para buscar",
"ui.concentric":"Concéntrico",
"ui.cone":"Cono",
"ui.conic_curve":"Curva cónica",
"ui.constant":"Constante",
"ui.constraints_conflict":"Conflicto entre restricciones",
@@ -2215,6 +2223,11 @@
"ui.draft":"Ángulo de salida",
"ui.drawing":"Dibujo",
"ui.drill_point":"Punta de broca",
"ui.easyprint_consent":"EasyPrint es el slicer de Printables en el navegador. Para abrir el modelo allí, SolidCraft lo sube a los servidores de Printables. El enlace caduca a los 30 minutos y tendrás que haber iniciado sesión en Printables. ¿Continuar?",
"ui.easyprint_failed":"No se pudo subir a EasyPrint: {error}",
"ui.easyprint_opened":"Abierto en EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, en el navegador)",
"ui.easyprint_uploading":"Subiendo a Printables…",
"ui.edit":"Editar",
"ui.editJointLimits":"Editar límites de unión",
"ui.edit_section_analysis":"Editar análisis de sección",
@@ -2271,6 +2284,7 @@
"ui.heading_circular_pattern":"PATRÓN CIRCULAR",
"ui.heading_coil":"HÉLICE",
"ui.heading_combine":"COMBINAR",
"ui.heading_cone":"CONO",
"ui.heading_configure":"CONFIGURAR",
"ui.heading_constraints":"RESTRICCIONES",
"ui.heading_construct":"CONSTRUIR",
@@ -2330,6 +2344,7 @@
"ui.heading_torus":"TORO",
"ui.heading_utilities":"UTILIDADES",
"ui.heading_utility":"UTILIDAD",
"ui.heading_weldments":"SOLDADURA",
"ui.help":"Ayuda",
"ui.hide":"Ocultar",
"ui.hideAxes":"Ocultar ejes",
@@ -2342,6 +2357,7 @@
"ui.history_first":"Ir al principio",
"ui.history_forward":"Avanzar un paso",
"ui.history_last":"Ir al final",
"ui.history_marker_tip":"Marcador del historial: arrastra para retroceder el modelo; las nuevas operaciones se insertan aquí",
"ui.history_title":"Historial",
"ui.hole":"Agujero",
"ui.hole_tap_type":"Tipo de roscado del agujero",
@@ -2505,9 +2521,7 @@
"ui.right":"Derecha",
"ui.save":"Guardar",
"ui.save_as":"Guardar como",
"ui.save_mesh_no_slicer":"No se ha encontrado ningún laminador: elige Personalizado… y selecciona tu laminador, o desactiva Enviar al laminador",
"ui.save_mesh_pick_bodies":"Selecciona los cuerpos que se van a guardar",
"ui.save_mesh_temp_failed":"No se ha podido crear {folder}: {error}",
"ui.saved_to":"Guardado en {path}",
"ui.sbox_pin":"Fijar arriba en la búsqueda S",
"ui.sbox_unpin":"Desfijar",
@@ -2522,7 +2536,6 @@
"ui.selection_shift_add_only":"Mayús+clic añade sin quitar",
"Preview leaves the current design unchanged.":"L'aperçu ne modifie pas la conception actuelle.",
"Profiles/Curves":"Profils/Courbes",
"Quantity":"Grandeur",
"Quantity":"Quantité",
"RECENT":"RÉCENTS",
"REFERENCE":"RÉFÉRENCE",
"RULED SURFACE":"SURFACE RÉGLÉE",
@@ -457,6 +457,7 @@
"cmd.canvas_delete":"Supprimer le canevas",
"cmd.canvas_edit":"Modifier le canevas",
"cmd.canvas_insert":"Canevas",
"cmd.canvas_to_sketch":"Esquisse depuis le canevas",
"cmd.component_activate":"Activer le composant",
"cmd.component_break_link":"Rompre le lien",
"cmd.component_capture_position":"Capturer la position",
@@ -534,6 +535,7 @@
"cmd.file_export":"Exporter",
"cmd.file_insert_mesh":"Insérer un maillage",
"cmd.file_insert_step":"Insérer un STEP",
"cmd.file_open_in_easyprint":"Ouvrir avec EasyPrint",
"cmd.file_save_mesh":"Enregistrer comme maillage",
"cmd.file_slicers":"Slicers installés",
"cmd.frame_member":"Élément de structure",
@@ -949,7 +951,7 @@
"issue.note_opened_copied":"Rapport copié. Collez-le dans le brouillon GitHub, joignez la capture d’écran, puis cliquez sur Submit new issue sur GitHub.",
"issue.note_removed":"Capture d’écran retirée de ce brouillon.",
"issue.note_report_copied":"Rapport copié. Collez-le dans le brouillon GitHub si nécessaire.",
"issue.note_save_failed":"Impossible d’enregistrer la capture d’écran: {error}",
"issue.note_save_failed":"Impossible d’enregistrer la capture d’écran: {error}",
"issue.note_saved":"Capture d’écran enregistrée. Joignez le PNG dans le brouillon GitHub.",
"issue.save_screenshot":"Enregistrer la capture d’écran…",
"joint.from_bodies_hint":"Chaque corps sélectionné passe dans un nouveau composant à lui et reste où il est.",
@@ -983,6 +985,7 @@
"motion.key_here":"Placer une image clé ici",
"motion.key_here_hint":"Ajouter une image clé à cette étape avec la valeur actuelle de la liaison",
"motion.no_collisions":"Aucune collision au cours de l'étude",
"motion.plot_quantity":"Grandeur",
"motion.step_back":"Étape précédente",
"motion.step_forward":"Étape suivante",
"motion.to_end":"À la fin",
@@ -1079,7 +1082,7 @@
"search.parameter":"Paramètre",
"search.recent":"Récents",
"search.run":"Exécuter la commande",
"search.suggest":"Vouliez-vous dire {label}?",
"search.suggest":"Vouliez-vous dire {label}?",
"search.try":"Saisissez un nom de commande, une entité de cette conception ou un paramètre. Pour exécuter une commande avec des valeurs, saisissez son id et un objet JSON : {example}",
"search.valid":"Valide",
"settings.display_quality_note":"Les niveaux plus fins affichent plus lisses les faces et arêtes courbes, mais leur maillage prend plus de temps. Seule la vue change, jamais la conception ni les exports.",
@@ -1094,7 +1097,7 @@
"settings.reset_intro":"Ces réglages ne fonctionnent pas sur cet ordinateur et empêcheraient SolidCraft de démarrer; ils ont donc été modifiés:",
"settings.reset_title":"Des réglages ont été modifiés",
"settings.supersampling_note":"Dessine la vue à une résolution plus élevée puis la réduit: arêtes, lignes et grille plus lisses sur les écrans de définition inférieure à la 4K. Cela coûte du temps GPU: environ 2,25× plus de pixels à 1,5× et 4× plus à 2×.",
"shortcut.json_error":"les paramètres doivent être au format JSON: {error}",
"shortcut.json_error":"les paramètres doivent être au format JSON: {error}",
"shortcut.no_command":"la commande `{id}` n’existe pas ; le texte avant le JSON doit être un ID de commande",
"shortcut.pin":"Épingler en haut de la boîte S",
"shortcut.unpin":"Désépingler",
@@ -1339,7 +1342,7 @@
"ui.agent_chat_pop_out":"Ouvrir dans une fenêtre séparée, déplaçable vers un autre écran",
"ui.agent_chat_privacy":"Rien n'est envoyé ailleurs qu'à l'agent que vous lancez, sous votre propre compte.",
"ui.agent_chat_privacy_note":"SolidCraft n'envoie rien nulle part: l'agent que vous lancez dialogue avec son propre service, sous votre propre compte. SolidCraft ne voit ni ne stocke jamais de clé ni de mot de passe.",
"ui.agent_chat_privacy_note":"Le chat n'envoie rien nulle part de lui-même: l'agent que vous lancez dialogue avec son propre service, sous votre propre compte. SolidCraft ne voit ni ne stocke jamais de clé ni de mot de passe.",
"ui.agent_chat_queue":"Mettre en file",
"ui.agent_chat_queue_full":"Trop de messages sont en attente: retirez-en un ou attendez l'agent.",
"ui.agent_chat_queued":"En file d'attente",
@@ -1403,6 +1406,7 @@
"ui.ai_http_busy":"le point d'accès est occupé ou a dépassé sa limite; réessayez plus tard",
"ui.ai_http_key":"le point d'accès a refusé la clé : vérifiez-la dans Préférences › IA",
"ui.ai_http_not_found":"point d'accès ou modèle introuvable: vérifiez l'URL de base et le nom du modèle",
"ui.ai_http_too_large":"la conversation est trop volumineuse pour ce point d'accès ; commencez une nouvelle discussion",
"ui.ai_key":"Clé d'API",
"ui.ai_key_hint":"facultative; conservée dans le trousseau de clés du système",
"ui.ai_key_needs_https":"Une clé n'est envoyée qu'en HTTPS, ou vers cet ordinateur.",
@@ -1523,6 +1527,7 @@
"ui.command_done":"{command} terminé",
"ui.command_palette_hint":"Tapez une commande, ex. {example} — ou S pour chercher",
"ui.concentric":"Concentricité",
"ui.cone":"Cône",
"ui.conic_curve":"Courbe conique",
"ui.constant":"Constant",
"ui.constraints_conflict":"Contraintes en conflit",
@@ -1581,6 +1586,11 @@
"ui.draft":"Dépouille",
"ui.drawing":"Mise en plan",
"ui.drill_point":"Pointe de foret",
"ui.easyprint_consent":"EasyPrint est le slicer de Printables dans le navigateur. Pour y ouvrir le modèle, SolidCraft l'envoie sur les serveurs de Printables. Le lien expire au bout de 30 minutes et vous devrez être connecté à Printables. Continuer?",
"ui.easyprint_failed":"Échec de l'envoi vers EasyPrint: {error}",
"ui.easyprint_opened":"Ouvert dans EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, dans le navigateur)",
"ui.easyprint_uploading":"Envoi vers Printables…",
"ui.edit":"Modifier",
"ui.editJointLimits":"Modifier les limites de liaison",
"ui.edit_section_analysis":"Modifier l'analyse de coupe",
"ui.agent_chat_privacy":"Nada vai a ningún sitio agás ao axente que executas, coa túa propia conta.",
"ui.agent_chat_privacy_note":"SolidCraft non envía nada a ningún sitio: o axente que executas fala co seu propio servizo coa túa propia conta. SolidCraft nunca ve nin garda ningunha clave nin contrasinal.",
"ui.agent_chat_privacy_note":"O chat non envía nada por si mesmo a ningún sitio: o axente que executas fala co seu propio servizo coa túa propia conta. SolidCraft nunca ve nin garda ningunha clave nin contrasinal.",
"ui.agent_chat_queue":"Pór na cola",
"ui.agent_chat_queue_full":"Hai demasiadas mensaxes agardando: quita unha ou agarda polo axente.",
"ui.agent_chat_queued":"Na cola",
@@ -1389,6 +1392,7 @@
"ui.ai_http_busy":"o punto de acceso está ocupado ou superou o seu límite; téntao de novo máis tarde",
"ui.ai_http_key":"o punto de acceso rexeitou a clave: compróbaa en Preferencias > IA",
"ui.ai_http_not_found":"non existe ese punto de acceso ou modelo: comproba o URL base e o nome do modelo",
"ui.ai_http_too_large":"a conversa é demasiado grande para este punto de acceso; comeza un chat novo",
"ui.ai_key":"Clave da API",
"ui.ai_key_hint":"opcional; gárdase no chaveiro do sistema",
"ui.ai_key_needs_https":"A clave só se envía por HTTPS ou a este ordenador.",
@@ -1509,6 +1513,7 @@
"ui.command_done":"{command} feito",
"ui.command_palette_hint":"Escribe unha orde, p. ex. {example} — ou preme S para buscar",
"ui.concentric":"Concéntrica",
"ui.cone":"Cono",
"ui.conic_curve":"Curva cónica",
"ui.constant":"Constante",
"ui.constraints_conflict":"Conflito de restricións",
@@ -1567,6 +1572,11 @@
"ui.draft":"Ángulo de desmoldaxe",
"ui.drawing":"Debuxo",
"ui.drill_point":"Punta de broca",
"ui.easyprint_consent":"EasyPrint é o slicer de Printables no navegador. Para abrir o modelo alí, SolidCraft cárgao nos servidores de Printables. A ligazón caduca aos 30 minutos e terás que ter a sesión iniciada en Printables. Continuar?",
"ui.easyprint_failed":"Non se puido cargar a EasyPrint: {error}",
"ui.easyprint_opened":"Aberto en EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, no navegador)",
"ui.easyprint_uploading":"Cargando a Printables…",
"ui.edit":"Editar",
"ui.editJointLimits":"Editar os límites da unión",
"ui.edit_section_analysis":"Editar a análise de sección",
@@ -1623,6 +1633,7 @@
"ui.heading_circular_pattern":"PATRÓN CIRCULAR",
"ui.heading_coil":"HÉLICE",
"ui.heading_combine":"COMBINAR",
"ui.heading_cone":"CONO",
"ui.heading_configure":"CONFIGURAR",
"ui.heading_constraints":"RESTRICIÓNS",
"ui.heading_construct":"CONSTRUÍR",
@@ -1682,6 +1693,7 @@
"ui.heading_torus":"TORO",
"ui.heading_utilities":"UTILIDADES",
"ui.heading_utility":"UTILIDADE",
"ui.heading_weldments":"SOLDADURA",
"ui.help":"Axuda",
"ui.hide":"Ocultar",
"ui.hideAxes":"Ocultar os eixes",
@@ -1857,9 +1869,7 @@
"ui.right":"Dereita",
"ui.save":"Gardar",
"ui.save_as":"Gardar como",
"ui.save_mesh_no_slicer":"Non se atopou ningún laminador: escolle Personalizado… e selecciona o teu programa laminador, ou desactiva Enviar ao laminador",
"ui.save_mesh_pick_bodies":"Selecciona os corpos que queres gardar",
"ui.save_mesh_temp_failed":"Non se puido crear {folder}: {error}",
"ui.saved_to":"Gardado {path}",
"ui.sbox_pin":"Fixar arriba na caixa S",
"ui.sbox_unpin":"Desfixar",
@@ -1874,7 +1884,6 @@
"ui.selection_shift_add_only":"Maiús+clic engade sen quitar",
"motion.key_here_hint":"Kulcs hozzáadása ennél a lépésnél a csukló jelenlegi értékével",
"motion.no_collisions":"Nincs ütközés a vizsgálat során",
"motion.plot_quantity":"Mennyiség",
"motion.step_back":"Lépés vissza",
"motion.step_forward":"Lépés tovább",
"motion.to_end":"A végére",
@@ -1268,7 +1271,7 @@
"ui.agent_chat_pop_out":"Megnyitás saját ablakban, amelyet másik képernyőre mozgathat",
"ui.agent_chat_previous_picture":"Előző kép",
"ui.agent_chat_privacy":"Semmi sem megy sehová, csak az Ön által futtatott ügynökhöz, az Ön saját fiókja alatt.",
"ui.agent_chat_privacy_note":"A SolidCraft semmit sem küld sehová: az Ön által futtatott ügynök a saját szolgáltatásával kommunikál, az Ön saját fiókja alatt. A SolidCraft soha nem lát és nem tárol kulcsot vagy jelszót.",
"ui.agent_chat_privacy_note":"A csevegés önmagától semmit sem küld sehová: az Ön által futtatott ügynök a saját szolgáltatásával kommunikál, az Ön saját fiókja alatt. A SolidCraft soha nem lát és nem tárol kulcsot vagy jelszót.",
"ui.agent_chat_queue":"Sorba",
"ui.agent_chat_queue_full":"Túl sok üzenet vár: távolítson el egyet, vagy várja meg az ügynököt.",
"ui.agent_chat_queued":"Sorban",
@@ -1332,6 +1335,7 @@
"ui.ai_http_busy":"a végpont foglalt vagy túllépte a korlátját; próbálja később",
"ui.ai_http_key":"a végpont elutasította a kulcsot: ellenőrizze a Beállítások > AI menüben",
"ui.ai_http_not_found":"nincs ilyen végpont vagy modell: ellenőrizze az alap-URL-t és a modell nevét",
"ui.ai_http_too_large":"a beszélgetés túl nagy ehhez a végponthoz; kezdjen új csevegést",
"ui.ai_key":"API-kulcs",
"ui.ai_key_hint":"opcionális; a rendszer kulcstartójában tárolódik",
"ui.ai_key_needs_https":"A kulcs csak HTTPS-en vagy erre a számítógépre kerül elküldésre.",
@@ -1451,6 +1455,7 @@
"ui.command_done":"{command} kész",
"ui.command_palette_hint":"Írjon be egy parancsot, pl. {example} – vagy nyomja meg az S-t a kereséshez",
"ui.concentric":"Koncentrikus",
"ui.cone":"Kúp",
"ui.conic_curve":"Kúpgörbe",
"ui.constant":"Állandó",
"ui.constraints_conflict":"Kényszerek ütköznek",
@@ -1509,6 +1514,11 @@
"ui.draft":"Dőlés",
"ui.drawing":"Rajz",
"ui.drill_point":"Fúrócsúcs",
"ui.easyprint_consent":"Az EasyPrint a Printables szeletelője a böngészőben. A modell ott való megnyitásához a SolidCraft feltölti azt a Printables szervereire. A hivatkozás 30 perc után lejár, és be kell jelentkeznie a Printablesbe. Folytatja?",
"ui.save_mesh_no_slicer":"Nem található szeletelő: válassza az Egyéni… lehetőséget és jelölje ki a szeletelőprogramot, vagy kapcsolja ki a Küldés szeletelőnek beállítást",
"ui.save_mesh_pick_bodies":"Jelölje ki a mentendő testeket",
"ui.save_mesh_temp_failed":"A(z) {folder} nem hozható létre: {error}",
"ui.saved_to":"Mentve: {path}",
"ui.sbox_pin":"Rögzítés az S doboz tetejére",
"ui.sbox_unpin":"Rögzítés feloldása",
@@ -1816,7 +1826,6 @@
"ui.selection_shift_add_only":"Shift+kattintás eltávolítás nélkül hozzáad",
"ui.selection_shift_add_only_tip":"Be: a Shift hozzáad, a Ctrl/Cmd vált. Ki (alapért.): a Shift és a Ctrl/Cmd is vált.",
"ui.send":"Küldés",
"ui.send_to_slicer":"Küldés szeletelőnek",
"ui.sent_to_slicer":"Elküldve ide: {slicer}",
"ui.sheet_gap":"Rés",
"ui.shell":"Héjazás",
@@ -1908,6 +1917,7 @@
"ui.untitled":"Névtelen",
"ui.up_axis_note":"Melyik tengely mutat felfelé a nézetekben (tengelynézet, Kezdőlap, Ránézés). A tervek változatlanok.",
"ui.updateToCurrentView":"Frissítés az aktuális nézetre",
"ui.agent_chat_privacy":"Tidak ada yang dikirim ke mana pun kecuali ke agen yang Anda jalankan, di bawah akun Anda sendiri.",
"ui.agent_chat_privacy_note":"SolidCraft tidak mengirim apa pun ke mana pun: agen yang Anda jalankan berbicara ke layanannya sendiri di bawah akun Anda sendiri. SolidCraft tidak pernah melihat atau menyimpan key atau kata sandi.",
"ui.agent_chat_privacy_note":"Obrolan tidak mengirim apa pun ke mana pun dengan sendirinya: agen yang Anda jalankan berbicara ke layanannya sendiri di bawah akun Anda sendiri. SolidCraft tidak pernah melihat atau menyimpan key atau kata sandi.",
"ui.agent_chat_queue":"Antre",
"ui.agent_chat_queue_full":"Terlalu banyak pesan menunggu: hapus satu atau tunggu agen.",
"ui.agent_chat_queued":"Mengantre",
@@ -1332,6 +1335,7 @@
"ui.ai_http_busy":"endpoint sibuk atau melebihi batas; coba lagi nanti",
"ui.ai_http_key":"endpoint menolak key: periksa di Preferensi > AI",
"ui.ai_http_not_found":"endpoint atau model tidak ada: periksa URL dasar dan nama model",
"ui.ai_http_too_large":"percakapan terlalu besar untuk endpoint ini; mulai obrolan baru",
"ui.ai_key":"Key API",
"ui.ai_key_hint":"opsional; disimpan di keychain sistem",
"ui.ai_key_needs_https":"Key hanya dikirim lewat HTTPS, atau ke komputer ini.",
@@ -1451,6 +1455,7 @@
"ui.command_done":"{command} selesai",
"ui.command_palette_hint":"Ketik perintah, mis. {example} — atau tekan S untuk mencari",
"ui.concentric":"Konsentris",
"ui.cone":"Kerucut",
"ui.conic_curve":"Kurva Konik",
"ui.constant":"Konstan",
"ui.constraints_conflict":"Batasan bertentangan",
@@ -1509,6 +1514,11 @@
"ui.draft":"Draft",
"ui.drawing":"Gambar Kerja",
"ui.drill_point":"Ujung Bor",
"ui.easyprint_consent":"EasyPrint adalah slicer Printables di browser. Untuk membuka model di sana, SolidCraft mengunggahnya ke server Printables. Tautan kedaluwarsa setelah 30 menit, dan Anda harus masuk ke Printables. Lanjutkan?",
"ui.easyprint_failed":"Gagal mengunggah ke EasyPrint: {error}",
"ui.easyprint_opened":"Dibuka di EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, di browser)",
"ui.easyprint_uploading":"Mengunggah ke Printables…",
"ui.agent_chat_privacy":"I dati vengono inviati solo all'agente che esegui, con il tuo account.",
"ui.agent_chat_privacy_note":"SolidCraft non invia dati ad alcun servizio: l'agente che esegui comunica con il proprio servizio usando il tuo account. SolidCraft non vede né memorizza mai chiavi o password.",
"ui.agent_chat_privacy_note":"La chat non invia da sola dati ad alcun servizio: l'agente che esegui comunica con il proprio servizio usando il tuo account. SolidCraft non vede né memorizza mai chiavi o password.",
"ui.agent_chat_queue":"Metti in coda",
"ui.agent_chat_queue_full":"Troppi messaggi in attesa: rimuovine uno o attendi l'agente.",
"ui.agent_chat_queued":"In coda",
@@ -1405,6 +1428,7 @@
"ui.ai_http_busy":"l'endpoint è occupato o ha superato il limite; riprova più tardi",
"ui.ai_http_key":"l'endpoint ha rifiutato la chiave: controllala in Preferenze > IA",
"ui.ai_http_not_found":"endpoint o modello inesistente: controlla l'URL di base e il nome del modello",
"ui.ai_http_too_large":"la conversazione è troppo grande per questo endpoint; inizia una nuova chat",
"ui.ai_key":"Chiave API",
"ui.ai_key_hint":"facoltativa; conservata nel portachiavi di sistema",
"ui.ai_key_needs_https":"La chiave viene inviata solo tramite HTTPS o a questo computer.",
@@ -1525,6 +1549,7 @@
"ui.command_done":"{command} completato",
"ui.command_palette_hint":"Digita un comando, ad es. {example} — oppure premi S per cercare",
"ui.concentric":"Concentrico",
"ui.cone":"Cono",
"ui.conic_curve":"Curva conica",
"ui.constant":"Costante",
"ui.constraints_conflict":"Vincoli in conflitto",
@@ -1583,6 +1608,11 @@
"ui.draft":"Sformo",
"ui.drawing":"Disegno",
"ui.drill_point":"Punta del trapano",
"ui.easyprint_consent":"EasyPrint è lo slicer di Printables nel browser. Per aprire lì il modello, SolidCraft lo carica sui server di Printables. Il link scade dopo 30 minuti e dovrai aver effettuato l'accesso a Printables. Continuare?",
"ui.easyprint_failed":"Caricamento su EasyPrint non riuscito: {error}",
"ui.easyprint_opened":"Aperto in EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, nel browser)",
"ui.easyprint_uploading":"Caricamento su Printables…",
"ui.edit":"Modifica",
"ui.editJointLimits":"Modifica limiti del giunto",
"ui.edit_section_analysis":"Modifica analisi in sezione",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing":"ビュー上で中ボタンをダブルクリックした時の動作: ホーム(全体を収めたホームビュー)、全体表示(向きを維持して全体を収める)、または無効",
"Drag to move the boss across the model. It snaps to sketch points on the face.":"ドラッグしてボスをモデル上で移動します。面上のスケッチ点にスナップします。",
"Drag to move the hole; click to remove it":"ドラッグで穴を移動、クリックで削除",
"Drag to move the rest across the model. It snaps to sketch points on the face.":"ドラッグしてレストをモデル上で移動します。面上のスケッチ点にスナップします。",
"Drag to move the snap fit across the model. It snaps to sketch points on the face.":"ドラッグしてスナップ フィットをモデル上で移動します。面上のスケッチ点にスナップします。",
"Drag to point the catch in another direction.":"ドラッグしてキャッチの向きを変えます。",
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.":"ドラッグしてレストを 5° 刻みで回転します。Alt または Ctrl を押したままにすると自由に回転します。",
"Drive Joint":"ジョイントを駆動",
"Duration":"時間",
"Edges":"エッジ",
@@ -398,7 +403,7 @@
"Profile / Edges":"プロファイル / エッジ",
"Profiles or curves":"プロファイルまたは曲線",
"Profiles/Curves":"プロファイル/曲線",
"Quantity":"量",
"Quantity":"数量",
"RECENT":"最近使用した項目",
"REFERENCE":"参照",
"RULED SURFACE":"ルールドサーフェス",
@@ -425,6 +430,8 @@
"Remove this point":"この点を削除",
"Rename Feature":"フィーチャーの名前を変更",
"Rename Folder":"フォルダーの名前を変更",
"Reopen designs":"デザインを再度開く",
"Reopen the designs that were open when SolidCraft was last closed":"前回 SolidCraft を閉じたときに開いていたデザインを再度開く",
"nav.tinkercad.help":"오른쪽 드래그는 회전, 가운데 또는 Shift+오른쪽 드래그는 이동입니다",
"nav.touchpad.help":"Shift를 누른 채 포인터를 움직이면 화면이 이동하고, Alt/Option을 누른 채 움직이면 회전합니다. 마우스 버튼은 필요 없습니다. 세로 스크롤로 확대/축소합니다.",
@@ -1217,6 +1223,7 @@
"prefs.design":"설계 기본값",
"prefs.design_note":"새 설계가 시작할 때의 설정입니다. 열려 있는 설계는 자체 설정을 유지합니다.",
"prefs.devices":"3D 마우스",
"prefs.dialog_buttons":"대화 상자 버튼",
"prefs.display":"표시",
"prefs.edges":"모서리 및 음영",
"prefs.extend":"확장",
@@ -1300,12 +1307,15 @@
"prefs.timeline_quarter_hour":"−15",
"prefs.toggle_item":"항목을 추가하거나 다시 제거",
"prefs.toolbar":"도구 모음",
"prefs.top":"위쪽",
"prefs.translucent":"반투명",
"prefs.under_browser":"브라우저 아래",
"prefs.under_cursor":"커서 아래",
"prefs.units_precision":"단위 및 정밀도",
"prefs.updates":"업데이트",
"prefs.view":"뷰 동작",
"prefs.view_confirm_corner":"뷰 모서리에 확인 및 취소",
"prefs.view_confirm_corner_note":"대화 상자나 스케치가 열려 있는 동안 뷰 오른쪽 위의 체크와 X 표시가 대화 상자의 확인과 취소와 같은 동작을 하거나 스케치를 마치고 취소합니다. 뷰 탐색기는 그 아래로 내려갑니다.",
"prefs.viewport":"뷰포트",
"prefs.wheel":"휠",
"prefs.wheel_trackpad":"휠 및 트랙패드",
@@ -1744,7 +1754,7 @@
"ui.agent_chat_pop_out":"다른 화면으로 옮길 수 있는 별도 창으로 열기",
"ui.agent_chat_previous_picture":"이전 이미지",
"ui.agent_chat_privacy":"사용자의 계정으로 실행하는 에이전트 외에는 아무 데도 전송되지 않습니다.",
"ui.agent_chat_privacy_note":"SolidCraft는 아무것도 어디로도 보내지 않습니다. 실행한 에이전트가 사용자의 계정으로 자신의 서비스와 통신합니다. SolidCraft는 키나 비밀번호를 보거나 저장하지 않습니다.",
"ui.agent_chat_privacy_note":"채팅 자체는 아무것도 어디로도 보내지 않습니다. 실행한 에이전트가 사용자의 계정으로 자신의 서비스와 통신합니다. SolidCraft는 키나 비밀번호를 보거나 저장하지 않습니다.",
"ui.agent_chat_queue":"대기열",
"ui.agent_chat_queue_full":"대기 중인 메시지가 너무 많습니다. 하나를 제거하거나 에이전트를 기다리세요.",
"ui.agent_chat_queued":"대기 중",
@@ -1810,6 +1820,7 @@
"ui.ai_http_busy":"엔드포인트가 바쁘거나 한도에 도달했습니다. 나중에 다시 시도하세요",
"ui.ai_http_key":"엔드포인트가 키를 거부했습니다. 환경 설정 > AI에서 확인하세요",
"ui.ai_http_not_found":"엔드포인트나 모델을 찾을 수 없습니다. 기본 URL과 모델 이름을 확인하세요",
"ui.ai_http_too_large":"대화가 이 엔드포인트에 비해 너무 큽니다. 새 채팅을 시작하세요",
"ui.ai_key":"API 키",
"ui.ai_key_hint":"선택 사항, 시스템 키체인에 저장",
"ui.ai_key_needs_https":"키는 HTTPS를 통해서만, 또는 이 컴퓨터로만 전송됩니다.",
@@ -1930,6 +1941,7 @@
"ui.command_done":"{command} 완료",
"ui.command_palette_hint":"명령 입력(예: {example}) — 또는 S 키를 눌러 검색",
"ui.concentric":"동심",
"ui.cone":"원뿔",
"ui.conic_curve":"원뿔 곡선",
"ui.constant":"상수",
"ui.constraints_conflict":"구속 조건 충돌",
@@ -1988,6 +2000,11 @@
"ui.draft":"구배",
"ui.drawing":"도면",
"ui.drill_point":"드릴 선단",
"ui.easyprint_consent":"EasyPrint는 Printables의 브라우저용 슬라이서입니다. 모델을 그곳에서 열기 위해 SolidCraft가 모델을 Printables 서버에 업로드합니다. 링크는 30분 후 만료되며 Printables에 로그인되어 있어야 합니다. 계속할까요?",
"ui.agent_chat_privacy":"Data hanya dihantar kepada ejen yang anda jalankan, melalui akaun anda sendiri.",
"ui.agent_chat_privacy_note":"SolidCraft tidak menghantar data. Ejen berhubung dengan perkhidmatannya melalui akaun anda. SolidCraft tidak melihat atau menyimpan kunci atau kata laluan.",
"ui.agent_chat_privacy_note":"Sembang tidak menghantar data dengan sendirinya. Ejen berhubung dengan perkhidmatannya melalui akaun anda. SolidCraft tidak melihat atau menyimpan kunci atau kata laluan.",
"ui.agent_chat_queue":"Hantar Nanti",
"ui.agent_chat_queue_full":"Terlalu banyak mesej menunggu: buang satu atau tunggu ejen selesai.",
"ui.agent_chat_queued":"Menunggu",
@@ -1389,6 +1392,7 @@
"ui.ai_http_busy":"titik akhir sibuk atau telah melebihi had; cuba lagi kemudian",
"ui.ai_http_key":"kunci ditolak oleh titik akhir: semak dalam Tetapan > AI",
"ui.ai_http_not_found":"titik akhir atau model tidak ditemui: semak URL asas dan nama model",
"ui.ai_http_too_large":"perbualan terlalu besar untuk titik akhir ini; mulakan sembang baharu",
"ui.ai_key":"Kunci API",
"ui.ai_key_hint":"tidak wajib; disimpan dalam rantai kunci sistem",
"ui.ai_key_needs_https":"Kunci hanya dihantar melalui HTTPS, atau ke komputer ini.",
@@ -1509,6 +1513,7 @@
"ui.command_done":"{command} selesai",
"ui.command_palette_hint":"Taip arahan, cth. {example} — atau tekan S untuk mencari",
"ui.concentric":"Sepusat",
"ui.cone":"Kon",
"ui.conic_curve":"Lengkung Kon",
"ui.constant":"Malar",
"ui.constraints_conflict":"Kekangan bercanggah",
@@ -1567,6 +1572,11 @@
"ui.draft":"Sudut Draf",
"ui.drawing":"Lukisan",
"ui.drill_point":"Hujung Gerudi",
"ui.easyprint_consent":"EasyPrint ialah slicer Printables dalam pelayar. Untuk membuka model di sana, SolidCraft memuat naiknya ke pelayan Printables. Pautan tamat tempoh selepas 30 minit, dan anda perlu log masuk ke Printables. Teruskan?",
"ui.easyprint_failed":"Muat naik ke EasyPrint gagal: {error}",
"ui.easyprint_opened":"Dibuka dalam EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, dalam pelayar)",
"ui.easyprint_uploading":"Memuat naik ke Printables…",
"ui.agent_chat_privacy":"Er gaat niets naar buiten behalve naar de agent die u uitvoert, onder uw eigen account.",
"ui.agent_chat_privacy_note":"SolidCraft verstuurt niets: de agent die u uitvoert praat met zijn eigen service onder uw eigen account. SolidCraft ziet of bewaart nooit een sleutel of wachtwoord.",
"ui.agent_chat_privacy_note":"De chat verstuurt zelf niets: de agent die u uitvoert praat met zijn eigen service onder uw eigen account. SolidCraft ziet of bewaart nooit een sleutel of wachtwoord.",
"ui.agent_chat_queue":"In wachtrij",
"ui.agent_chat_queue_full":"Te veel berichten wachten: verwijder er een of wacht op de agent.",
"ui.agent_chat_queued":"In wachtrij",
@@ -1332,6 +1335,7 @@
"ui.ai_http_busy":"het eindpunt is bezet of boven zijn limiet; probeer het later opnieuw",
"ui.ai_http_key":"het eindpunt weigerde de sleutel: controleer deze in Voorkeuren > AI",
"ui.ai_http_not_found":"geen eindpunt of model gevonden: controleer de basis-URL en de modelnaam",
"ui.ai_http_too_large":"het gesprek is te groot voor dit endpoint; begin een nieuwe chat",
"ui.ai_key":"API-sleutel",
"ui.ai_key_hint":"optioneel; bewaard in de sleutelhanger van het systeem",
"ui.ai_key_needs_https":"Een sleutel wordt alleen via HTTPS of naar deze computer verzonden.",
@@ -1451,6 +1455,7 @@
"ui.command_done":"{command} gereed",
"ui.command_palette_hint":"Typ een opdracht, bijv. {example} — of druk op S om te zoeken",
"ui.easyprint_consent":"EasyPrint is de slicer van Printables in de browser. Om het model daar te openen, uploadt SolidCraft het naar de servers van Printables. De link verloopt na 30 minuten en u moet bij Printables zijn ingelogd. Doorgaan?",
"ui.easyprint_failed":"Uploaden naar EasyPrint mislukt: {error}",
"ui.easyprint_opened":"Geopend in EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, in de browser)",
"ui.easyprint_uploading":"Uploaden naar Printables…",
"ui.fillet_corner_ball_tip":"A piece of the rolling ball's sphere, tangent (G1) to each round.",
"ui.fillet_corner_needs_constant":"Smooth patch corners (G2) need Constant radius and Exact fit.",
"ui.fillet_corner_patch":"Smooth patch",
"ui.fillet_corner_patch_tip":"Curvature-continuous (G2): the corner is set back along each edge and filled with a patch, so reflections run through it without a kink.",
"ui.fillet_fit_needs_constant":"Radius fit needs Constant radius.",
"ui.fillet_setback":"Setback",
"ui.fillet_setback_hint":"{factor} × radius",
"ui.fillet_setback_tip":"How far along each edge from the corner the patch starts. Empty: {factor} × the radius.",
"ui.fillet_type_tip":"The section across the edge. Curvature continuous: a curvature-continuous (G2) section between planar faces, so reflections run from the faces into the round without a kink."
},
"ar":{
"ui.fillet_corner_ball":"كرة",
"ui.fillet_corner_ball_tip":"جزء من سطح الكرة المتدحرجة، مماسّ (G1) لكل تقويس.",
"ui.fillet_corner_needs_constant":"زوايا الرقعة الناعمة (G2) تتطلب نصف قطر ثابتًا وملاءمة دقيقة لنصف القطر.",
"ui.fillet_corner_patch":"رقعة ناعمة",
"ui.fillet_corner_patch_tip":"متصل الانحناء (G2): تُرجَع الزاوية على طول كل حافة وتُملأ برقعة، فتمر الانعكاسات عبرها دون انكسار.",
"ui.fillet_fit_needs_constant":"ملاءمة نصف القطر تتطلب نصف قطر ثابتًا.",
"ui.fillet_setback":"مسافة الإرجاع",
"ui.fillet_setback_hint":"{factor} × نصف القطر",
"ui.fillet_setback_tip":"المسافة على طول كل حافة من الزاوية التي تبدأ عندها الرقعة. فارغ: {factor} ضعف نصف القطر.",
"ui.fillet_type_tip":"المقطع عبر الحافة. متصل الانحناء: مقطع متصل الانحناء (G2) بين وجوه مستوية، فتمر الانعكاسات من الوجوه إلى التقويس دون انكسار."
},
"de":{
"ui.fillet_corner_ball":"Kugel",
"ui.fillet_corner_ball_tip":"Ein Stück der Kugel des rollenden Balls, tangential (G1) zu jeder Rundung.",
"ui.fillet_corner_needs_constant":"Ecken mit glattem Flächenstück (G2) brauchen einen konstanten Radius und exakte Radiusanpassung.",
"ui.fillet_corner_patch":"Glattes Flächenstück",
"ui.fillet_corner_patch_tip":"Krümmungsstetig (G2): Die Ecke wird entlang jeder Kante zurückgesetzt und mit einem Flächenstück gefüllt, sodass Spiegelungen ohne Knick hindurchlaufen.",
"ui.fillet_fit_needs_constant":"Radiusanpassung braucht einen konstanten Radius.",
"ui.fillet_setback":"Rücksprung",
"ui.fillet_setback_hint":"{factor} × Radius",
"ui.fillet_setback_tip":"Wie weit von der Ecke entlang jeder Kante das Flächenstück beginnt. Leer: {factor} × der Radius.",
"ui.fillet_smooth_radius":"Radius glatt ändern",
"ui.fillet_type_curvature":"Krümmungsstetig",
"ui.fillet_type_tip":"Der Querschnitt über die Kante. Krümmungsstetig: ein krümmungsstetiger Querschnitt (G2) zwischen ebenen Flächen, sodass Spiegelungen ohne Knick von den Flächen in die Rundung laufen."
},
"he":{
"ui.fillet_corner_ball":"כדור",
"ui.fillet_corner_ball_tip":"חלק מספירת הכדור המתגלגל, משיק (G1) לכל עיגול.",
"ui.fillet_corner_needs_constant":"פינות עם טלאי חלק (G2) דורשות רדיוס קבוע והתאמת רדיוס מדויקת.",
"ui.fillet_corner_patch":"טלאי חלק",
"ui.fillet_corner_patch_tip":"רציף בעקמומיות (G2): הפינה נסוגה לאורך כל קצה ומתמלאת בטלאי, כך שההשתקפויות עוברות בה בלי שבר.",
"prefs.profile_operations_note":"Records how long SMLib calls, kernel operations, features, commands and imports take. Agents read it with diagnostics.profile."
},
"ar":{
"cmd.diagnostics_profile":"التحليل الزمني",
"prefs.profile_operations":"تحليل زمن العمليات",
"prefs.profile_operations_note":"يسجّل المدة التي تستغرقها استدعاءات SMLib وعمليات النواة والميزات والأوامر وعمليات الاستيراد. يقرؤه الوكلاء عبر diagnostics.profile."
"prefs.profile_operations_note":"Zeichnet auf, wie lange SMLib-Aufrufe, Kernel-Vorgänge, Features, Befehle und Importe dauern. Agenten lesen es mit diagnostics.profile."
},
"he":{
"cmd.diagnostics_profile":"פרופיל ביצועים",
"prefs.profile_operations":"מדידת זמני פעולות",
"prefs.profile_operations_note":"מתעד כמה זמן לוקחות קריאות SMLib, פעולות ליבה, תכונות, פקודות וייבוא. סוכנים קוראים זאת עם diagnostics.profile."
"prefs.metric_tap_drill_major_less_pitch":"Metric tapped holes drill the major diameter less the pitch (M8: 6.75 mm), not the tap maker's drill",
"thread.drill_range":"The tap maker's drill is {drill} mm. The internal thread's standard minor diameter is {minor} mm. A drill from {low} to {high} mm is accepted.",
"thread.drill_range_custom":"The drill is the major diameter less the pitch, {drill} mm. The internal thread's standard minor diameter is {minor} mm. A drill from {low} to {high} mm is accepted.",
"thread.drill_range_taper":"The tap maker's drill is {drill} mm. The standard's minor diameter where the thread ends is {minor} mm. A drill from {low} to {high} mm is accepted."
"thread.drill_range":"مثقاب صانع اللقمة {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
"thread.drill_range_custom":"المثقاب هو القطر الأكبر ناقص الخطوة، {drill} مم. القطر الأصغر القياسي للسن الداخلي {minor} مم. يُقبل مثقاب من {low} إلى {high} مم.",
"thread.drill_range_taper":"مثقاب صانع اللقمة {drill} مم. القطر الأصغر القياسي عند نهاية السن {minor} مم. يُقبل مثقاب من {low} إلى {high} مم."
},
"he":{
"prefs.holes":"חורים",
"prefs.metric_tap_drill_major_less_pitch":"חורים מתוברגים מטריים נקדחים בקוטר הגדול פחות הפסיעה (M8: 6.75 מ\"מ) ולא במקדח של יצרן המברז",
"thread.drill_range":"מקדח יצרן המברז הוא {drill} מ\"מ. הקוטר הקטן התקני של התבריג הפנימי הוא {minor} מ\"מ. מתקבל מקדח מ-{low} עד {high} מ\"מ.",
"thread.drill_range_custom":"המקדח הוא הקוטר הגדול פחות הפסיעה, {drill} מ\"מ. הקוטר הקטן התקני של התבריג הפנימי הוא {minor} מ\"מ. מתקבל מקדח מ-{low} עד {high} מ\"מ.",
"thread.drill_range_taper":"מקדח יצרן המברז הוא {drill} מ\"מ. הקוטר הקטן התקני בסוף התבריג הוא {minor} מ\"מ. מתקבל מקדח מ-{low} עד {high} מ\"מ."
"thread.drill_range":"丝锥制造商的钻头为 {drill} mm。内螺纹的标准小径为 {minor} mm。可使用 {low} 至 {high} mm 的钻头。",
"thread.drill_range_custom":"钻头为大径减螺距,{drill} mm。内螺纹的标准小径为 {minor} mm。可使用 {low} 至 {high} mm 的钻头。",
"thread.drill_range_taper":"丝锥制造商的钻头为 {drill} mm。螺纹终端处的标准小径为 {minor} mm。可使用 {low} 至 {high} mm 的钻头。"
},
"de":{
"prefs.holes":"Bohrungen",
"prefs.metric_tap_drill_major_less_pitch":"Metrische Gewindebohrungen mit Nenndurchmesser minus Steigung bohren (M8: 6,75 mm), nicht mit dem Bohrer des Gewindebohrerherstellers",
"thread.drill_range":"Der Bohrer des Gewindebohrerherstellers ist {drill} mm. Der genormte Kerndurchmesser des Innengewindes ist {minor} mm. Ein Bohrer von {low} bis {high} mm wird angenommen.",
"thread.drill_range_custom":"Der Bohrer ist der Nenndurchmesser minus Steigung, {drill} mm. Der genormte Kerndurchmesser des Innengewindes ist {minor} mm. Ein Bohrer von {low} bis {high} mm wird angenommen.",
"thread.drill_range_taper":"Der Bohrer des Gewindebohrerherstellers ist {drill} mm. Der genormte Kerndurchmesser am Gewindeende ist {minor} mm. Ein Bohrer von {low} bis {high} mm wird angenommen."
},
"ko":{
"prefs.holes":"구멍",
"prefs.metric_tap_drill_major_less_pitch":"미터 탭 구멍을 탭 제조사의 드릴 대신 바깥지름에서 피치를 뺀 값으로 뚫기 (M8: 6.75 mm)",
"thread.drill_range":"탭 제조사의 드릴은 {drill} mm입니다. 암나사의 규격 골지름은 {minor} mm입니다. {low}~{high} mm 드릴을 사용할 수 있습니다.",
"thread.drill_range_custom":"드릴은 바깥지름에서 피치를 뺀 {drill} mm입니다. 암나사의 규격 골지름은 {minor} mm입니다. {low}~{high} mm 드릴을 사용할 수 있습니다.",
"thread.drill_range_taper":"탭 제조사의 드릴은 {drill} mm입니다. 나사 끝에서의 규격 골지름은 {minor} mm입니다. {low}~{high} mm 드릴을 사용할 수 있습니다."
"Double-click to rename":"Kliknij dwukrotnie, aby zmienić nazwę",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing":"Dwukrotne kliknięcie środkowym przyciskiem w widoku: Widok główny (kadruje to, co widoczne), Dopasuj (kadruje to, co widoczne, zachowując orientację) lub nic",
"Drag to move the boss across the model. It snaps to sketch points on the face.":"Przeciągnij, aby przesunąć występ po modelu. Przyciąga się do punktów szkicu na ścianie.",
"Drag to move the rest across the model. It snaps to sketch points on the face.":"Przeciągnij, aby przesunąć podparcie po modelu. Przyciąga się do punktów szkicu na ścianie.",
"Drag to move the snap fit across the model. It snaps to sketch points on the face.":"Przeciągnij, aby przesunąć zatrzask po modelu. Przyciąga się do punktów szkicu na ścianie.",
"Drag to point the catch in another direction.":"Przeciągnij, aby skierować zaczep w inną stronę.",
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.":"Przeciągnij, aby obrócić podparcie co 5°. Przytrzymaj Alt lub Ctrl, aby obracać swobodnie.",
"ui.agent_chat_privacy":"Nic nie trafia nigdzie poza uruchomionym przez Ciebie agentem, na Twoim własnym koncie.",
"ui.agent_chat_privacy_note":"SolidCraft nigdzie niczego nie wysyła: uruchomiony przez Ciebie agent łączy się z własną usługą na Twoim koncie. SolidCraft nigdy nie widzi ani nie przechowuje klucza ani hasła.",
"ui.agent_chat_privacy_note":"Czat sam z siebie nigdzie niczego nie wysyła: uruchomiony przez Ciebie agent łączy się z własną usługą na Twoim koncie. SolidCraft nigdy nie widzi ani nie przechowuje klucza ani hasła.",
"ui.agent_chat_queue":"Kolejka",
"ui.agent_chat_queue_full":"Za dużo wiadomości czeka: usuń jedną lub poczekaj na agenta.",
"ui.agent_chat_queued":"W kolejce",
@@ -1575,6 +1585,7 @@
"ui.ai_http_busy":"punkt końcowy jest zajęty lub przekroczył limit; spróbuj ponownie później",
"ui.ai_http_key":"punkt końcowy odrzucił klucz: sprawdź go w Preferencje > AI",
"ui.ai_http_not_found":"nie ma takiego punktu końcowego lub modelu: sprawdź bazowy URL i nazwę modelu",
"ui.ai_http_too_large":"rozmowa jest za duża dla tego punktu końcowego; rozpocznij nowy czat",
"ui.ai_key":"Klucz API",
"ui.ai_key_hint":"opcjonalny; przechowywany w systemowym pęku kluczy",
"ui.ai_key_needs_https":"Klucz jest wysyłany tylko przez HTTPS albo do tego komputera.",
@@ -1695,6 +1706,7 @@
"ui.command_done":"Wykonano: {command}",
"ui.command_palette_hint":"Wpisz polecenie, np. {example} — lub naciśnij S, aby wyszukać",
"ui.concentric":"Współśrodkowość",
"ui.cone":"Stożek",
"ui.conic_curve":"Krzywa stożkowa",
"ui.constant":"Stały",
"ui.constraints_conflict":"Konflikt wiązań",
@@ -1753,6 +1765,11 @@
"ui.draft":"Pochylenie",
"ui.drawing":"Rysunek",
"ui.drill_point":"Wierzchołek wiertła",
"ui.easyprint_consent":"EasyPrint to slicer Printables w przeglądarce. Aby otworzyć w nim model, SolidCraft wysyła go na serwery Printables. Link wygasa po 30 minutach, a Ty musisz być zalogowany w Printables. Kontynuować?",
"ui.easyprint_failed":"Nie udało się wysłać do EasyPrint: {error}",
"ui.easyprint_opened":"Otwarto w EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, w przeglądarce)",
"ui.easyprint_uploading":"Wysyłanie do Printables…",
"ui.nav_bridge":"Nawiguj programem urządzenia na tym komputerze",
"ui.nav_bridge_connected":"Połączono",
"ui.nav_bridge_port":"Port",
"ui.nav_bridge_tip":"Własny program urządzenia wejściowego 3D (np. sterownik domowej myszy 3D) porusza widokiem przez połączenie lokalne, więc kursor myszy zostaje Twój. SolidCraft łączy się z nim na tym porcie, tylko na tym komputerze.",
"ui.nav_bridge_waiting":"Czekam na program urządzenia",
"ui.navigation":"Nawigacja",
"ui.needs_design":"{command}: najpierw otwórz projekt",
"ui.newSketchGroup":"Nowa grupa szkiców",
@@ -1990,6 +2014,8 @@
"ui.pinToToolbar":"Przypnij do paska narzędzi",
"ui.pipe":"Rura",
"ui.placement":"Umiejscowienie",
"ui.placement_handles":"Uchwyty umieszczania",
"ui.placement_handles_tip":"Uchwyty na podglądzie podparcia, występu lub zatrzasku: przeciągnij znacznik, aby przesunąć element po modelu, obróć go pierścieniem",
"ui.plane":"Płaszczyzna",
"ui.plane_at_angle":"Płaszczyzna pod kątem",
"ui.plugins":"Wtyczki",
@@ -2050,9 +2076,7 @@
"ui.right":"Prawy",
"ui.save":"Zapisz",
"ui.save_as":"Zapisz jako",
"ui.save_mesh_no_slicer":"Nie znaleziono slicera: wybierz Niestandardowy… i wskaż program slicera albo wyłącz Wyślij do slicera",
"ui.save_mesh_pick_bodies":"Zaznacz bryły do zapisania",
"ui.save_mesh_temp_failed":"Nie udało się utworzyć {folder}: {error}",
"ui.saved_to":"Zapisano {path}",
"ui.sbox_pin":"Przypnij na górze okna S",
"ui.sbox_unpin":"Odepnij",
@@ -2067,7 +2091,6 @@
"ui.selection_shift_add_only":"Shift+kliknięcie tylko dodaje",
"cmd.file_open_in_easyprint":"Abrir com o EasyPrint",
"cmd.file_save_mesh":"Salvar como malha",
"cmd.file_slicers":"Fatiadores instalados",
"cmd.frame_member":"Membro estrutural",
@@ -1145,6 +1147,7 @@
"motion.key_here":"Inserir quadro-chave aqui",
"motion.key_here_hint":"Adicionar um quadro-chave neste passo com o valor atual da junta",
"motion.no_collisions":"Sem colisões durante o estudo",
"motion.plot_quantity":"Quantidade",
"motion.step_back":"Passo anterior",
"motion.step_forward":"Próximo passo",
"motion.to_end":"Para o fim",
@@ -1154,6 +1157,7 @@
"nav.blender.help":"Arrastar com o meio orbita, Shift+meio desloca, Ctrl+meio aplica zoom; arrastar com o direito não mexe na câmera",
"nav.free_orbit.help":"O mouse e o mouse 3D giram passando pelos dois polos e preservam o giro lateral (roll). O encaixe em eixo com Shift fica desativado.",
"nav.free_orbit.label":"Órbita livre",
"nav.free_orbit.snap_help":"Mouse e mouse 3D giram através dos dois polos e preservam a rotação da imagem. Mantenha Shift pressionado durante uma órbita por arraste para alinhar a direção da vista e a rotação da imagem.",
"nav.fusion.help":"Arrastar com o meio desloca, Shift+meio orbita, arrastar com o direito orbita",
"nav.inventor.help":"Arrastar com o meio desloca, Shift+meio orbita, Ctrl+meio aplica zoom, arrastar com o direito orbita",
"nav.openscad.help":"Arrastar com o esquerdo no espaço vazio orbita, Shift+arrastar com o esquerdo seleciona por caixa, arrastar a geometria do esboço a edita; arrastar com o direito ou o meio desloca, Shift+arrastar com o direito ou o meio aplica zoom",
@@ -1636,7 +1640,7 @@
"ui.agent_chat_pop_out":"Abrir em janela própria, que você pode mover para outra tela",
"ui.agent_chat_privacy":"Nada vai para lugar algum além do agente que você executa, na sua própria conta.",
"ui.agent_chat_privacy_note":"O SolidCraft não envia nada a lugar nenhum: o agente que você executa fala com o próprio serviço, na sua própria conta. O SolidCraft nunca vê nem guarda chaves ou senhas.",
"ui.agent_chat_privacy_note":"O chat não envia nada a lugar nenhum por conta própria: o agente que você executa fala com o próprio serviço, na sua própria conta. O SolidCraft nunca vê nem guarda chaves ou senhas.",
"ui.agent_chat_queue":"Fila",
"ui.agent_chat_queue_full":"Há mensagens demais esperando: remova uma ou aguarde o agente.",
"ui.agent_chat_queued":"Na fila",
@@ -1704,6 +1708,7 @@
"ui.ai_http_busy":"o endpoint está ocupado ou acima do limite; tente de novo mais tarde",
"ui.ai_http_key":"o endpoint recusou a chave: verifique-a em Preferências > IA",
"ui.ai_http_not_found":"endpoint ou modelo inexistente: verifique a URL base e o nome do modelo",
"ui.ai_http_too_large":"a conversa é grande demais para este endpoint; comece um novo chat",
"ui.ai_key":"Chave de API",
"ui.ai_key_hint":"opcional; guardada no chaveiro do sistema",
"ui.ai_key_needs_https":"Uma chave só é enviada por HTTPS ou para este computador.",
@@ -1828,6 +1833,7 @@
"ui.command_done":"{command} concluído",
"ui.command_palette_hint":"Digite um comando, p. ex. {example} — ou pressione S para pesquisar",
"ui.concentric":"Concêntrico",
"ui.cone":"Cone",
"ui.conic_curve":"Curva cônica",
"ui.constant":"Constante",
"ui.constraints_conflict":"Restrições em conflito",
@@ -1886,6 +1892,11 @@
"ui.draft":"Ângulo de saída",
"ui.drawing":"Desenho",
"ui.drill_point":"Ponta de broca",
"ui.easyprint_consent":"O EasyPrint é o slicer do Printables no navegador. Para abrir o modelo lá, o SolidCraft o envia para os servidores do Printables. O link expira em 30 minutos e você precisa estar conectado ao Printables. Continuar?",
"ui.easyprint_failed":"Falha ao enviar para o EasyPrint: {error}",
"ui.easyprint_opened":"Aberto no EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, no navegador)",
"ui.easyprint_uploading":"Enviando para o Printables…",
"ui.edit":"Editar",
"ui.editJointLimits":"Editar limites da junta",
"ui.edit_section_analysis":"Editar análise de seção",
@@ -1945,6 +1956,7 @@
"ui.heading_circular_pattern":"PADRÃO CIRCULAR",
"ui.heading_coil":"ESPIRAL",
"ui.heading_combine":"COMBINAR",
"ui.heading_cone":"CONE",
"ui.heading_configure":"CONFIGURAR",
"ui.heading_constraints":"RESTRIÇÕES",
"ui.heading_construct":"CONSTRUIR",
@@ -2004,6 +2016,7 @@
"ui.heading_torus":"TORO",
"ui.heading_utilities":"UTILITÁRIOS",
"ui.heading_utility":"UTILITÁRIO",
"ui.heading_weldments":"SOLDAGEM",
"ui.help":"Ajuda",
"ui.hide":"Ocultar",
"ui.hideAxes":"Ocultar eixos",
@@ -2191,9 +2204,7 @@
"ui.right":"Direita",
"ui.save":"Salvar",
"ui.save_as":"Salvar como",
"ui.save_mesh_no_slicer":"Nenhum fatiador encontrado: escolha Personalizado… e selecione seu programa fatiador, ou desative Enviar ao fatiador",
"ui.save_mesh_pick_bodies":"Selecione os corpos a salvar",
"ui.save_mesh_temp_failed":"Não foi possível criar {folder}: {error}",
"ui.saved_to":"Salvo {path}",
"ui.sbox_pin":"Fixar no topo da caixa S",
"ui.sbox_unpin":"Desafixar",
@@ -2208,7 +2219,6 @@
"ui.selection_shift_add_only":"Shift+clique adiciona sem remover",
"ui.agent_chat_privacy":"Nimic nu merge nicăieri în afară de agentul pe care îl rulați, sub propriul cont.",
"ui.agent_chat_privacy_note":"SolidCraft nu trimite nimic nicăieri: agentul pe care îl rulați comunică cu propriul serviciu sub contul dumneavoastră. SolidCraft nu vede și nu stochează niciodată o cheie sau o parolă.",
"ui.agent_chat_privacy_note":"Chatul nu trimite nimic nicăieri de la sine: agentul pe care îl rulați comunică cu propriul serviciu sub contul dumneavoastră. SolidCraft nu vede și nu stochează niciodată o cheie sau o parolă.",
"ui.agent_chat_queue":"Coadă",
"ui.agent_chat_queue_full":"Prea multe mesaje așteaptă: eliminați unul sau așteptați agentul.",
"ui.agent_chat_queued":"În coadă",
@@ -1332,6 +1335,7 @@
"ui.ai_http_busy":"endpointul este ocupat sau și-a depășit limita; încercați mai târziu",
"ui.ai_http_key":"endpointul a refuzat cheia: verificați-o în Preferințe > AI",
"ui.ai_http_not_found":"nu există un astfel de endpoint sau model: verificați URL-ul de bază și numele modelului",
"ui.ai_http_too_large":"conversația este prea mare pentru acest endpoint; începeți un chat nou",
"ui.ai_key":"Cheie API",
"ui.ai_key_hint":"opțional; păstrată în depozitul de chei al sistemului",
"ui.ai_key_needs_https":"O cheie se trimite doar prin HTTPS sau către acest calculator.",
@@ -1451,6 +1455,7 @@
"ui.command_done":"{command} efectuat",
"ui.command_palette_hint":"Tastați o comandă, ex. {example} — sau apăsați S pentru căutare",
"ui.concentric":"Concentric",
"ui.cone":"Con",
"ui.conic_curve":"Curbă conică",
"ui.constant":"Constant",
"ui.constraints_conflict":"Conflict de constrângeri",
@@ -1509,6 +1514,11 @@
"ui.draft":"Înclinare",
"ui.drawing":"Desen",
"ui.drill_point":"Vârf de burghiu",
"ui.easyprint_consent":"EasyPrint este slicerul Printables din browser. Pentru a deschide modelul acolo, SolidCraft îl încarcă pe serverele Printables. Linkul expiră după 30 de minute, iar dumneavoastră trebuie să fiți autentificat pe Printables. Continuați?",
"ui.easyprint_failed":"Încărcarea în EasyPrint a eșuat: {error}",
"ui.easyprint_opened":"Deschis în EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, în browser)",
"ui.easyprint_uploading":"Se încarcă pe Printables…",
"ui.agent_chat_privacy":"Ничего не уходит никуда, кроме запускаемого вами агента, под вашей учётной записью.",
"ui.agent_chat_privacy_note":"SolidCraft ничего никуда не отправляет: запускаемый вами агент обращается к своему сервису под вашей учётной записью. SolidCraft никогда не видит и не хранит ключ или пароль.",
"ui.agent_chat_privacy_note":"Чат сам ничего никуда не отправляет: запускаемый вами агент обращается к своему сервису под вашей учётной записью. SolidCraft никогда не видит и не хранит ключ или пароль.",
"ui.agent_chat_queue":"В очередь",
"ui.agent_chat_queue_full":"Слишком много сообщений в ожидании: удалите одно или дождитесь агента.",
"ui.agent_chat_queued":"В очереди",
@@ -1388,6 +1391,7 @@
"ui.ai_http_busy":"конечная точка занята или превысила лимит; повторите позже",
"ui.ai_http_key":"конечная точка отклонила ключ: проверьте его в Настройки > ИИ",
"ui.ai_http_not_found":"нет такой конечной точки или модели: проверьте базовый URL и имя модели",
"ui.ai_http_too_large":"разговор слишком велик для этой конечной точки; начните новый чат",
"ui.ai_key":"Ключ API",
"ui.ai_key_hint":"необязательно; хранится в системной связке ключей",
"ui.ai_key_needs_https":"Ключ отправляется только по HTTPS или на этот компьютер.",
@@ -1508,6 +1512,7 @@
"ui.command_done":"{command} — готово",
"ui.command_palette_hint":"Введите команду, напр. {example} — или нажмите S для поиска",
"ui.easyprint_consent":"EasyPrint — это слайсер Printables в браузере. Чтобы открыть модель там, SolidCraft загружает её на серверы Printables. Ссылка действует 30 минут, и вам нужно войти в Printables. Продолжить?",
"ui.easyprint_failed":"Не удалось загрузить в EasyPrint: {error}",
"ui.easyprint_opened":"Открыто в EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, в браузере)",
"ui.easyprint_uploading":"Загрузка в Printables…",
"ui.agent_chat_privacy":"Inget skickas någonstans utom till agenten du kör, under ditt eget konto.",
"ui.agent_chat_privacy_note":"SolidCraft skickar ingenting någonstans: agenten du kör talar med sin egen tjänst under ditt eget konto. SolidCraft ser eller lagrar aldrig en nyckel eller ett lösenord.",
"ui.agent_chat_privacy_note":"Chatten skickar inget någonstans av sig själv: agenten du kör talar med sin egen tjänst under ditt eget konto. SolidCraft ser eller lagrar aldrig en nyckel eller ett lösenord.",
"ui.agent_chat_queue":"Kö",
"ui.agent_chat_queue_full":"För många meddelanden väntar: ta bort ett eller vänta på agenten.",
"ui.agent_chat_queued":"Köad",
@@ -1332,6 +1335,7 @@
"ui.ai_http_busy":"slutpunkten är upptagen eller över sin gräns; försök igen senare",
"ui.ai_http_key":"slutpunkten nekade nyckeln: kontrollera den under Inställningar > AI",
"ui.ai_http_not_found":"ingen sådan slutpunkt eller modell: kontrollera bas-URL:en och modellnamnet",
"ui.ai_http_too_large":"konversationen är för stor för den här slutpunkten; starta en ny chatt",
"ui.ai_key":"API-nyckel",
"ui.ai_key_hint":"valfritt; lagras i systemets nyckelring",
"ui.ai_key_needs_https":"En nyckel skickas endast via HTTPS, eller till den här datorn.",
@@ -1451,6 +1455,7 @@
"ui.command_done":"{command} klart",
"ui.command_palette_hint":"Skriv ett kommando, t.ex. {example} — eller tryck på S för att söka",
"ui.concentric":"Koncentrisk",
"ui.cone":"Kon",
"ui.conic_curve":"Konisk kurva",
"ui.constant":"Konstant",
"ui.constraints_conflict":"Villkor i konflikt",
@@ -1509,6 +1514,11 @@
"ui.draft":"Släppvinkel",
"ui.drawing":"Ritning",
"ui.drill_point":"Borrspets",
"ui.easyprint_consent":"EasyPrint är Printables slicer i webbläsaren. För att öppna modellen där laddar SolidCraft upp den till Printables servrar. Länken upphör efter 30 minuter och du måste vara inloggad på Printables. Fortsätta?",
"ui.easyprint_failed":"Uppladdningen till EasyPrint misslyckades: {error}",
"ui.easyprint_opened":"Öppnad i EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, i webbläsaren)",
"ui.easyprint_uploading":"Laddar upp till Printables…",
"Preview leaves the current design unchanged.":"Önizleme geçerli tasarımı değiştirmez.",
"Profiles/Curves":"Profiller/Eğriler",
"Quantity":"Büyüklük",
"Quantity":"Adet",
"RECENT":"SON KULLANILANLAR",
"REFERENCE":"REFERANS",
"RULED SURFACE":"REGLE YÜZEY",
@@ -456,6 +456,7 @@
"cmd.canvas_delete":"Tuvali sil",
"cmd.canvas_edit":"Tuvali düzenle",
"cmd.canvas_insert":"Tuval",
"cmd.canvas_to_sketch":"Tuvalden eskiz",
"cmd.component_activate":"Bileşeni etkinleştir",
"cmd.component_break_link":"Bağı kopar",
"cmd.component_capture_position":"Konumu yakala",
@@ -533,6 +534,7 @@
"cmd.file_export":"Dışa aktar",
"cmd.file_insert_mesh":"Ağ ekle",
"cmd.file_insert_step":"STEP ekle",
"cmd.file_open_in_easyprint":"EasyPrint ile aç",
"cmd.file_save_mesh":"Ağ olarak kaydet",
"cmd.file_slicers":"Yüklü dilimleyiciler",
"cmd.frame_member":"Çerçeve Elemanı",
@@ -970,6 +972,7 @@
"motion.key_here":"Buraya anahtar kare ekle",
"motion.key_here_hint":"Bu adıma bağlantının mevcut değeriyle bir anahtar kare ekle",
"motion.no_collisions":"Etüt boyunca çarpışma yok",
"motion.plot_quantity":"Büyüklük",
"motion.step_back":"Bir adım geri",
"motion.step_forward":"Bir adım ileri",
"motion.to_end":"Sona git",
@@ -1324,7 +1327,7 @@
"ui.agent_chat_pop_out":"Başka bir ekrana taşıyabileceğiniz ayrı bir pencerede aç",
"ui.agent_chat_previous_picture":"Önceki resim",
"ui.agent_chat_privacy":"Kendi hesabınızla çalıştırdığınız ajan dışında hiçbir yere bir şey gitmez.",
"ui.agent_chat_privacy_note":"SolidCraft hiçbir yere bir şey göndermez: Başlattığınız ajan, sizin hesabınızla kendi hizmetine bağlanır. SolidCraft hiçbir anahtarı veya parolayı görmez ve kaydetmez.",
"ui.agent_chat_privacy_note":"Sohbet kendi başına hiçbir yere bir şey göndermez: Başlattığınız ajan, sizin hesabınızla kendi hizmetine bağlanır. SolidCraft hiçbir anahtarı veya parolayı görmez ve kaydetmez.",
"ui.agent_chat_queue":"Sıraya al",
"ui.agent_chat_queue_full":"Çok fazla ileti bekliyor: birini kaldırın veya ajanı bekleyin.",
"ui.agent_chat_queued":"Sırada",
@@ -1388,6 +1391,7 @@
"ui.ai_http_busy":"uç nokta meşgul veya sınırda; daha sonra tekrar deneyin",
"ui.ai_http_key":"uç nokta anahtarı reddetti: Ayarlar > AI altında kontrol edin",
"ui.ai_http_not_found":"Uç nokta veya model bulunamadı: temel URL'yi ve model adını kontrol edin",
"ui.ai_http_too_large":"konuşma bu uç nokta için çok büyük; yeni bir sohbet başlatın",
"ui.ai_key":"API anahtarı",
"ui.ai_key_hint":"isteğe bağlı; sistem anahtar zincirinde saklanır",
"ui.ai_key_needs_https":"Anahtar yalnızca HTTPS üzerinden veya bu bilgisayara gönderilir.",
@@ -1508,6 +1512,7 @@
"ui.command_done":"{command} tamamlandı",
"ui.command_palette_hint":"Bir komut yazın (ör. {example}) — veya aramak için S tuşuna basın",
"ui.easyprint_consent":"EasyPrint, Printables'ın tarayıcıdaki dilimleyicisidir. Modeli orada açmak için SolidCraft onu Printables sunucularına yükler. Bağlantı 30 dakika sonra sona erer ve Printables'ta oturum açmış olmanız gerekir. Devam edilsin mi?",
"ui.agent_chat_privacy":"Нічого нікуди не надсилається, крім агента, який ви запускаєте, під вашим власним обліковим записом.",
"ui.agent_chat_privacy_note":"SolidCraft нічого нікуди не надсилає: агент, який ви запускаєте, спілкується з власним сервісом під вашим обліковим записом. SolidCraft ніколи не бачить і не зберігає ключ чи пароль.",
"ui.agent_chat_privacy_note":"Чат сам нічого нікуди не надсилає: агент, який ви запускаєте, спілкується з власним сервісом під вашим обліковим записом. SolidCraft ніколи не бачить і не зберігає ключ чи пароль.",
"ui.agent_chat_queue":"У чергу",
"ui.agent_chat_queue_full":"Забагато повідомлень в очікуванні: приберіть одне або зачекайте на агента.",
"ui.agent_chat_queued":"У черзі",
@@ -1684,6 +1687,7 @@
"ui.ai_http_busy":"кінцева точка зайнята або перевищила ліміт; спробуйте пізніше",
"ui.ai_http_key":"кінцева точка відхилила ключ: перевірте його в Налаштування > ШІ",
"ui.ai_http_not_found":"немає такої кінцевої точки чи моделі: перевірте базову URL-адресу й назву моделі",
"ui.ai_http_too_large":"розмова завелика для цієї кінцевої точки; почніть новий чат",
"ui.ai_key":"Ключ API",
"ui.ai_key_hint":"необов'язково; зберігається в системному сховищі ключів",
"ui.ai_key_needs_https":"Ключ надсилається лише через HTTPS або на цей комп'ютер.",
@@ -1804,6 +1808,7 @@
"ui.command_done":"{command} виконано",
"ui.command_palette_hint":"Введіть команду, напр. {example} — або натисніть S для пошуку",
"ui.concentric":"Концентричність",
"ui.cone":"Конус",
"ui.conic_curve":"Конічна крива",
"ui.constant":"Стала",
"ui.constraints_conflict":"Конфлікт обмежень",
@@ -1862,6 +1867,11 @@
"ui.draft":"Ухил",
"ui.drawing":"Креслення",
"ui.drill_point":"Вістря свердла",
"ui.easyprint_consent":"EasyPrint — це слайсер Printables у браузері. Щоб відкрити модель там, SolidCraft завантажує її на сервери Printables. Посилання діє 30 хвилин, і вам потрібно увійти в Printables. Продовжити?",
"ui.easyprint_failed":"Не вдалося завантажити в EasyPrint: {error}",
"ui.easyprint_opened":"Відкрито в EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, у браузері)",
"ui.easyprint_uploading":"Завантаження на Printables…",
"ui.edit":"Редагувати",
"ui.editJointLimits":"Редагувати обмеження з'єднання",
"ui.edit_section_analysis":"Редагувати аналіз перерізу",
@@ -1918,6 +1928,7 @@
"ui.heading_circular_pattern":"КРУГОВИЙ МАСИВ",
"ui.heading_coil":"СПІРАЛЬ",
"ui.heading_combine":"ОБ'ЄДНАТИ",
"ui.heading_cone":"КОНУС",
"ui.heading_configure":"НАЛАШТУВАТИ",
"ui.heading_constraints":"ОБМЕЖЕННЯ",
"ui.heading_construct":"ПОБУДОВА",
@@ -1977,6 +1988,7 @@
"ui.heading_torus":"ТОР",
"ui.heading_utilities":"УТИЛІТИ",
"ui.heading_utility":"УТИЛІТА",
"ui.heading_weldments":"ЗВАРЮВАННЯ",
"ui.help":"Довідка",
"ui.hide":"Приховати",
"ui.hideAxes":"Приховати осі",
@@ -2159,9 +2171,7 @@
"ui.right":"Справа",
"ui.save":"Зберегти",
"ui.save_as":"Зберегти як",
"ui.save_mesh_no_slicer":"Слайсер не знайдено: виберіть «Власна…» і вкажіть свою програму-слайсер або вимкніть «Надіслати в слайсер»",
"ui.save_mesh_pick_bodies":"Виберіть тіла для збереження",
"ui.save_mesh_temp_failed":"Не вдалося створити {folder}: {error}",
"ui.saved_to":"Збережено {path}",
"ui.sbox_pin":"Закріпити вгорі поля S",
"ui.sbox_unpin":"Відкріпити",
@@ -2176,7 +2186,6 @@
"ui.selection_shift_add_only":"Shift+клацання додає без вилучення",
"ui.selection_shift_add_only_tip":"Увімк.: Shift додає елементи, Ctrl/Cmd перемикає. Вимк. (типово): Shift і Ctrl/Cmd обидва перемикають.",
"ui.send":"Надіслати",
"ui.send_to_slicer":"Надіслати в слайсер",
"ui.sent_to_slicer":"Надіслано в {slicer}",
"ui.sheet_gap":"Проміжок",
"ui.shell":"Оболонка",
@@ -2268,6 +2277,7 @@
"ui.untitled":"Без назви",
"ui.up_axis_note":"Яка вісь спрямована вгору у видах (вид осі, головний, наведення). Проєкти не змінюються.",
"ui.updateToCurrentView":"Оновити до поточного виду",
"ui.upload":"Завантажити",
"ui.view_centre":"Центр виду",
"ui.visual_shaded_hidden_edges":"Затінений із прихованими ребрами",
"ui.visual_shaded_with_edges":"Затінений із ребрами",
"cmd.file_save_mesh":"Lưu dưới dạng lưới đa giác",
"cmd.file_slicers":"Phần mềm cắt lớp đã cài đặt",
"cmd.frame_member":"Thanh khung",
@@ -971,6 +973,7 @@
"motion.key_here":"Đặt khóa tại đây",
"motion.key_here_hint":"Thêm một khóa tại bước này với giá trị hiện tại của khớp nối",
"motion.no_collisions":"Không có va chạm trong suốt quá trình khảo sát",
"motion.plot_quantity":"Số lượng",
"motion.step_back":"Lùi một bước",
"motion.step_forward":"Tiến một bước",
"motion.to_end":"Đến cuối",
@@ -1325,7 +1328,7 @@
"ui.agent_chat_pop_out":"Mở trong cửa sổ riêng để bạn có thể di chuyển sang màn hình khác",
"ui.agent_chat_previous_picture":"Ảnh trước",
"ui.agent_chat_privacy":"Không có dữ liệu nào được gửi đi bất cứ đâu ngoại trừ tới tác tử bạn chạy, theo tài khoản của chính bạn.",
"ui.agent_chat_privacy_note":"SolidCraft không gửi bất kỳ dữ liệu nào ra ngoài: AI Agent giao tiếp trực tiếp với dịch vụ bằng tài khoản của bạn. SolidCraft không bao giờ thấy hay lưu khóa/mật khẩu.",
"ui.agent_chat_privacy_note":"Khung chat tự nó không gửi bất kỳ dữ liệu nào ra ngoài: AI Agent giao tiếp trực tiếp với dịch vụ bằng tài khoản của bạn. SolidCraft không bao giờ thấy hay lưu khóa/mật khẩu.",
"ui.agent_chat_queue":"Hàng đợi",
"ui.agent_chat_queue_full":"Quá nhiều tin nhắn đang chờ: hãy xóa bớt hoặc chờ AI hoàn thành.",
"ui.agent_chat_queued":"Đã vào hàng đợi",
@@ -1389,6 +1392,7 @@
"ui.ai_http_busy":"điểm cuối đang bận hoặc vượt quá giới hạn; vui lòng thử lại sau",
"ui.ai_http_key":"điểm cuối từ chối khóa API: hãy kiểm tra trong Tùy chọn cài đặt > AI",
"ui.ai_http_not_found":"không tìm thấy điểm cuối hoặc mô hình: hãy kiểm tra URL cơ sở và tên mô hình",
"ui.ai_http_too_large":"cuộc trò chuyện quá lớn đối với điểm cuối này; hãy bắt đầu cuộc trò chuyện mới",
"ui.ai_key":"Khóa API",
"ui.ai_key_hint":"tùy chọn; lưu trữ bảo mật trong chuỗi khóa hệ thống",
"ui.ai_key_needs_https":"Khóa chỉ được gửi qua HTTPS, hoặc trực tiếp trên máy tính này.",
@@ -1509,6 +1513,7 @@
"ui.command_done":"Hoàn tất lệnh {command}",
"ui.command_palette_hint":"Nhập một lệnh, ví dụ: {example} — hoặc nhấn S để tìm kiếm",
"ui.concentric":"Đồng tâm",
"ui.cone":"Hình nón",
"ui.conic_curve":"Đường cong Conic",
"ui.constant":"Hằng số",
"ui.constraints_conflict":"Xung đột ràng buộc",
@@ -1567,6 +1572,11 @@
"ui.draft":"Độ côn thoát khuôn",
"ui.drawing":"Bản vẽ 2D",
"ui.drill_point":"Đỉnh mũi khoan (Drill Point)",
"ui.easyprint_consent":"EasyPrint là trình cắt lớp (slicer) của Printables chạy trong trình duyệt. Để mở mô hình ở đó, SolidCraft tải mô hình lên máy chủ của Printables. Liên kết hết hạn sau 30 phút và bạn cần đăng nhập Printables. Tiếp tục?",
"ui.easyprint_failed":"Tải lên EasyPrint thất bại: {error}",
"ui.easyprint_opened":"Đã mở trong EasyPrint",
"ui.easyprint_row":"EasyPrint (Printables, trong trình duyệt)",
"ui.easyprint_uploading":"Đang tải lên Printables…",
"ui.edit":"Chỉnh sửa",
"ui.editJointLimits":"Chỉnh sửa giới hạn khớp nối",
"ui.edit_section_analysis":"Chỉnh sửa phân tích mặt cắt",
"ui.save_mesh_no_slicer":"Không tìm thấy phần mềm cắt lớp (slicer): hãy chọn Tùy chỉnh… và chỉ định chương trình cắt lớp, hoặc tắt tùy chọn Gửi đến slicer",
"ui.save_mesh_pick_bodies":"Chọn các thân khối cần lưu",
"ui.save_mesh_temp_failed":"Không thể tạo thư mục {folder}: {error}",
"ui.saved_to":"Đã lưu {path}",
"ui.sbox_pin":"Ghim lên đầu hộp tìm kiếm phím S",
"ui.sbox_unpin":"Bỏ ghim",
@@ -1874,7 +1884,6 @@
"ui.selection_shift_add_only":"Shift+nhấp chỉ thêm, không bỏ chọn",
"ui.selection_shift_add_only_tip":"Bật: Shift thêm mục, Ctrl/Cmd bật/tắt. Tắt (mặc định): cả Shift và Ctrl/Cmd đều bật/tắt.",
"ui.send":"Gửi",
"ui.send_to_slicer":"Gửi sang phần mềm cắt lát (Slicer)",
"ui.sent_to_slicer":"Đã gửi sang {slicer}",
"ui.sheet_gap":"Khe hở tấm",
"ui.shell":"Tạo vỏ mỏng (Shell)",
@@ -1966,6 +1975,7 @@
"ui.untitled":"Chưa đặt tên",
"ui.up_axis_note":"Trục nào hướng lên trên trong các góc nhìn (góc nhìn theo trục, Chuẩn, Nhìn thẳng). Các mô hình thiết kế không bị thay đổi.",
"ui.updateToCurrentView":"Cập nhật theo góc nhìn hiện tại",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing":"在视图中双击中键:主视图(主视图,框住所显示的内容)、适合窗口(框住所显示的内容,保持方向)或无动作",
"Drag to move the boss across the model. It snaps to sketch points on the face.":"拖动以在模型上移动凸台。它会吸附到面上的草图点。",
"Drag to move the hole; click to remove it":"拖动以移动孔,单击以删除",
"Drag to move the rest across the model. It snaps to sketch points on the face.":"拖动以在模型上移动垫块。它会吸附到面上的草图点。",
"Drag to move the snap fit across the model. It snaps to sketch points on the face.":"拖动以在模型上移动卡扣。它会吸附到面上的草图点。",
"Drag to point the catch in another direction.":"拖动以将卡钩指向另一个方向。",
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.":"拖动以 5° 为步长旋转垫块。按住 Alt 或 Ctrl 可自由旋转。",
"Drive Joint":"驱动接头",
"Duration":"时长",
"Edges":"边",
@@ -398,7 +403,7 @@
"Profile / Edges":"轮廓 / 边",
"Profiles or curves":"轮廓或曲线",
"Profiles/Curves":"轮廓/曲线",
"Quantity":"物理量",
"Quantity":"数量",
"RECENT":"最近使用",
"REFERENCE":"参考",
"RULED SURFACE":"直纹曲面",
@@ -425,6 +430,8 @@
"Remove this point":"移除此点",
"Rename Feature":"重命名特征",
"Rename Folder":"重命名文件夹",
"Reopen designs":"重新打开设计",
"Reopen the designs that were open when SolidCraft was last closed":"重新打开上次关闭 SolidCraft 时打开的设计",
"Replace existing library file":"替换现有库文件",
"Report a bug — review a screenshot and open a GitHub draft":"报告缺陷 — 复核截图并开启 GitHub 草稿",
"Report text copied. Paste it into GitHub and submit your report there.":"报告文本已复制。请在GitHub中粘贴并提交。",
| tests/kernel-cases/import/4561-convert-box-mesh-with-a-zero-area-triangle/box-degenerate.stl | SolidCraft contributors | synthetic ASCII STL written by hand: a box with one zero-area triangle, for the kernel regression case of tangency-labs/solidcraft#4561 | MIT OR Apache-2.0 |
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.