Commit Graph

297 Commits

Author SHA1 Message Date
jyviko 5c45570c91 Kernel rig: rustfmt; measured values in results are decimals, within tolerance; the ladder records 06/023 passing
The single results form compares a whole number exactly (a count). The construction and inspect
cases gave positions and volumes as whole numbers, which the form they were written for compared
within tolerance; they are decimals now. The NPT groove fix makes ladder 06/023 pass: its golden
entry is the passing run (51 faces, 8226.4042 mm3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:08:31 -04:00
jyviko 014a9eced5 Kernel rig: a flat_pattern field grades a sheet metal case's flat pattern
From the script's last sheet.flat_pattern result: the outline's size, its area less the
cut-outs', and the bend lines, each matched to its own one of the result's either way round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:08:31 -04:00
jyviko 70f695c6ba Kernel rig: grade construction geometry and the numbers a command replies with
A case's construction field names construction points, axes and planes with the point, line or
plane each must be, and results reads values out of the commands' own replies by JSON pointer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:08:31 -04:00
jyviko bc3c9185cd Kernel rig: grade the kinds of a result's surfaces, its B-spline degrees and spans, and its free edges
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:08:31 -04:00
jyviko 235645db50 Kernel rig: the ellipse length's Simpson weights as a named step
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:08:31 -04:00
jyviko 45c9553262 Kernel rig: a case can check the files it writes and the commands' answers
A case's expected.json may name files written to $OUT (json, csv rows, or a DXF's
entity counts and curve lengths per layer) and a command's answer by step, each
matched as a part of what was made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:08:31 -04:00
jyviko e0c7867b9b run_strict: the box's volume to 1e-9, as SMLib integrates it
allow_errors_writes_anyway_and_a_saved_design_keeps_failing compared the box's volume
with 4000.0 to the bit, which the old kernel's closed-form planar volume gave. SMLib
integrates mass properties numerically (relative accuracy 1e-4 asked for) and gives
the 20 x 20 x 10 box within a unit or two in the last place (3999.999999999999), the
correct answer to the precision the measure has. The test now checks 4000 mm³ to a
relative 1e-9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:13:01 -04:00
jyviko 39bf63b16a Kernel rig: designed continuity cases, G0 to G4, that prove the measurement
A new `continuity` area: seams whose order is certain by construction, each graded
`exact` (reads its order and not the next) at picked edges with Continuity Analysis's
default stations and tolerances: a box's crease (g0), a chamfer in place of a round (g0),
an r5 round on planes (g1), and a profile closed by a g1, g2, g3 or g4 blend curve between
lines, extruded (g1 … g4). `continuity.edges` may now be a list of points, picked as
`inspect.continuity` picks them, and `continuity.exact` asks for the order and no more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:22:17 -04:00
jyviko c37b0d2f02 Kernel rig: a case's expected continuity order at its blend seams
expected.json may give `continuity: {order: g0…g4, edges?: "blend" | "corner"}`: the
seams of each round, chamfer face or corner patch with the faces either side of the edge it
replaced, and of each corner patch with its rounds, are measured as Continuity Analysis
measures them (seam_diagnostics at its default 33 stations and default tolerances), and the
case fails naming the worst seam, its faces, the order it reaches and the next order's worst
value. The seams come from the faces' persistent names (doc::naming::blend_seams). The older
`{near, within_mm, …}` form is kept.

Every fillet-area case with a body gets its order: constant rounds and rolling corners g1,
curvature rounds g2, chamfers g0, the corner: g2 patch g2 at its seams.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:22:17 -04:00
jyviko 97721ad05f Fillet setbacks not past where the rounds meet are refused naming the least; the rig grades a refusal by its reason
A corner setback must reach past where the corner's rounds' contact lines
meet without one (smlib_blend_setback); short of it SMLib failed with its
own status (1001). The kernel now refuses it before SMLib runs, for the
rolling-ball corner (fillet_setback) and the curvature corner
(fillet_g2_corners) alike, naming the least: on each face at a corner of
planar faces and straight edges the two rails, r tan(psi/2) from their
edges, cross (d2 + d1 cos alpha) / sin alpha along an edge (r on faces
square to each other). Other corners stay SMLib's to refuse.

The rig's `refused` cases now name the reason (`refusal`, a part of the
message, required), so a failure for another reason no longer passes; a
`solid_or_refused` case may name one. Fillets: the setback cases and
783, 3435 pass; 1390, 896, 4782 past-a-full-round and the no-tangent-chain
case are refused for an unclear reason and fail.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 21:15:20 -04:00
jyviko 77bd85cabf cli: snapshot --quality coarse|normal|fine|ultra meshes the bodies at that display quality
As the app's Display Quality setting, set before the design opens so every display plan is
made at it; a level that isn't one is refused with the levels. For close-up reviews of
imported faces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:53:08 -04:00
jyviko 8fbb575a5e kernel-cases: a continuity check, and ladder 07/015 graded by it
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>
2026-10-27 12:24:10 -04:00
jyviko fafd977b05 Profiler: the Profile operations preference switches recording at run time
- 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>
2026-10-27 11:40:49 -04:00
jyviko 9cd655259d Profiler: commands, feature evaluation, io; diagnostics.profile; CLI --profile
- Session::execute is a command span; Model::evaluate and each feature computed
  (by its type) are spans; io's import, export and design read/write too; the
  STEP reader's topology, pcurves and solid record.
- diagnostics.profile (contract + output schema): report, start/stop, reset,
  Chrome trace file.
- A STEP/IGES import's reply carries its own profile while recording.
- Every app compiles the profiler in by default (feature `profile`);
  SOLIDCRAFT_PROFILE=1 starts it.
- solidcraft-cli run/eval/bench-open --profile print the table on stderr,
  --profile-out writes the trace; run --profile works on designs and STEP files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:40:49 -04:00
jyviko 4e26dc6402 The command line and the web app link SMLib by default too
As the desktop app: SMLib is the only kernel, so a build without it models nothing. The web
app links the prebuilt wasm archive, so it needs no wasi-sdk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoZgkCdAVrscuw2kjQTWDs
2026-10-27 02:28:53 -04:00
jyviko e476aba94f SMLib in the browser: built with wasi-sdk for wasm32-unknown-unknown
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
2026-10-27 02:28:53 -04:00
jyviko bc15221fa3 The desktop app links SMLib by default
SMLib is the only kernel, so a desktop build without it models nothing. The app gets the
smlib feature the CLI has, on by default.

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
2026-10-27 02:28:51 -04:00
jyviko af4e97b648 cli: the rig and the oracle reach MeshTolerance through the engine
solidcraft-cli depends on the engine, not the kernel: solidcraft_kernel::MeshTolerance did not
resolve; it is solidcraft_engine::kernel::MeshTolerance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
2026-10-27 02:28:50 -04:00
jyviko 712caf8bf4 engine: no boolean retries to report; overlaps are SMLib's exact intersection
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
2026-10-27 02:28:50 -04:00
jyviko 1ecf00b5ac engine: feature replies carry no boolean diagnostics
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
2026-10-27 02:28:49 -04:00
jyviko 001c62ab16 kernel-cases: the rig runs SMLib only and keeps each run's SMLib dumps
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
2026-10-27 02:28:49 -04:00
jyviko beebf22032 kernel-cases: --dumps=DIR leaves each failing SMLib call's inputs for replay
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
2026-10-27 02:28:49 -04:00
jyviko 166ebe2ee9 The truck crates leave the workspace; the rig and threads run on SMLib alone
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
2026-10-27 02:28:49 -04:00
jyviko 42997a62ed WIP kernel: Body::mesh(&MeshTolerance) as the one mesher (does not build yet)
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
2026-10-27 02:28:47 -04:00
jyviko e1a2571696 kernel-cases: SOLIDCRAFT_KERNEL=truck grades as the default kernel
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
2026-10-27 02:28:47 -04:00
jyviko d672cfa013 kernel: one switch, SOLIDCRAFT_KERNEL, picks the kernel for every operation
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
2026-10-27 02:28:47 -04:00
bherbruck 8cb976b9b2 kernel-cases: watertight checks the display mesh, welding vertices by distance
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>
2026-10-27 02:28:46 -04:00
bherbruck 58ce14f45f kernel-cases: count ranges, a watertight mesh check, and per-case timeouts
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>
2026-10-27 02:28:46 -04:00
jyviko 35ebc816ba kernel-cases: a missing capability's message is the case's own text
The report already says "not supported yet" for the status; the error repeated it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
2026-10-27 02:28:46 -04:00
jyviko bf0761f6b9 kernel-cases: base recipes, missing capabilities, and the commands' own time
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
2026-10-27 02:28:45 -04:00
bherbruck c24b066d25 kernel-cases: an outcome for geometry with no single manifold answer
`"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>
2026-10-27 02:28:45 -04:00
bherbruck 678a70da2c kernel-cases: shells as a graded count, no SMLib verdict where it can't take the record, $OUT
- `"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>
2026-10-27 02:28:45 -04:00
bherbruck 7bb74d1c6b kernel-cases: an smlib feature for the CLI, and no grading of a kernel the build isn't running
- 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>
2026-10-27 02:28:45 -04:00
bherbruck 732fd2ace2 kernel-cases: a regression rig that grades each kernel against the case's own answer
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>
2026-10-27 02:28:44 -04:00
jyviko a8c8f88323 naming: list removed lines before added ones in the corpus diff
A failure now reads as a diff does: the recorded line (-) and then the
line the rebuild gives (+), and the message says which is which.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
2026-10-27 02:28:44 -04:00
jyviko a55196e7ad naming: the corpus rebuilds with the same names and references
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
2026-10-27 02:28:44 -04:00
DuBearte b47f82d406 Reopen the designs open at the last quit (#5542) (#6006)
* 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>
2026-10-27 03:34:04 +00:00
Victor Santos Oliveira a7c1909766 CLI: evaluate scripts on the input design (#6861) (#6872)
* 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>
2026-10-27 02:55:01 +00:00
DuBearte 3726959d82 Control channel: check the token before parsing the whole request (#6643)
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>
2026-10-27 02:04:28 +00:00
yhamil04 ebb6cc53ab Crash reports: capture native crashes on macOS (#2113) (#6593)
* 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>
2026-10-27 00:14:00 +00:00
The Nomad afd6a5c584 Send to Slicer: send the selected bodies to a slicer (#2595) (#3600)
* 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
2026-10-26 19:59:35 -04:00
MAKAO 30f951ecf9 Navigation bridge: a device's own app moves the view (#6094)
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>
2026-10-26 22:14:52 +00:00
raykholo 66b872a749 Web app: open a design from a same-origin path (#6670)
?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.
2026-10-26 12:23:52 +00:00
Mateusz Sych d895fef354 Control channel: a pushed design.changed event for clients that subscribe (#6985) (#6986)
* 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>
2026-10-26 11:10:03 +00:00
Colin Faulkingham 03bf992bc1 Keep linked components valid through Undo after Save As (#6860) (#6877)
* claim: #6860

* fix: preserve linked component targets across Save As history (#6860)
2026-10-26 11:08:32 +00:00
raykholo c264c7a42a Startup falls back from a Direct3D 12 that can't build the UI pipeline (#6648) (#6682) 2026-10-26 10:44:13 +00:00